diff --git a/.codeclimate.yml b/.codeclimate.yml index 2d3659da46..81b8bd3c4c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,4 +5,4 @@ languages: PHP: true exclude_paths: - "public/vendor/*" -- "tests/*" \ No newline at end of file +- "test/*" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..0799652254 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,20 @@ +node_modules/ +public/src/nodebb.min.js +*.sublime-project +*.sublime-workspace +.project +.vagrant +.DS_Store +logs/ +/public/templates +/public/uploads +/public/sounds +/public/vendor +/public/nodebb.min.js +/public/acp.min.js +/public/src/modules/string.js +.idea/ +.vscode/ +*.ipr +*.iws +/coverage diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000..ccbf4944a3 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,112 @@ +{ + "extends": "airbnb", + + "rules": { + "handle-callback-err": [ "error","^(e$|(e|(.*(_e|E)))rr)" ], + "linebreak-style": "off", + "one-var": "off", + "no-undef": "off", + "max-len": "off", + "no-new": "off", + "max-nested-callbacks": "off", + "no-mixed-requires": "off", + "brace-style": "off", + "max-statements-per-line": "off", + "no-unused-vars": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-concat": "off", + "require-jsdoc": "off", + "eqeqeq": "off", + "camelcase": "off", + "no-negated-condition": "off", + "one-var-declaration-per-line": "off", + "new-cap": "off", + "no-lonely-if": "off", + "radix": "off", + "no-else-return": "off", + "no-useless-escape": "off", + "block-scoped-var": "off", + "operator-assignment": "off", + "default-case": "off", + "yoda": "off", + "no-use-before-define": "off", + "no-loop-func": "off", + "no-void": "off", + "valid-jsdoc": "off", + "o-eq-null": "off", + "no-cond-assign": "off", + "no-eq-null": "off", + "no-redeclare": "off", + "no-unreachable": "off", + "no-nested-ternary": "off", + "operator-linebreak": "off", + "guard-for-in": "off", + "no-unneeded-ternary": "off", + "no-sequences": "off", + "no-extend-native": "off", + "no-shadow-restricted-names": "off", + "no-extra-boolean-cast": "off", + "no-undef-init": "off", + "no-script-url": "off", + "no-path-concat": "off", + "no-unused-expressions": "off", + "no-restricted-module": "off", + "no-return-assign": "off", + "no-restricted-modules": "off", + "no-tabs": "off", + "indent": "off", + "func-names": "off", + "prefer-arrow-callback": "off", + "object-curly-spacing": "off", + "no-var": "off", + "no-shadow": "off", + "prefer-template": "off", + "padded-blocks": "off", + "eol-last": "off", + "lines-around-directive": "off", + "space-before-blocks": "off", + "no-restricted-syntax": "off", + "vars-on-top": "off", + "no-prototype-builtins": "off", + "object-shorthand": "off", + "no-param-reassign": "off", + "consistent-return": "off", + "strict": "off", + "comma-dangle": "off", + "no-multi-spaces": "off", + "quotes": "off", + "keyword-spacing": "off", + "no-plusplus": "off", + "no-mixed-operators": "off", + "semi": "off", + "comma-spacing": "off", + "global-require": "off", + "no-trailing-spaces": "off", + "key-spacing": "off", + "import/newline-after-import": "off", + "no-underscore-dangle": "off", + "prefer-spread": "off", + "no-multiple-empty-lines": "off", + "spaced-comment": "off", + "prefer-rest-params": "off", + "space-in-parens": "off", + "block-spacing": "off", + "quote-props": "off", + "no-console": "off", + "space-unary-ops": "off", + "import/no-dynamic-require": "off", + "semi-spacing": "off", + "no-bitwise": "off", + "no-empty": "off", + "array-bracket-spacin": "off", + "dot-notation": "off", + "func-call-spacing": "off", + "newline-per-chained-call": "off", + "newline-per-chained-call": "off", + "array-bracket-spacing": "off", + "object-property-newline": "off", + "no-continue": "off", + "no-extra-semi": "off", + "no-spaced-func": "off" + } +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..28dd965678 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +Please include the following information when submitting a bug report/issue: + +* NodeBB version and git hash (to find your git hash, execute `git rev-parse HEAD` from the main NodeBB directory) +* Database (mongo or redis) and it's version. +* Exact steps to cause this issue + 1. First I did this... + 2. Then, I clicked on this item... +* What you expected + * e.g. I expected *abc* to *xyz* +* What happened instead + * e.g. Instead, I got *zyx* and NodeBB set fire to my house + +Thank you! diff --git a/.gitignore b/.gitignore index a8ea9cb1eb..510c95a201 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ npm-debug.log node_modules/ sftp-config.json config.json +jsconfig.json public/src/nodebb.min.js !src/views/config.json public/css/*.css @@ -15,6 +16,7 @@ provision.sh *.komodoproject .DS_Store feeds/recent.rss +.eslintcache logs/ @@ -50,4 +52,4 @@ tx.exe .transifexrc ##Coverage output -coverage \ No newline at end of file +coverage diff --git a/.travis.yml b/.travis.yml index ffd3716c2a..e89064595e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,13 +5,15 @@ before_install: - "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list" - "sudo apt-get update" - "sudo apt-get install mongodb-org-server" - - npm i --production - - node app --setup="{\"url\":\"http://127.0.0.1:4567/\",\"secret\":\"abcdef\",\"database\":\"mongo\",\"mongo:host\":\"127.0.0.1\",\"mongo:port\":27017,\"mongo:username\":\"\",\"mongo:password\":\"\",\"mongo:database\":0,\"redis:host\":\"127.0.0.1\",\"redis:port\":6379,\"redis:password\":\"\",\"redis:database\":0,\"admin:username\":\"admin\",\"admin:email\":\"test@example.org\",\"admin:password\":\"abcdef\",\"admin:password:confirm\":\"abcdef\"}" --ci="{\"host\":\"127.0.0.1\",\"port\":27017,\"database\":0}" -before_script: + - "npm i --production" + - sh -c "if [ '$DB' = 'mongodb' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567/\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"mongo\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":27017,\\\"database\\\":0}\"; fi" + - sh -c "if [ '$DB' = 'redis' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567/\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"redis\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":6379,\\\"database\\\":0}\"; fi" +before_script: - "until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done" language: node_js env: - - CXX=g++-4.8 + - CXX=g++-4.8 DB=mongodb + - CXX=g++-4.8 DB=redis addons: apt: sources: @@ -19,11 +21,9 @@ addons: packages: - g++-4.8 node_js: - - "4.2" - - "4.1" - - "4.0" - - "0.11" - - "0.10" + - "6" + - "5" + - "4" branches: only: - master \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index a6aca2083a..9f1585f301 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,16 +6,17 @@ var fork = require('child_process').fork, incomplete = []; -module.exports = function(grunt) { +module.exports = function (grunt) { + var args = []; + if (!grunt.option('verbose')) { + args.push('--log-level=info'); + } + function update(action, filepath, target) { - var args = [], + var updateArgs = args.slice(), fromFile = '', compiling = '', time = Date.now(); - - if (!grunt.option('verbose')) { - args.push('--log-level=info'); - } if (target === 'lessUpdated_Client') { fromFile = ['js', 'tpl', 'acpLess']; @@ -33,17 +34,17 @@ module.exports = function(grunt) { fromFile = ['clientLess', 'acpLess', 'js', 'tpl']; } - fromFile = fromFile.filter(function(ext) { + fromFile = fromFile.filter(function (ext) { return incomplete.indexOf(ext) === -1; }); - args.push('--from-file=' + fromFile.join(',')); + updateArgs.push('--from-file=' + fromFile.join(',')); incomplete.push(compiling); worker.kill(); - worker = fork('app.js', args, { env: env }); + worker = fork('app.js', updateArgs, { env: env }); - worker.on('message', function() { + worker.on('message', function () { if (incomplete.length) { incomplete = []; @@ -101,6 +102,6 @@ module.exports = function(grunt) { env.NODE_ENV = 'development'; - worker = fork('app.js', [], { env: env }); + worker = fork('app.js', args, { env: env }); grunt.event.on('watch', update); }; \ No newline at end of file diff --git a/README.md b/README.md index 200a444b6d..70e2c9773b 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,13 @@ Additional functionality is enabled through the use of third-party plugins. [](http://i.imgur.com/LmHtPho.png) [](http://i.imgur.com/paiJPJk.jpg) -[](http://i.imgur.com/8OLssij.png) -[](http://i.imgur.com/JKOc0LZ.png) +[](http://i.imgur.com/HwNEXGu.png) +[](http://i.imgur.com/II1byYs.png) + + ## How can I follow along/contribute? -* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/NodeBB/NodeBB/wiki/Version-History-%26-Roadmap) * If you are a developer, feel free to check out the source and submit pull requests. We also have a wide array of [plugins](http://community.nodebb.org/category/7/nodebb-plugins) which would be a great starting point for learning the codebase. * If you are a designer, [NodeBB needs themes](http://community.nodebb.org/category/10/nodebb-themes)! NodeBB's theming system allows extention of the base templates as well as styling via LESS or CSS. NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether. * If you know languages other than English you can help us translate NodeBB. We use [Transifex](https://www.transifex.com/projects/p/nodebb/) for internationalization. diff --git a/app.js b/app.js index ed5238285e..82ffd2f7b1 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ /* NodeBB - A better forum platform for the modern web https://github.com/NodeBB/NodeBB/ - Copyright (C) 2013-2014 NodeBB Inc. + Copyright (C) 2013-2016 NodeBB Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -36,7 +36,7 @@ global.env = process.env.NODE_ENV || 'production'; winston.remove(winston.transports.Console); winston.add(winston.transports.Console, { colorize: true, - timestamp: function() { + timestamp: function () { var date = new Date(); return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, @@ -51,13 +51,14 @@ if (nconf.get('config')) { configFile = path.resolve(__dirname, nconf.get('config')); } -var configExists = file.existsSync(configFile); +var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); loadConfig(); +versionCheck(); if (!process.send) { // If run using `node app`, log GNU copyright info along with server info - winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-2014 NodeBB Inc.'); + winston.info('NodeBB v' + nconf.get('version') + ' Copyright (C) 2013-' + (new Date()).getFullYear() + ' NodeBB Inc.'); winston.info('This program comes with ABSOLUTELY NO WARRANTY.'); winston.info('This is free software, and you are welcome to redistribute it under certain conditions.'); winston.info(''); @@ -103,6 +104,10 @@ function loadConfig() { nconf.set('themes_path', path.resolve(__dirname, nconf.get('themes_path'))); nconf.set('core_templates_path', path.join(__dirname, 'src/views')); nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); + + if (nconf.get('url')) { + nconf.set('url_parsed', url.parse(nconf.get('url'))); + } } @@ -113,15 +118,18 @@ function start() { if (!nconf.get('upload_path')) { nconf.set('upload_path', '/public/uploads'); } + if (!nconf.get('sessionKey')) { + nconf.set('sessionKey', 'express.sid'); + } // Parse out the relative_url and other goodies from the configured URL var urlObject = url.parse(nconf.get('url')); var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); - nconf.set('secure', urlObject.protocol === 'https'); + nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('use_port', !!urlObject.port); nconf.set('relative_path', relativePath); - nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || 4567); - nconf.set('upload_url', '/uploads/'); + nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + nconf.set('upload_url', nconf.get('upload_path').replace(/^\/public/, '')); if (nconf.get('isPrimary') === 'true') { winston.info('Time: %s', (new Date()).toString()); @@ -138,7 +146,7 @@ function start() { process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); process.on('SIGHUP', restart); - process.on('message', function(message) { + process.on('message', function (message) { if (typeof message !== 'object') { return; } @@ -165,7 +173,7 @@ function start() { } }); - process.on('uncaughtException', function(err) { + process.on('uncaughtException', function (err) { winston.error(err.stack); console.log(err.stack); @@ -176,16 +184,21 @@ function start() { async.waterfall([ async.apply(db.init), async.apply(db.checkCompatibility), - function(next) { + function (next) { require('./src/meta').configs.init(next); }, - function(next) { - require('./src/meta').dependencies.check(next); + function (next) { + if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) { + require('./src/meta').dependencies.check(next); + } else { + winston.warn('[init] Dependency checking skipped!'); + setImmediate(next); + } }, - function(next) { + function (next) { require('./src/upgrade').check(next); }, - function(next) { + function (next) { var webserver = require('./src/webserver'); require('./src/socket.io').init(webserver.server); @@ -196,7 +209,7 @@ function start() { webserver.listen(); } - ], function(err) { + ], function (err) { if (err) { switch(err.message) { case 'schema-out-of-date': @@ -207,6 +220,10 @@ function start() { winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:'); winston.warn(' ./nodebb upgrade'); break; + case 'dependencies-missing': + winston.warn('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:'); + winston.warn(' ./nodebb upgrade'); + break; default: if (err.stacktrace !== false) { winston.error(err.stack); @@ -234,7 +251,7 @@ function setup() { install.setup(function (err, data) { var separator = ' '; if (process.stdout.columns > 10) { - for(var x=0,cols=process.stdout.columns-10;x<cols;x++) { + for(var x = 0,cols = process.stdout.columns - 10;x < cols;x++) { separator += '='; } } @@ -263,7 +280,7 @@ function setup() { } function upgrade() { - require('./src/database').init(function(err) { + require('./src/database').init(function (err) { if (err) { winston.error(err.stack); process.exit(); @@ -275,7 +292,12 @@ function upgrade() { } function activate() { - require('./src/database').init(function(err) { + require('./src/database').init(function (err) { + if (err) { + winston.error(err.stack); + process.exit(1); + } + var plugin = nconf.get('_')[1] ? nconf.get('_')[1] : nconf.get('activate'), db = require('./src/database'); @@ -286,10 +308,20 @@ function activate() { } function listPlugins() { - require('./src/database').init(function(err) { + require('./src/database').init(function (err) { + if (err) { + winston.error(err.stack); + process.exit(1); + } + var db = require('./src/database'); - db.getSortedSetRange('plugins:active', 0, -1, function(err, plugins) { + db.getSortedSetRange('plugins:active', 0, -1, function (err, plugins) { + if (err) { + winston.error(err.stack); + process.exit(1); + } + winston.info('Active plugins: \n\t - ' + plugins.join('\n\t - ')); process.exit(); }); @@ -319,3 +351,15 @@ function restart() { shutdown(1); } } + +function versionCheck() { + var version = process.version.slice(1); + var range = pkg.engines.node; + var semver = require('semver'); + var compatible = semver.satisfies(version, range); + + if (!compatible) { + winston.warn('Your version of Node.js is too outdated for NodeBB. Please update your version of Node.js.'); + winston.warn('Recommended ' + range.green + ', '.reset + version.yellow + ' provided\n'.reset); + } +} \ No newline at end of file diff --git a/bcrypt.js b/bcrypt.js index ae81d09eb7..4040aa2a8f 100644 --- a/bcrypt.js +++ b/bcrypt.js @@ -5,7 +5,7 @@ var bcrypt = require('bcryptjs'), async = require('async'); -process.on('message', function(msg) { +process.on('message', function (msg) { if (msg.type === 'hash') { hashPassword(msg.password, msg.rounds); } else if (msg.type === 'compare') { @@ -15,10 +15,10 @@ process.on('message', function(msg) { function hashPassword(password, rounds) { async.waterfall([ - function(next) { + function (next) { bcrypt.genSalt(parseInt(rounds, 10), next); }, - function(salt, next) { + function (salt, next) { bcrypt.hash(password, salt, next); } ], done); diff --git a/install/data/categories.json b/install/data/categories.json index a86ff24e7c..58ac6e8d40 100644 --- a/install/data/categories.json +++ b/install/data/categories.json @@ -2,6 +2,7 @@ { "name": "Announcements", "description": "Announcements regarding our community", + "descriptionParsed": "<p>Announcements regarding our community</p>\n", "bgColor": "#fda34b", "color": "#fff", "icon" : "fa-bullhorn", @@ -10,6 +11,7 @@ { "name": "General Discussion", "description": "A place to talk about whatever you want", + "descriptionParsed": "<p>A place to talk about whatever you want</p>\n", "bgColor": "#59b3d0", "color": "#fff", "icon" : "fa-comments-o", @@ -18,6 +20,7 @@ { "name": "Blogs", "description": "Blog posts from individual members", + "descriptionParsed": "<p>Blog posts from individual members</p>\n", "bgColor": "#86ba4b", "color": "#fff", "icon" : "fa-newspaper-o", @@ -26,6 +29,7 @@ { "name": "Comments & Feedback", "description": "Got a question? Ask away!", + "descriptionParsed": "<p>Got a question? Ask away!</p>\n", "bgColor": "#e95c5a", "color": "#fff", "icon" : "fa-question", diff --git a/install/data/defaults.json b/install/data/defaults.json index 5041ff83ea..c471db6b89 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -9,6 +9,8 @@ "maximumPostLength": 32767, "minimumTagsPerTopic": 0, "maximumTagsPerTopic": 5, + "minimumTagLength": 3, + "maximumTagLength": 15, "allowGuestSearching": 0, "allowTopicsThumbnail": 0, "registrationType": "normal", @@ -29,6 +31,9 @@ "profileImageDimension": 128, "requireEmailConfirmation": 0, "allowProfileImageUploads": 1, - "teaserPost": "last", - "allowPrivateGroups": 1 -} \ No newline at end of file + "teaserPost": "last-reply", + "allowPrivateGroups": 1, + "unreadCutoff": 2, + "bookmarkThreshold": 5, + "topicsPerList": 20 +} diff --git a/install/data/footer.json b/install/data/footer.json index 7c71cad648..12528110c6 100644 --- a/install/data/footer.json +++ b/install/data/footer.json @@ -2,7 +2,7 @@ { "widget": "html", "data" : { - "html": "<footer id=\"footer\" class=\"container footer\">\r\n\t<div class=\"copyright\">\r\n\t\tCopyright © 2015 <a target=\"_blank\" href=\"https://nodebb.org\">NodeBB Forums</a> | <a target=\"_blank\" href=\"//github.com/NodeBB/NodeBB/graphs/contributors\">Contributors</a>\r\n\t</div>\r\n</footer>", + "html": "<footer id=\"footer\" class=\"container footer\">\r\n\t<div class=\"copyright\">\r\n\t\tCopyright © 2016 <a target=\"_blank\" href=\"https://nodebb.org\">NodeBB Forums</a> | <a target=\"_blank\" href=\"//github.com/NodeBB/NodeBB/graphs/contributors\">Contributors</a>\r\n\t</div>\r\n</footer>", "title":"", "container":"" } diff --git a/install/databases.js b/install/databases.js index 8c5a76555b..c314ad75ef 100644 --- a/install/databases.js +++ b/install/databases.js @@ -9,7 +9,7 @@ var questions = { mongo: require('../src/database/mongo').questions }; -module.exports = function(config, callback) { +module.exports = function (config, callback) { async.waterfall([ function (next) { process.stdout.write('\n'); @@ -74,7 +74,7 @@ function saveDatabaseConfig(config, databaseConfig, callback) { } var allQuestions = questions.redis.concat(questions.mongo); - for (var x=0; x<allQuestions.length; x++) { + for (var x = 0; x < allQuestions.length; x++) { delete config[allQuestions[x].name]; } diff --git a/install/web.js b/install/web.js index 7d2cfad21a..72284fea5f 100644 --- a/install/web.js +++ b/install/web.js @@ -1,36 +1,36 @@ "use strict"; -var winston = require('winston'), - express = require('express'), - bodyParser = require('body-parser'), - fs = require('fs'), - path = require('path'), - less = require('less'), - async = require('async'), - uglify = require('uglify-js'), - nconf = require('nconf'), - app = express(), - server; +var winston = require('winston'); +var express = require('express'); +var bodyParser = require('body-parser'); +var fs = require('fs'); +var path = require('path'); +var less = require('less'); +var async = require('async'); +var uglify = require('uglify-js'); +var nconf = require('nconf'); +var app = express(); +var server; winston.add(winston.transports.File, { filename: 'logs/webinstall.log', colorize: true, - timestamp: function() { + timestamp: function () { var date = new Date(); return date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; }, level: 'verbose' }); -var web = {}, - scripts = [ - 'public/vendor/xregexp/xregexp.js', - 'public/vendor/xregexp/unicode/unicode-base.js', - 'public/src/utils.js', - 'public/src/installer/install.js' - ]; +var web = {}; +var scripts = [ + 'public/vendor/xregexp/xregexp.js', + 'public/vendor/xregexp/unicode/unicode-base.js', + 'public/src/utils.js', + 'public/src/installer/install.js' +]; -web.install = function(port) { +web.install = function (port) { port = port || 4567; winston.info('Launching web installer on port', port); @@ -42,7 +42,7 @@ web.install = function(port) { extended: true })); - async.parallel([compileLess, compileJS], function() { + async.parallel([compileLess, compileJS], function () { setupRoutes(); launchExpress(port); }); @@ -50,7 +50,7 @@ web.install = function(port) { function launchExpress(port) { - server = app.listen(port, function() { + server = app.listen(port, function () { winston.info('Web installer listening on http://%s:%s', '0.0.0.0', port); }); } @@ -62,22 +62,23 @@ function setupRoutes() { } function welcome(req, res) { - var dbs = ['redis', 'mongo'], - databases = []; - - dbs.forEach(function(el) { - databases.push({ + var dbs = ['redis', 'mongo']; + var databases = dbs.map(function (el) { + return { name: el, questions: require('../src/database/' + el).questions - }); + }; }); + var defaults = require('./data/defaults'); + res.render('install/index', { databases: databases, skipDatabaseSetup: !!nconf.get('database'), error: res.locals.error ? true : false, success: res.locals.success ? true : false, - values: req.body + values: req.body, + minimumPasswordLength: defaults.minimumPasswordLength }); } @@ -92,7 +93,7 @@ function install(req, res) { env: process.env }); - child.on('close', function(data) { + child.on('close', function (data) { if (data === 0) { res.locals.success = true; } else { @@ -104,7 +105,6 @@ function install(req, res) { } function launch(req, res) { - var pidFilePath = __dirname + '../pidfile'; res.json({}); server.close(); @@ -129,8 +129,12 @@ function compileLess(callback) { return callback(false); } - fs.readFile(path.join(__dirname, '../public/less/install.less'), function(err, style) { - less.render(style.toString(), function(err, css) { + fs.readFile(path.join(__dirname, '../public/less/install.less'), function (err, style) { + if (err) { + return winston.error('Unable to read LESS install file: ', err); + } + + less.render(style.toString(), function (err, css) { if(err) { return winston.error('Unable to compile LESS: ', err); } @@ -146,10 +150,10 @@ function compileJS(callback) { return callback(false); } - var scriptPath = path.join(__dirname, '..'), - result = uglify.minify(scripts.map(function(script) { - return path.join(scriptPath, script); - })); + var scriptPath = path.join(__dirname, '..'); + var result = uglify.minify(scripts.map(function (script) { + return path.join(scriptPath, script); + })); fs.writeFile(path.join(__dirname, '../public/nodebb.min.js'), result.code, callback); diff --git a/loader.js b/loader.js index b78abdc4a1..02a668c174 100644 --- a/loader.js +++ b/loader.js @@ -29,12 +29,13 @@ var pidFilePath = __dirname + '/pidfile', css: { cache: undefined, acpCache: undefined - } + }, + templatesCompiled: false }; -Loader.init = function(callback) { +Loader.init = function (callback) { if (silent) { - console.log = function() { + console.log = function () { var args = Array.prototype.slice.call(arguments); output.write(args.join(' ') + '\n'); }; @@ -46,7 +47,7 @@ Loader.init = function(callback) { callback(); }; -Loader.displayStartupMessages = function(callback) { +Loader.displayStartupMessages = function (callback) { console.log(''); console.log('NodeBB v' + pkg.version + ' Copyright (C) 2013-2014 NodeBB Inc.'); console.log('This program comes with ABSOLUTELY NO WARRANTY.'); @@ -56,25 +57,25 @@ Loader.displayStartupMessages = function(callback) { callback(); }; -Loader.addWorkerEvents = function(worker) { +Loader.addWorkerEvents = function (worker) { - worker.on('exit', function(code, signal) { + worker.on('exit', function (code, signal) { if (code !== 0) { - if (Loader.timesStarted < numProcs*3) { + if (Loader.timesStarted < numProcs * 3) { Loader.timesStarted++; if (Loader.crashTimer) { clearTimeout(Loader.crashTimer); } - Loader.crashTimer = setTimeout(function() { + Loader.crashTimer = setTimeout(function () { Loader.timesStarted = 0; }, 10000); } else { - console.log(numProcs*3 + ' restarts in 10 seconds, most likely an error on startup. Halting.'); + console.log(numProcs * 3 + ' restarts in 10 seconds, most likely an error on startup. Halting.'); process.exit(); } } - console.log('[cluster] Child Process (' + worker.pid + ') has exited (code: ' + code + ', signal: ' + signal +')'); + console.log('[cluster] Child Process (' + worker.pid + ') has exited (code: ' + code + ', signal: ' + signal + ')'); if (!(worker.suicide || code === 0)) { console.log('[cluster] Spinning up another process...'); @@ -82,25 +83,14 @@ Loader.addWorkerEvents = function(worker) { } }); - worker.on('message', function(message) { + worker.on('message', function (message) { if (message && typeof message === 'object' && message.action) { switch (message.action) { case 'ready': - if (Loader.js.target['nodebb.min.js'] && Loader.js.target['nodebb.min.js'].cache && !worker.isPrimary) { - worker.send({ - action: 'js-propagate', - cache: Loader.js.target['nodebb.min.js'].cache, - map: Loader.js.target['nodebb.min.js'].map, - target: 'nodebb.min.js' - }); - } - - if (Loader.js.target['acp.min.js'] && Loader.js.target['acp.min.js'].cache && !worker.isPrimary) { + if (Loader.js.target['nodebb.min.js'] && Loader.js.target['acp.min.js'] && !worker.isPrimary) { worker.send({ action: 'js-propagate', - cache: Loader.js.target['acp.min.js'].cache, - map: Loader.js.target['acp.min.js'].map, - target: 'acp.min.js' + data: Loader.js.target }); } @@ -112,6 +102,12 @@ Loader.addWorkerEvents = function(worker) { }); } + if (Loader.templatesCompiled && !worker.isPrimary) { + worker.send({ + action: 'templates:compiled' + }); + } + break; case 'restart': @@ -141,6 +137,8 @@ Loader.addWorkerEvents = function(worker) { }, worker.pid); break; case 'templates:compiled': + Loader.templatesCompiled = true; + Loader.notifyWorkers({ action: 'templates:compiled', }, worker.pid); @@ -150,11 +148,11 @@ Loader.addWorkerEvents = function(worker) { }); }; -Loader.start = function(callback) { +Loader.start = function (callback) { numProcs = getPorts().length; console.log('Clustering enabled: Spinning up ' + numProcs + ' process(es).\n'); - for (var x=0; x<numProcs; ++x) { + for (var x = 0; x < numProcs; ++x) { forkWorker(x, x === 0); } @@ -171,7 +169,7 @@ function forkWorker(index, isPrimary) { } process.env.isPrimary = isPrimary; - process.env.isCluster = true; + process.env.isCluster = ports.length > 1 ? true : false; process.env.port = ports[index]; var worker = fork('app.js', [], { @@ -207,21 +205,22 @@ function getPorts() { return port; } -Loader.restart = function() { +Loader.restart = function () { killWorkers(); - + nconf.remove('file'); + nconf.use('file', { file: path.join(__dirname, '/config.json') }); Loader.start(); }; -Loader.reload = function() { - workers.forEach(function(worker) { +Loader.reload = function () { + workers.forEach(function (worker) { worker.send({ action: 'reload' }); }); }; -Loader.stop = function() { +Loader.stop = function () { killWorkers(); // Clean up the pidfile @@ -229,15 +228,15 @@ Loader.stop = function() { }; function killWorkers() { - workers.forEach(function(worker) { + workers.forEach(function (worker) { worker.suicide = true; worker.kill(); }); } -Loader.notifyWorkers = function(msg, worker_pid) { +Loader.notifyWorkers = function (msg, worker_pid) { worker_pid = parseInt(worker_pid, 10); - workers.forEach(function(worker) { + workers.forEach(function (worker) { if (parseInt(worker.pid, 10) !== worker_pid) { try { worker.send(msg); @@ -248,7 +247,7 @@ Loader.notifyWorkers = function(msg, worker_pid) { }); }; -fs.open(path.join(__dirname, 'config.json'), 'r', function(err) { +fs.open(path.join(__dirname, 'config.json'), 'r', function (err) { if (!err) { if (nconf.get('daemon') !== 'false' && nconf.get('daemon') !== false) { if (file.existsSync(pidFilePath)) { @@ -273,7 +272,7 @@ fs.open(path.join(__dirname, 'config.json'), 'r', function(err) { Loader.init, Loader.displayStartupMessages, Loader.start - ], function(err) { + ], function (err) { if (err) { console.log('[loader] Error during startup: ' + err.message); } diff --git a/minifier.js b/minifier.js index 50106e2957..25c0177175 100644 --- a/minifier.js +++ b/minifier.js @@ -12,18 +12,18 @@ var Minifier = { /* Javascript */ Minifier.js.minify = function (scripts, minify, callback) { - scripts = scripts.filter(function(file) { + scripts = scripts.filter(function (file) { return file && file.endsWith('.js'); }); - async.filter(scripts, function(script, next) { - file.exists(script, function(exists) { + async.filter(scripts, function (script, next) { + file.exists(script, function (exists) { if (!exists) { console.warn('[minifier] file not found, ' + script); } next(exists); }); - }, function(scripts) { + }, function (scripts) { if (minify) { minifyScripts(scripts, callback); } else { @@ -32,10 +32,10 @@ Minifier.js.minify = function (scripts, minify, callback) { }); }; -process.on('message', function(payload) { +process.on('message', function (payload) { switch(payload.action) { case 'js': - Minifier.js.minify(payload.scripts, payload.minify, function(minified/*, sourceMap*/) { + Minifier.js.minify(payload.scripts, payload.minify, function (minified/*, sourceMap*/) { process.send({ type: 'end', // sourceMap: sourceMap, @@ -65,7 +65,7 @@ function minifyScripts(scripts, callback) { } function concatenateScripts(scripts, callback) { - async.map(scripts, fs.readFile, function(err, scripts) { + async.map(scripts, fs.readFile, function (err, scripts) { if (err) { process.send({ type: 'error', diff --git a/nodebb b/nodebb index 417c6a9d80..53ae28db22 100755 --- a/nodebb +++ b/nodebb @@ -1,14 +1,25 @@ #!/usr/bin/env node -var colors = require('colors'), - cproc = require('child_process'), - argv = require('minimist')(process.argv.slice(2)), - fs = require('fs'), - path = require('path'), - request = require('request'), - semver = require('semver'), - prompt = require('prompt'), - async = require('async'); +try { + var colors = require('colors'), + cproc = require('child_process'), + argv = require('minimist')(process.argv.slice(2)), + fs = require('fs'), + path = require('path'), + request = require('request'), + semver = require('semver'), + prompt = require('prompt'), + async = require('async'); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + process.stdout.write('NodeBB could not be started because it\'s dependencies have not been installed.\n'); + process.stdout.write('Please ensure that you have executed "npm install --production" prior to running NodeBB.\n\n'); + process.stdout.write('For more information, please see: https://docs.nodebb.org/en/latest/installing/os.html\n\n'); + process.stdout.write('Could not start: ' + e.code + '\n'); + + process.exit(1); + } +} var getRunningPid = function(callback) { fs.readFile(__dirname + '/pidfile', { @@ -118,12 +129,13 @@ var getRunningPid = function(callback) { version: async.apply(getCurrentVersion) }), function(payload, next) { - if (!payload.plugins.length) { + var toCheck = Object.keys(payload.plugins); + + if (!toCheck.length) { process.stdout.write('OK'.green + '\n'.reset); return next(null, []); // no extraneous plugins installed } - var toCheck = Object.keys(payload.plugins); request({ method: 'GET', url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='), @@ -195,7 +207,7 @@ var getRunningPid = function(callback) { description: 'Proceed with upgrade (y|n)?'.reset, type: 'string' }, function(err, result) { - if (result.upgrade === 'y' || result.upgrade === 'yes') { + if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) { process.stdout.write('\nUpgrading packages...'); var args = ['npm', 'i']; found.forEach(function(suggestObj) { diff --git a/package.json b/package.json index 151660ddf2..43a6f46567 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.0.2", + "version": "1.2.1", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -11,30 +11,36 @@ "main": "app.js", "scripts": { "start": "node loader.js", - "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- ./tests -t 10000" + "lint": "eslint --cache .", + "pretest": "npm run lint", + "test": "istanbul cover _mocha test", + "test-windows": "_mocha test" }, "dependencies": { "async": "~1.5.0", "autoprefixer": "^6.2.3", "bcryptjs": "~2.3.0", "body-parser": "^1.9.0", + "chart.js": "^2.1.0", "colors": "^1.1.0", "compression": "^1.1.0", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "~1.1.0", "connect-multiparty": "^2.0.0", - "connect-redis": "~3.0.2", + "connect-redis": "~3.1.0", "cookie-parser": "^1.3.3", "cron": "^1.0.5", "csurf": "^1.6.1", "daemon": "~1.1.0", - "express": "^4.9.5", + "express": "^4.14.0", "express-session": "^1.8.2", "express-useragent": "0.2.4", "html-to-text": "2.0.0", "ip": "1.1.2", "jimp": "0.2.21", + "jquery": "^3.1.0", + "json-2-csv": "^2.0.22", "less": "^2.0.0", "logrotate-stream": "^0.2.3", "lru-cache": "4.0.0", @@ -43,40 +49,43 @@ "mkdirp": "~0.5.0", "mongodb": "~2.1.3", "morgan": "^1.3.2", + "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "3.0.18", - "nodebb-plugin-dbsearch": "1.0.1", - "nodebb-plugin-emoji-extended": "1.0.3", - "nodebb-plugin-markdown": "5.0.1", - "nodebb-plugin-mentions": "1.0.21", + "nodebb-plugin-composer-default": "4.2.9", + "nodebb-plugin-dbsearch": "1.0.2", + "nodebb-plugin-emoji-extended": "1.1.1", + "nodebb-plugin-emoji-one": "1.1.5", + "nodebb-plugin-markdown": "6.0.2", + "nodebb-plugin-mentions": "1.1.3", "nodebb-plugin-soundpack-default": "0.1.6", - "nodebb-plugin-spam-be-gone": "0.4.6", - "nodebb-rewards-essentials": "0.0.8", - "nodebb-theme-lavender": "3.0.9", - "nodebb-theme-persona": "4.0.115", - "nodebb-theme-vanilla": "5.0.61", - "nodebb-widget-essentials": "2.0.9", + "nodebb-plugin-spam-be-gone": "0.4.10", + "nodebb-rewards-essentials": "0.0.9", + "nodebb-theme-lavender": "3.0.15", + "nodebb-theme-persona": "4.1.63", + "nodebb-theme-vanilla": "5.1.43", + "nodebb-widget-essentials": "2.0.12", "nodemailer": "2.0.0", "nodemailer-sendmail-transport": "1.0.0", "nodemailer-smtp-transport": "^2.4.1", "passport": "^0.3.0", "passport-local": "1.0.0", "postcss": "^5.0.13", + "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", - "redis": "~2.4.2", + "redis": "~2.6.2", "request": "^2.44.0", "rimraf": "~2.5.0", "rss": "^1.0.0", - "semver": "^5.0.1", + "semver": "^5.1.0", "serve-favicon": "^2.1.5", "sitemap": "^1.4.0", - "socket.io": "^1.4.0", + "socket.io": "^1.4.8", "socket.io-client": "^1.4.0", - "socket.io-redis": "^1.0.0", + "socket.io-redis": "1.1.1", "socketio-wildcard": "~0.3.0", "string": "^3.0.0", "templates.js": "0.3.4", - "toobusy-js": "^0.4.2", + "toobusy-js": "^0.5.1", "uglify-js": "^2.6.0", "underscore": "^1.8.3", "underscore.deep": "^0.5.1", @@ -85,16 +94,21 @@ "xregexp": "~3.1.0" }, "devDependencies": { + "eslint": "^3.7.1", + "eslint-config-airbnb": "^12.0.0", + "eslint-plugin-import": "^1.16.0", + "eslint-plugin-jsx-a11y": "^2.2.3", + "eslint-plugin-react": "^6.4.1", "grunt": "~0.4.5", "grunt-contrib-watch": "^1.0.0", "istanbul": "^0.4.2", - "mocha": "~1.13.0" + "mocha": "~3.1.0" }, "bugs": { "url": "https://github.com/NodeBB/NodeBB/issues" }, "engines": { - "node": ">=0.10" + "node": ">=4" }, "maintainers": [ { diff --git a/public/503.html b/public/503.html index 124e92ef26..119c710ab7 100644 --- a/public/503.html +++ b/public/503.html @@ -1,7 +1,8 @@ <html> <head> <title>Excessive Load Warning</title> - <link href='http://fonts.googleapis.com/css?family=Ubuntu:400,500,700' rel='stylesheet' type='text/css'> + <link href='https://fonts.googleapis.com/css?family=Ubuntu:400,500,700' rel='stylesheet' type='text/css'> + <meta name="viewport" content="width=device-width, initial-scale=1"> <style type="text/css"> body { background: #00A9EA; @@ -32,6 +33,20 @@ font-size: 28px; } + @media (max-width: 640px) { + h1 { + font-size: 125px; + } + + p { + font-size: 16px; + } + + p strong { + font-size: 20px; + } + } + .center { position: relative; top: 50%; @@ -148,11 +163,13 @@ <div class="center"> <h1 id="click-me" class="animated bounce">503</h1> <p> - <strong>This forum is temporarily unavailable due to excessive load.</strong> <br /> + <strong>This forum is temporarily unavailable due to excessive load.</strong> + </p> + <p> We shouldn't be down for long. Please check back shortly. Sorry for the inconvenience! </p> - <p id="hide" class="hide"> - <small>Alright. You can stop clicking... it's not going to make the site come back sooner!</small> + <p> + <small id="hide" class="hide">Alright. You can stop clicking... it's not going to make the site come back sooner!</small> </p> </div> </div> diff --git a/public/language/ar/category.json b/public/language/ar/category.json index 192d1f02c8..6f61d752c6 100644 --- a/public/language/ar/category.json +++ b/public/language/ar/category.json @@ -2,15 +2,19 @@ "category": "فئة", "subcategories": "فئة فرعية", "new_topic_button": "موضوع جديد", - "guest-login-post": "يجب عليك تسجيل الدخول للرد", + "guest-login-post": "سجل بالدخول للرد", "no_topics": "<strong>لا توجد مواضيع في هذه الفئة</strong>لم لا تحاول إنشاء موضوع؟<br />", "browsing": "تصفح", - "no_replies": "لا توجد ردود.", - "no_new_posts": "لا يوجد مشاركات جديدة.", + "no_replies": "لم يرد أحد", + "no_new_posts": "لا توجد مشاركات جديدة.", "share_this_category": "انشر هذه الفئة", - "watch": "متابعة", + "watch": "تابع", "ignore": "تجاهل", - "watch.message": "أنت اﻷن متابع لتحديثات هذه الفئة", - "ignore.message": "أنت اﻷن تتجاهل تحديثات هذه الفئة", - "watched-categories": "Watched categories" + "watching": "متابع", + "ignoring": "متجاهل", + "watching.description": "أظهر المواضيع في غير مقروء", + "ignoring.description": "لا تظهر المواضيع في غير مقروء", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "الفئات المتابعة" } \ No newline at end of file diff --git a/public/language/ar/email.json b/public/language/ar/email.json index 0799f8892e..67171b01a6 100644 --- a/public/language/ar/email.json +++ b/public/language/ar/email.json @@ -21,9 +21,10 @@ "digest.cta": "انقر هنا لمشاهدة %1", "digest.unsub.info": "تم إرسال هذا الإشعار بآخر المستجدات وفقا لخيارات تسجيلكم.", "digest.no_topics": "ليس هناك مواضيع نشيطة في %1 الماضي", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "يوم", + "digest.week": "أسبوع", + "digest.month": "شهر", + "digest.subject": "إستهلاك ل", "notif.chat.subject": "هناك محادثة جديدة من %1", "notif.chat.cta": "انقر هنا لمتابعة المحادثة", "notif.chat.unsub.info": "تم إرسال هذا الإشعار بوجودة محادثة جديدة وفقا لخيارات تسجيلك.", diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 635f404c8c..a1b111af1f 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -1,5 +1,5 @@ { - "invalid-data": "بيانات غير صالحة", + "invalid-data": "بيانات غير صحيحة", "not-logged-in": "لم تقم بتسجيل الدخول", "account-locked": "تم حظر حسابك مؤقتًا.", "search-requires-login": "البحث في المنتدى يتطلب حساب - الرجاء تسجيل الدخول أو التسجيل", @@ -14,20 +14,25 @@ "invalid-password": "كلمة السر غير مقبولة", "invalid-username-or-password": "المرجود تحديد اسم مستخدم و كلمة مرور", "invalid-search-term": "كلمة البحث غير صحيحة", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "لم تتمكن من تسجيل الدخول. هنالك أحتمال ان جلستك انتهت. رجاءًا حاول مرة اخرى.", + "invalid-pagination-value": "رقم الصفحة غير صحيح ، يجب أن يكون بين %1 و %2 .", "username-taken": "اسم المستخدم مأخوذ", "email-taken": "البريد الالكتروني مأخوذ", "email-not-confirmed": "عنوان بريدك الإلكتروني غير مفعل بعد. انقر هنا لتفعيله من فضلك.", "email-not-confirmed-chat": "لا يمكنك الدردشة حتى تقوم بتأكيد بريدك الإلكتروني، الرجاء إضغط هنا لتأكيد بريدك اﻹلكتروني.", + "email-not-confirmed-email-sent": "بريدك الإلكتروني غير مفعل. رجاءًا تفحص صندوق البريد عن رسالة التفعيل.", "no-email-to-confirm": "هذا المنتدى يستلزم تفعيل بريدك الإلكتروني، انقر هنا من فضلك لإدخاله.", "email-confirm-failed": "لم نستطع تفعيل بريدك الإلكتروني، المرجو المحاولة لاحقًا.", "confirm-email-already-sent": "لقد تم ارسال بريد التأكيد، الرجاء اﻹنتظار 1% دقائق لإعادة اﻹرسال", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "اسم المستخدم قصير.", "username-too-long": "اسم المستخدم طويل", - "password-too-long": "Password too long", + "password-too-long": "كلمة السر طويلة ", "user-banned": "المستخدم محظور", + "user-banned-reason": "المستخدم محظور (السبب: %1)", "user-too-new": "عذرا, يجب أن تنتظر 1% ثواني قبل قيامك بأول مشاركة", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "نأسف، لقد تم حظرك من استخدام وتصفح المنتدى. إذا كنت تعتقد أن هذا خطأ رجاءًا اتصل بالإدارة. ", + "ban-expiry-missing": "رجاءًا ضع تاريخ نهاية الحظر. ", "no-category": "قائمة غير موجودة", "no-topic": "موضوع غير موجود", "no-post": "رد غير موجود", @@ -37,67 +42,90 @@ "no-privileges": "لاتملك الصلاحيات اللازمة للقيام بهذه العملية", "category-disabled": "قائمة معطلة", "topic-locked": "الموضوع مقفول", - "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", - "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", - "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", - "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", - "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "post-edit-duration-expired": "يسمح لك بتعديل مشاركتك حتى %1 ثانية من نشرها", + "post-edit-duration-expired-minutes": "يسمح لك بتعديل مشاركتك حتى %1 دقيقة من نشرها", + "post-edit-duration-expired-minutes-seconds": "يسمح لك بتعديل مشاركتك حتى %1 دقيقة و %2 ثوان من نشرها", + "post-edit-duration-expired-hours": "يسمح لك بتعديل مشاركتك حتى %1 ساعة من نشرها", + "post-edit-duration-expired-hours-minutes": "يسمح لك بتعديل مشاركتك حتى %1 ساعة و %2 دقيقة من نشرها", + "post-edit-duration-expired-days": "يسمح لك بتعديل مشاركتك حتى %1 يوم من نشرها", + "post-edit-duration-expired-days-hours": "يسمح لك بتعديل مشاركتك حتى %1 يوم و %2 ساعة من نشرها", + "post-delete-duration-expired": "يسمح لك بحذف مشاركتك حتى %1 ثانية من نشرها", + "post-delete-duration-expired-minutes": "يسمح لك بحذف مشاركتك حتى %1 دقيقة من نشرها", + "post-delete-duration-expired-minutes-seconds": "يسمح لك بحذف مشاركتك حتى %1 دقيقة و %2 ثوان من نشرها", + "post-delete-duration-expired-hours": "يسمح لك بحذف مشاركتك حتى %1 ساعة من نشرها", + "post-delete-duration-expired-hours-minutes": "يسمح لك بحذف مشاركتك حتى %1 ساعة و %2 دقيقة من نشرها", + "post-delete-duration-expired-days": "يسمح لك بحذف مشاركتك حتى %1 يوم من نشرها", + "post-delete-duration-expired-days-hours": "يسمح لك بحذف مشاركتك حتى %1 يوم و %2 ساعة من نشرها", + "cant-delete-topic-has-reply": "لا يمكنك حذف مشاركة تم الرد عليها", + "cant-delete-topic-has-replies": "لا يمكنك حذف مشاركة حصدت %1 ردود", + "content-too-short": "يرجى ادخال موضوع أطول. على المواضيع أن تحتوي على %1 حرف على الأقل.", + "content-too-long": "يرجى ادخال موضوع أقصر. على المواضيع أن لا تتخطى %1 حرف.", + "title-too-short": "يرجى إدخال عنوان أطول. على العناوين أن تحتوي على %1 حرف على الأقل.", + "title-too-long": "يرجى ادخال عنوان أقصر. على العناوين أن لا تتخطى %1 حرف.", + "category-not-selected": "Category not selected.", + "too-many-posts": "يسمح لك بالنشر مرة كل %1 ثانية - يرجى الإنتظار قبل النشر مجدداً", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", - "still-uploading": "الرجاء انتظار الرفع", - "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "still-uploading": "الرجاء الانتظار حتى يكتمل الرفع.", + "file-too-big": "الحد الأقصى لرفع الملفات %1 كيلو بت. رجاءًا ارفع ملف أصغر", + "guest-upload-disabled": "خاصية رفع الملفات غير مفعلة للزوار.", + "already-favourited": "سبق وفضلت هذه المشاركة.", + "already-unfavourited": "لقد ازلت هذه المشاركةمن المفضلة من قبل ألا تذكر؟", "cant-ban-other-admins": "لايمكن حظر مدبر نظام آخر.", - "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", - "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", + "cant-remove-last-admin": "رجاءًا ، أضف مدير أخر قبل حذف صلاحيات الإدارة من حسابك.", + "cant-delete-admin": "رجاءًا أزل صلاحيات الإدارة قبل حذف الحساب. ", + "invalid-image-type": "نوع الصورة غير مدعوم. الأنواع المدعومة هي : %1", + "invalid-image-extension": "امتداد الصورة غير مدعوم.", + "invalid-file-type": "صيغة الملف غير مدعومة. الأنواع المدعومة هي: %1", "group-name-too-short": "اسم المجموعة قصير", + "group-name-too-long": "اسم المجموعة طويل.", "group-already-exists": "المجموعة موجودة مسبقا", "group-name-change-not-allowed": "لايسمح بتغيير أسماء المجموعات", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", + "group-already-member": "أنت عضو في هذه المجموعة.", + "group-not-member": "أنت لست عضو في هذه المجموعة.", "group-needs-owner": "هذه المجموعة تتطلب مالك واحد على اﻷقل", - "group-already-invited": "This user has already been invited", - "group-already-requested": "Your membership request has already been submitted", + "group-already-invited": "المستخدم سبق وأن تمت دعوته", + "group-already-requested": "سبق وتم تسجيل طلب العضوية", "post-already-deleted": "سبق وتم حذف هذا الرد", "post-already-restored": "سبق وتم إلغاء حذف هذا الرد", "topic-already-deleted": "سبق وتم حذف هذا الموضوع", "topic-already-restored": "سبق وتم إلغاء حذف هذا الرد", - "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "cant-purge-main-post": "لا يمكنك محو المشاركة الأساسية، يرجى حذف الموضوع بدلاً عن ذلك", "topic-thumbnails-are-disabled": "الصور المصغرة غير مفعلة.", "invalid-file": "ملف غير مقبول", "uploads-are-disabled": "رفع الملفات غير مفعل", "signature-too-long": "عذرا، توقيعك يجب ألا يتجاوز %1 حرفًا.", - "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "about-me-too-long": "نأسف، ( عني ) لا يمكن أن يكون أكثر من %1 حرف. ", "cant-chat-with-yourself": "لايمكنك فتح محادثة مع نفسك", "chat-restricted": "هذا المستخدم عطل المحادثات الواردة عليه. يجب أن يتبعك حتى تتمكن من فتح محادثة معه.", - "chat-disabled": "Chat system disabled", + "chat-disabled": "نظام المحادثة معطل.", "too-many-messages": "لقد أرسلت الكثير من الرسائل، الرجاء اﻹنتظار قليلاً", - "invalid-chat-message": "Invalid chat message", - "chat-message-too-long": "Chat message is too long", - "cant-edit-chat-message": "You are not allowed to edit this message", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "invalid-chat-message": "الرسالة غير صالحة.", + "chat-message-too-long": "الرسالة طويلة.", + "cant-edit-chat-message": "غير مصرح لك بتعديل الرسالة.", + "cant-remove-last-user": "لأيمكنك إزالت اخر مستخدم.", + "cant-delete-chat-message": "غير مصرح لك بحذف الرسالة.", + "already-voting-for-this-post": "لقد شاركت بالتصويت ، ألا تذكر؟", "reputation-system-disabled": "نظام السمعة معطل", "downvoting-disabled": "التصويتات السلبية معطلة", "not-enough-reputation-to-downvote": "ليس لديك سمعة تكفي لإضافة صوت سلبي لهذا الموضوع", "not-enough-reputation-to-flag": "ليس لديك سمعة تكفي للإشعار بموضوع مخل", - "already-flagged": "You have already flagged this post", + "already-flagged": "لقد بلغت عن هذه المشاركة من قبل.", "reload-failed": "المنتدى واجه مشكلة أثناء إعادة التحميل: \"%1\". سيواصل المنتدى خدمة العملاء السابقين لكن يجب عليك إلغاء أي تغيير قمت به قبل إعادة التحميل.", "registration-error": "حدث خطأ أثناء التسجيل", "parse-error": "حدث خطأ ما أثناء تحليل استجابة الخادم", "wrong-login-type-email": "الرجاء استعمال بريدك اﻹلكتروني للدخول", "wrong-login-type-username": "الرجاء استعمال اسم المستخدم الخاص بك للدخول", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "invite-maximum-met": "لقد قمت بدعوة الحد الأقصى من الأشخاص (%1 من %2)", + "no-session-found": "لم دخول مسجل!", + "not-in-room": "المستخدم غير موجود في الغرفة.", + "no-users-in-room": "لا يوجد مستخدمين في الغرفة.", + "cant-kick-self": "لا يمكنك طرد نفسك من المجموعة.", + "no-users-selected": "لا يوجد مستخدم محدد.", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "يبدو أن فترة التسجيل لم تعد قائمة او هي غير مطابقة مع الخادم. يرجى إعادة تحميل هذه الصفحة." } \ No newline at end of file diff --git a/public/language/ar/global.json b/public/language/ar/global.json index e0a2d51643..e95f80d67f 100644 --- a/public/language/ar/global.json +++ b/public/language/ar/global.json @@ -4,11 +4,13 @@ "buttons.close": "أغلق", "403.title": "غير مسموح بالدخول", "403.message": "يبدو أنك قد تعثرت على صفحة لا تمتلك الصلاحية للدخول إليها", - "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", + "403.login": "حاول أن <a href='%1/login'>تسجل دخولك</a>.", "404.title": "لم يتم العثور", - "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "خطأ داخلي.", + "404.message": "الصفحة غير موجودة. العودة لـ <a href='%1/'>الرئيسية</a>", + "500.title": "خطأ داخلي", "500.message": "عفوا! يبدو وكأنه شيء ذهب على نحو خاطئ!", + "400.title": "طلب سيئ", + "400.message": "الرابط غير صحيح. رجاءًا تأكد من الرابط أو ارجع لـ <a href='%1/'>الرئيسية</a>", "register": "تسجيل", "login": "دخول", "please_log_in": "المرجو تسجيل الدخول", @@ -17,6 +19,7 @@ "welcome_back": "مرحبًا بعودتك", "you_have_successfully_logged_in": "تم سجيل الدخول بنجاح", "save_changes": "حفظ التغييرات", + "save": "Save", "close": "أغلق", "pagination": "الصفحات", "pagination.out_of": "%1 من %2", @@ -33,7 +36,7 @@ "header.notifications": "التنبيهات", "header.search": "بحث", "header.profile": "ملف", - "header.navigation": "Navigation", + "header.navigation": "الاستكشاف", "notifications.loading": "تحميل التبليغات", "chats.loading": "تحميل الدردشات", "motd.welcome": "مرحبا بكم NodeBB، منصة مناقشة المستقبل", @@ -49,29 +52,31 @@ "users": "الأعضاء", "topics": "المواضيع", "posts": "المشاركات", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", + "best": "الأفضل", + "upvoters": "الموافقين", + "upvoted": "مصوت بالموجب", + "downvoters": "مصوتين بالسالب", + "downvoted": "مصوت بالسالب", "views": "المشاهدات", "reputation": "السمعة", "read_more": "اقرأ المزيد", "more": "المزيد", - "posted_ago_by_guest": "كتب %1 من طرف زائر", - "posted_ago_by": "كتب %1 من طرف %2", + "posted_ago_by_guest": "كتب %1 بواسطة زائر", + "posted_ago_by": "كتب %1 بواسطة %2", "posted_ago": "كتب %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", + "posted_in": "كتب في %1", + "posted_in_by": "كتب في 1% بواسطة %2", "posted_in_ago": "كتب في %1 %2", "posted_in_ago_by": "كتب في %1 %2 من طرف %3", "user_posted_ago": "%1 كتب %2", "guest_posted_ago": "كتب زائر %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "اخر تحرير بواسطة 1%", "norecentposts": "لاوجود لمشاركات جديدة", "norecenttopics": "لاوجود لمواضيع جديدة", "recentposts": "آخر المشاركات", "recentips": "آخر عناوين ال IP التي سجلت الدخول", "away": "غير متواجد", - "dnd": "Do not disturb", + "dnd": "عدم الإزعاج", "invisible": "مخفي", "offline": "غير متصل", "email": "عنوان البريد الإلكتروني", @@ -84,11 +89,14 @@ "follow": "متابعة", "unfollow": "إلغاء المتابعة", "delete_all": "حذف الكل", - "map": "Map", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "map": "خريطة", + "sessions": "الجلسة", + "ip_address": "عنوان الآي بي", + "enter_page_number": "ادخل رقم الصفحة", + "upload_file": "ارفع ملف", + "upload": "ارفع", + "allowed-file-types": "صيغ الملفات المدعومة هي 1%", + "unsaved-changes": "لديك تغييرات لم تحفظ. هل أنت متأكد من تغيير الصفحة؟", + "reconnecting-message": "يبدو أن اتصالك لـ %1 قد فقد. رجاءًا أنتظر ثم حاول الإتصال مرة اخرى.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ar/groups.json b/public/language/ar/groups.json index 673fba23e2..d9bb5dc4d9 100644 --- a/public/language/ar/groups.json +++ b/public/language/ar/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "غادر المجموعة", "membership.reject": "رفض", "new-group.group_name": "اسم المجموعة", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/ar/login.json b/public/language/ar/login.json index 29d256203f..60bf2773af 100644 --- a/public/language/ar/login.json +++ b/public/language/ar/login.json @@ -5,7 +5,8 @@ "remember_me": "تذكرني؟", "forgot_password": "نسيت كلمة المرور؟", "alternative_logins": "تسجيلات الدخول البديلة", - "failed_login_attempt": "فشلت محاولة تسجيل الدخول، يرجى المحاولة مرة أخرى.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "قمت بتسجيل الدخول بنجاح!", - "dont_have_account": "لا تملك حساب؟" + "dont_have_account": "لا تملك حساب؟", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index 1c23da87ad..a9a07325e6 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -17,7 +17,7 @@ "chat.seven_days": "7 أيام", "chat.thirty_days": "30 يومًا", "chat.three_months": "3 أشهر", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.delete_message_confirm": "هل أنت متأكد من أنك تريد حذف هذه الرسالة؟", "chat.roomname": "Chat Room %1", "chat.add-users-to-room": "Add users to room", "composer.compose": "اكتب", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "إلغاء", "bootbox.confirm": "تأكيد", diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json index 81733db21e..9df1e7e355 100644 --- a/public/language/ar/notifications.json +++ b/public/language/ar/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> أشعَرَ بمشاركة مخلة في <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/ar/pages.json b/public/language/ar/pages.json index 74170e7af3..84bc238010 100644 --- a/public/language/ar/pages.json +++ b/public/language/ar/pages.json @@ -1,45 +1,48 @@ { "home": "الصفحة الرئيسية", "unread": "المواضيع الغير مقروءة", - "popular-day": "Popular topics today", - "popular-week": "Popular topics this week", - "popular-month": "Popular topics this month", - "popular-alltime": "All time popular topics", + "popular-day": "المواضيع الشائعة اليوم", + "popular-week": "المواضيع الشائعة هذا الأسبوع", + "popular-month": "المواضيع الشائعة هذا الشهر", + "popular-alltime": "المواضيع الشائعة منذ القدم", "recent": "المواضيع الحديثة", "flagged-posts": "Flagged Posts", "users/online": "اﻷعضاء المتصلون", "users/latest": "أحدث اﻷعضاء", - "users/sort-posts": "Users with the most posts", - "users/sort-reputation": "Users with the most reputation", - "users/banned": "Banned Users", - "users/search": "User Search", + "users/sort-posts": "الأعضاء الأكثر نشاطاً", + "users/sort-reputation": "الأعضاء الأفضل سمعة", + "users/banned": "الأعضاء المحظورون", + "users/most-flags": "Most flagged users", + "users/search": "بحث عن مستخدم", "notifications": "التنبيهات", "tags": "الكلمات الدلالية", - "tag": "Topics tagged under \"%1\"", - "register": "Register an account", - "login": "Login to your account", - "reset": "Reset your account password", + "tag": "المواضيع المرتبطة بالوسم %1", + "register": "تسجيل حساب", + "registration-complete": "Registration complete", + "login": "سجل الدخول الى حسابك", + "reset": "إعادة تعيين كلمة مرور حسابك", "categories": "الفئات", "groups": "المجموعات", - "group": "%1 group", - "chats": "Chats", + "group": "%1 مجموعة", + "chats": "محادثات", "chat": "Chatting with %1", "account/edit": "Editing \"%1\"", "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "معلومات الحساب", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", - "account/settings": "User Settings", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "خيارات المستخدم", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", "account/downvoted": "Posts downvoted by %1", "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "confirm": "تم التحقق من عنوان البريد الإلكتروني", "maintenance.text": "جاري صيانة %1. المرجو العودة لاحقًا.", "maintenance.messageIntro": "بالإضافة إلى ذلك، قام مدبر النظام بترك هذه الرسالة:", "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." diff --git a/public/language/ar/recent.json b/public/language/ar/recent.json index 4fc13736d0..fcbc6949be 100644 --- a/public/language/ar/recent.json +++ b/public/language/ar/recent.json @@ -6,7 +6,7 @@ "year": "سنة", "alltime": "دائمًا", "no_recent_topics": "لايوجد مواضيع جديدة", - "no_popular_topics": "There are no popular topics.", + "no_popular_topics": "لا يوجد مواضيع شائعة", "there-is-a-new-topic": "يوجد موضوع جديد", "there-is-a-new-topic-and-a-new-post": "يوجد موضوع جديد و رد جديد", "there-is-a-new-topic-and-new-posts": "يوجد موضوع جديد و %1 ردود جديدة ", diff --git a/public/language/ar/register.json b/public/language/ar/register.json index 9960bcace0..c6ccf2231e 100644 --- a/public/language/ar/register.json +++ b/public/language/ar/register.json @@ -1,5 +1,6 @@ { "register": "تسجيل", + "cancel_registration": "إلغاء التسجيل", "help.email": "افتراضيا، سيتم إخفاء بريدك الإلكتروني من العامة.", "help.username_restrictions": "اسم مستخدم فريدة من نوعها بين1% و2% حرفا. يمكن للآخرين ذكرك @ <'span id='your-username> اسم المستخدم </span>.", "help.minimum_password_length": "كلمة المرور يجب أن تكون على الأقل بها 1% أحرف", @@ -15,5 +16,8 @@ "alternative_registration": "طريقة تسجيل بديلة", "terms_of_use": "شروط الاستخدام", "agree_to_terms_of_use": "أوافق على شروط الاستخدام", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index 1e66ad91eb..46f816b0e4 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -13,7 +13,7 @@ "notify_me": "تلق تنبيهات بالردود الجديدة في هذا الموضوع", "quote": "اقتبس", "reply": "رد", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "رد بموضوع", "guest-login-reply": "يجب عليك تسجيل الدخول للرد", "edit": "تعديل", "delete": "حذف", @@ -26,23 +26,47 @@ "tools": "أدوات", "flag": "تبليغ", "locked": "مقفل", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "مثبت", + "moved": "منقول", + "bookmark_instructions": "اضغط هنا للعودة لأخر مشاركة مقروءة في الموضوع", "flag_title": "إشعار بمشاركة مخلة.", "flag_success": "تم الإشعار بهذه المشاركة على أنها مخلة", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "هذه المشاركة محذوفة. فقط من لهم صلاحية الإشراف على ا لمشاركات يمكنهم معاينتها.", "following_topic.message": "ستستلم تنبيها عند كل مشاركة جديدة في هذا الموضوع.", - "not_following_topic.message": "لن تستلم أي تنبيه بخصوص عذا الموضوع بعد الآن.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "المرجو إنشاء حساب أو تسجيل الدخول حتى يمكنك متابعة هذا الموضوع.", "markAsUnreadForAll.success": "تم تحديد الموضوع على أنه غير مقروء.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "حدده كغير مقروء", + "mark_unread.success": "الموضوع حدد على أنه غير مقروء", "watch": "مراقبة", "unwatch": "الغاء المراقبة", "watch.title": "استلم تنبيها بالردود الجديدة في هذا الموضوع", "unwatch.title": "ألغ مراقبة هذا الموضوع", "share_this_post": "انشر هذا الموضوع", + "watching": "مراقبة", + "not-watching": "غير مراقب", + "ignoring": "تجاهل", + "watching.description": "بلغني بالردود الجديدة<br/>\nاظهر الموضوع في غير مقروء", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "أدوات الموضوع", - "thread_tools.markAsUnreadForAll": "علم غير مقروء", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "علق الموضوع", "thread_tools.unpin": "إلغاء تعليق الموضوع", "thread_tools.lock": "أقفل الموضوع", @@ -51,7 +75,7 @@ "thread_tools.move_all": "نقل الكل", "thread_tools.fork": "إنشاء فرع الموضوع", "thread_tools.delete": "حذف الموضوع", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "مشاركات محذوفة", "thread_tools.delete_confirm": "هل أنت متأكد أنك تريد حذف هذا الموضوع؟", "thread_tools.restore": "استعادة الموضوع", "thread_tools.restore_confirm": "هل أنت متأكد أنك تريد استعادة هذا الموضوع؟", @@ -65,9 +89,9 @@ "disabled_categories_note": "الفئات المعطلة رمادية", "confirm_move": "انقل", "confirm_fork": "فرع", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "المفضلة", + "favourites": "المفضلات", + "favourites.has_no_favourites": "لم تقم بتفضيل اي مشاركة حتى الأن.", "loading_more_posts": "تحميل المزيد من المشاركات", "move_topic": "نقل الموضوع", "move_topics": "نقل المواضيع", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "هذا الموضوع سوف ينقل إلى فئة", "fork_topic_instruction": "إضغط على المشاركات التي تريد تفريعها", "fork_no_pids": "لم تختر أي مشاركة", + "fork_pid_count": "1% مشاركة محددة", "fork_success": "تم إنشاء فرع للموضوع بنجاح! إضغط هنا لمعاينة الفرع.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "أدخل عنوان موضوعك هنا...", @@ -103,10 +128,10 @@ "most_posts": "اﻷكثر رداً", "stale.title": "Create new topic instead?", "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", - "link_back": "Re: [%1](%2)", - "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "stale.create": "موضوع جديد", + "stale.reply_anyway": "الرد على هذا الموضوع ", + "link_back": "رد: [%1](%2)", + "spam": "سبام", + "offensive": "مسيئ", + "custom-flag-reason": "أدخل سبب التبليغ" } \ No newline at end of file diff --git a/public/language/ar/unread.json b/public/language/ar/unread.json index 992ffa6f5c..2d139f5e75 100644 --- a/public/language/ar/unread.json +++ b/public/language/ar/unread.json @@ -6,5 +6,8 @@ "selected": "المحددة", "all": "الكل", "all_categories": "كل الفئات", - "topics_marked_as_read.success": "تم تحديد المواضيع على أنها مقروءة!" + "topics_marked_as_read.success": "تم تحديد المواضيع على أنها مقروءة!", + "all-topics": "كل المواضيع", + "new-topics": "مواضيع جديدة", + "watched-topics": "المواضيع المتابعة" } \ No newline at end of file diff --git a/public/language/ar/uploads.json b/public/language/ar/uploads.json index 1622cb5693..2002597f72 100644 --- a/public/language/ar/uploads.json +++ b/public/language/ar/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "جاري رفع الملف...", + "select-file-to-upload": "إختر ملف لرفعه!", + "upload-success": "تم رفع الملف بنجاح!", + "maximum-file-size": "الحجم الأقصى %1 كيلوبت" } \ No newline at end of file diff --git a/public/language/ar/user.json b/public/language/ar/user.json index 23519da3a5..2bec30bc76 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -6,6 +6,7 @@ "postcount": "عدد المشاركات", "email": "البريد الإلكتروني", "confirm_email": "تأكيد عنوان البريد الإلكتروني", + "account_info": "معلومات الحساب", "ban_account": "Ban Account", "ban_account_confirm": "هل تريد حقاً حظر هاذا العضو؟", "unban_account": "Unban Account", @@ -36,10 +37,10 @@ "more": "المزيد", "profile_update_success": "تم تحديث الملف الشخصي بنجاح", "change_picture": "تغيير الصورة", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "تغيير اسم المستخدم", + "change_email": "تغيير البريد اﻹلكتروني", "edit": "تعديل", - "edit-profile": "Edit Profile", + "edit-profile": "تعديل الملف الشخصي", "default_picture": "Default Icon", "uploaded_picture": "الصورة المرفوعة", "upload_new_picture": "رفع صورة جديدة", @@ -88,22 +89,41 @@ "topics_per_page": "المواضيع في كل صفحة", "posts_per_page": "الردود في كل صفحة", "notification_sounds": "تشغيل صوت عند تلقي تنبيه", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "خيارات التصفح", "open_links_in_new_tab": "فتح الروابط الخارجية في نافدة جديدة", "enable_topic_searching": "تفعيل خاصية البحث داخل المواضيع", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "متابعة المواضيع التي تقوم بالرد فيها", - "follow_topics_you_create": "متابعة المواضيع التي تنشئها", - "grouptitle": "حدد عنوان المجموعة الذي تريد عرضه", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "عنوان المجموعة", "no-group-title": "لا يوجد عنوان للمجموعة", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", - "homepage": "Homepage", + "homepage": "الصفحة الرئيسية", "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", "custom_route": "Custom Homepage Route", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/ar/users.json b/public/language/ar/users.json index 24e7416521..11a71a9341 100644 --- a/public/language/ar/users.json +++ b/public/language/ar/users.json @@ -2,6 +2,7 @@ "latest_users": "أحدث الأعضاء", "top_posters": "اﻷكثر مشاركة", "most_reputation": "أعلى سمعة", + "most_flags": "Most Flags", "search": "بحث", "enter_username": "أدخل اسم مستخدم للبحث", "load_more": "حمل المزيد", diff --git a/public/language/bg/category.json b/public/language/bg/category.json index ace3e651d1..a81544e227 100644 --- a/public/language/bg/category.json +++ b/public/language/bg/category.json @@ -10,7 +10,11 @@ "share_this_category": "Споделяне на тази категория", "watch": "Следене", "ignore": "Игнориране", - "watch.message": "Вече следите обновленията в тази категория", - "ignore.message": "Вече не следите обновленията в тази категория", - "watched-categories": "Следени категории" + "watching": "Наблюдавате", + "ignoring": "Пренебрегвате", + "watching.description": "Темите ще се показват в непрочетените", + "ignoring.description": "Темите няма да се показват в непрочетените", + "watch.message": "Вече наблюдавате новите неща в тази категория и всички под-категории", + "ignore.message": "Вече не следите новите неща в тази категория и всички под-категории", + "watched-categories": "Наблюдавани категории" } \ No newline at end of file diff --git a/public/language/bg/email.json b/public/language/bg/email.json index 12ebb52350..480dd09fff 100644 --- a/public/language/bg/email.json +++ b/public/language/bg/email.json @@ -24,6 +24,7 @@ "digest.day": "ден", "digest.week": "месец", "digest.month": "година", + "digest.subject": "Резюме за %1", "notif.chat.subject": "Получено е ново съобщение от %1", "notif.chat.cta": "Натиснете тук, за да продължите разговора", "notif.chat.unsub.info": "Това известие за разговор беше изпратено до Вас поради настройките Ви за абонаментите.", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 0eaeb842d2..edfc95351b 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -1,5 +1,5 @@ { - "invalid-data": "Невалидни данни", + "invalid-data": "Грешни данни", "not-logged-in": "Изглежда не сте влезли в системата.", "account-locked": "Вашият акаунт беше заключен временно", "search-requires-login": "Търсенето изисква акаунт – моля, влезте или се регистрирайте.", @@ -14,20 +14,25 @@ "invalid-password": "Грешна парола", "invalid-username-or-password": "Моля, посочете потребителско име и парола", "invalid-search-term": "Грешен текст за търсене", + "csrf-invalid": "Не успяхме да Ви впишем, най-вероятно защото сесията Ви е изтекла. Моля, опитайте отново", "invalid-pagination-value": "Грешен номер на страница, трябва да бъде между %1 и %2", "username-taken": "Потребителското име е заето", "email-taken": "Е-пощата е заета", "email-not-confirmed": "Вашата е-поща все още не е потвърдена. Моля, натиснете тук, за да потвърдите е-пощата си.", "email-not-confirmed-chat": "Няма да можете да пишете в разговори, докато е-пощата Ви не бъде потвърдена. Моля, натиснете тук, за да потвърдите е-пощата си.", + "email-not-confirmed-email-sent": "Вашата е-поща все още не е потвърдена. Моля, проверете входящата си кутия за писмото за потвърждение.", "no-email-to-confirm": "Този форум изисква потвърдена е-поща. Моля, натиснете тук, за да въведете е-поща", "email-confirm-failed": "Не успяхме да потвърдим е-пощата Ви. Моля, опитайте отново по-късно.", "confirm-email-already-sent": "Е-писмото за потвърждение вече е изпратено. Моля, почакайте още %1 минута/и, преди да изпратите ново.", + "sendmail-not-found": "Изпълнимият файл на „sendmail“ не може да бъде намерен. Моля, уверете се, че е инсталиран и изпълним за потребителя, чрез който е пуснат NodeBB.", "username-too-short": "Потребителското име е твърде кратко", "username-too-long": "Потребителското име е твърде дълго", "password-too-long": "Паролата е твърде дълга", "user-banned": "Потребителят е блокиран", + "user-banned-reason": "Потребителят е блокиран (Причина: %1)", "user-too-new": "Съжаляваме, но трябва да изчакате поне %1 секунда/и, преди да направите първата си публикация", "blacklisted-ip": "Съжаляваме, но Вашият IP адрес е забранен за ползване в тази общност. Ако смятате, че това е грешка, моля, свържете се с администратор.", + "ban-expiry-missing": "Моля, задайте крайна дата за това блокиране", "no-category": "Категорията не съществува", "no-topic": "Темата не съществува", "no-post": "Публикацията не съществува", @@ -38,10 +43,26 @@ "category-disabled": "Категорията е изключена", "topic-locked": "Темата е заключена", "post-edit-duration-expired": "Можете да редактирате публикациите си до %1 секунда/и, след като ги пуснете", + "post-edit-duration-expired-minutes": "Можете да редактирате публикациите си до %1 минута/и, след като ги пуснете", + "post-edit-duration-expired-minutes-seconds": "Можете да редактирате публикациите си до %1 минута/и и %2 секунда/и, след като ги пуснете", + "post-edit-duration-expired-hours": "Можете да редактирате публикациите си до %1 час(а), след като ги пуснете", + "post-edit-duration-expired-hours-minutes": "Можете да редактирате публикациите си до %1 час(а) и %2 минута/и, след като ги пуснете", + "post-edit-duration-expired-days": "Можете да редактирате публикациите си до %1 ден(а), след като ги пуснете", + "post-edit-duration-expired-days-hours": "Можете да редактирате публикациите си до %1 ден(а) и %2 час(а), след като ги пуснете", + "post-delete-duration-expired": "Можете да изтривате публикациите си до %1 секунда/и, след като ги пуснете", + "post-delete-duration-expired-minutes": "Можете да изтривате публикациите си до %1 минута/и, след като ги пуснете", + "post-delete-duration-expired-minutes-seconds": "Можете да изтривате публикациите си до %1 минута/и и %2 секунда/и, след като ги пуснете", + "post-delete-duration-expired-hours": "Можете да изтривате публикациите си до %1 час(а), след като ги пуснете", + "post-delete-duration-expired-hours-minutes": "Можете да изтривате публикациите си до %1 час(а) и %2 минута/и, след като ги пуснете", + "post-delete-duration-expired-days": "Можете да изтривате публикациите си до %1 ден(а), след като ги пуснете", + "post-delete-duration-expired-days-hours": "Можете да изтривате публикациите си до %1 ден(а) и %2 час(а), след като ги пуснете", + "cant-delete-topic-has-reply": "Не можете да изтриете темата си, след като в нея вече има един отговор", + "cant-delete-topic-has-replies": "Не можете да изтриете темата си, след като в нея вече има %1 отговора", "content-too-short": "Моля, въведете по-дълъг текст на публикацията. Публикациите трябва да съдържат поне %1 символ(а).", "content-too-long": "Моля, въведете по-кратък текст на публикацията. Публикациите трябва да съдържат не повече от %1 символ(а).", "title-too-short": "Моля, въведете по-дълго заглавие. Заглавията трябва да съдържат поне %1 символ(а).", "title-too-long": "Моля, въведете по-кратко заглавие. Заглавията трябва да съдържат не повече от %1 символ(а).", + "category-not-selected": "Не е избрана категория.", "too-many-posts": "Можете да публикувате веднъж на %1 секунда/и – моля, изчакайте малко, преди да опитате да публикувате отново", "too-many-posts-newbie": "Като нов потребител, Вие можете да публикувате веднъж на %1 секунда/и, докато не натрупате %2 репутация – моля, изчакайте малко, преди да опитате да публикувате отново", "tag-too-short": "Моля, въведете по-дълъг етикет. Етикетите трябва да съдържат поне %1 символ(а)", @@ -55,10 +76,12 @@ "already-unfavourited": "Вече сте премахнали отметката си към тази публикация", "cant-ban-other-admins": "Не можете да блокирате другите администратори!", "cant-remove-last-admin": "Вие сте единственият администратор. Добавете друг потребител като администратор, преди да премахнете себе си като администратор", + "cant-delete-admin": "Премахнете администраторските права от този акаунт, преди да го изтриете.", "invalid-image-type": "Грешен тип на изображение. Позволените типове са: %1", "invalid-image-extension": "Грешно разширение на изображението", "invalid-file-type": "Грешен тип на файл. Позволените типове са: %1", "group-name-too-short": "Името на групата е твърде кратко", + "group-name-too-long": "Името на групата е твърде дълго", "group-already-exists": "Вече съществува такава група", "group-name-change-not-allowed": "Промяната на името на групата не е разрешено", "group-already-member": "Потребителят вече членува в тази група", @@ -80,11 +103,12 @@ "chat-restricted": "Този потребител е ограничил съобщенията до себе си. Той трябва първо да Ви последва, преди да можете да си пишете с него.", "chat-disabled": "Системата за разговори е изключена", "too-many-messages": "Изпратили сте твърде много съобщения. Моля, изчакайте малко.", - "invalid-chat-message": "Невалидно съобщение", + "invalid-chat-message": "Неправилно съобщение", "chat-message-too-long": "Съобщението е твърде дълго", "cant-edit-chat-message": "Нямате право да редактирате това съобщение", "cant-remove-last-user": "Не можете да премахнете последния потребител", "cant-delete-chat-message": "Нямате право да изтриете това съобщение", + "already-voting-for-this-post": "Вече сте дали глас за тази публикация.", "reputation-system-disabled": "Системата за репутация е изключена.", "downvoting-disabled": "Отрицателното гласуване е изключено", "not-enough-reputation-to-downvote": "Нямате достатъчно репутация, за да гласувате отрицателно за тази публикация", @@ -99,5 +123,9 @@ "no-session-found": "Не е открита сесия за вход!", "not-in-room": "Потребителят не е в стаята", "no-users-in-room": "Няма потребители в тази стая", - "cant-kick-self": "Не можете да изритате себе си от групата" + "cant-kick-self": "Не можете да изритате себе си от групата", + "no-users-selected": "Няма избран(и) потребител(и)", + "invalid-home-page-route": "Грешен път към началната страница", + "invalid-session": "Несъответствие в сесията", + "invalid-session-text": "Изглежда сесията Ви за вход вече е изтекла, или не съответства на сървъра. Моля, презаредете страницата." } \ No newline at end of file diff --git a/public/language/bg/global.json b/public/language/bg/global.json index 3236a43698..644165e47d 100644 --- a/public/language/bg/global.json +++ b/public/language/bg/global.json @@ -9,6 +9,8 @@ "404.message": "Изглежда сте се опитали да посетите страница, която не съществува. Върнете се към <a href='%1/'>началната страница</a>.", "500.title": "Вътрешна грешка.", "500.message": "Опа! Изглежда нещо се обърка!", + "400.title": "Грешна заявка.", + "400.message": "Тази връзка изглежда повредена. Моля, проверете я и опитайте отново. В противен случай се върнете на <a href='%1/'>началната страница</a>.", "register": "Регистрация", "login": "Вход", "please_log_in": "Моля, влезте", @@ -17,6 +19,7 @@ "welcome_back": "Добре дошли отново", "you_have_successfully_logged_in": "Вие влязохте успешно", "save_changes": "Запазване на промените", + "save": "Запазване", "close": "Затваряне", "pagination": "Страници", "pagination.out_of": "%1 от %2", @@ -50,7 +53,9 @@ "topics": "Теми", "posts": "Публ.", "best": "Най-добри", + "upvoters": "Гласували положително", "upvoted": "С положителни гласове", + "downvoters": "Гласували отрицателно", "downvoted": "С отрицателни гласове", "views": "Прегл.", "reputation": "Репутация", @@ -90,5 +95,8 @@ "enter_page_number": "Въведете номер на страница", "upload_file": "Качване на файл", "upload": "Качване", - "allowed-file-types": "Разрешените файлови типове са: %1" + "allowed-file-types": "Разрешените файлови типове са: %1", + "unsaved-changes": "Имате незапазени промени. Наистина ли искате да напуснете тази страница?", + "reconnecting-message": "Изглежда връзката Ви към %1 беше прекъсната. Моля, изчакайте докато се опитаме да Ви свържем отново.", + "play": "Пускане" } \ No newline at end of file diff --git a/public/language/bg/groups.json b/public/language/bg/groups.json index f5ab2262ce..14f4d84bf3 100644 --- a/public/language/bg/groups.json +++ b/public/language/bg/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Напускане на групата", "membership.reject": "Отхвърляне", "new-group.group_name": "Име на групата:", - "upload-group-cover": "Качване на снимка за показване на групата" + "upload-group-cover": "Качване на снимка за показване на групата", + "bulk-invite-instructions": "Въведете списък от потребителски имена, разделени със запетаи", + "bulk-invite": "Масова покана" } \ No newline at end of file diff --git a/public/language/bg/login.json b/public/language/bg/login.json index f8340495af..12a06f5fcb 100644 --- a/public/language/bg/login.json +++ b/public/language/bg/login.json @@ -5,7 +5,8 @@ "remember_me": "Запомнете ме?", "forgot_password": "Забравена парола?", "alternative_logins": "Други начини за влизане", - "failed_login_attempt": "Неуспешно влизане. Моля, опитайте отново.", + "failed_login_attempt": "Влизането беше неуспешно", "login_successful": "Вие влязохте успешно!", - "dont_have_account": "Нямате акаунт?" + "dont_have_account": "Нямате акаунт?", + "logged-out-due-to-inactivity": "Вие излязохте автоматично от администраторския контролен панел, поради бездействие." } \ No newline at end of file diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index f3afdaaa4a..3838200b0a 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -17,7 +17,7 @@ "chat.seven_days": "7 дни", "chat.thirty_days": "30 дни", "chat.three_months": "3 месеца", - "chat.delete_message_confirm": "Сигурен/а ли сте, че искате да изтриете това съобщение?", + "chat.delete_message_confirm": "Наистина ли искате да изтриете това съобщение?", "chat.roomname": "Стая за разговори %1", "chat.add-users-to-room": "Добавяне на потребители към стаята", "composer.compose": "Писане", @@ -25,10 +25,20 @@ "composer.hide_preview": "Скриване на прегледа", "composer.user_said_in": "%1 каза в %2:", "composer.user_said": "%1 каза:", - "composer.discard": "Сигурен/а ли сте, че искате да отхвърлите тази публикация?", + "composer.discard": "Наистина ли искате да отхвърлите тази публикация?", "composer.submit_and_lock": "Публикуване и заключване", "composer.toggle_dropdown": "Превключване на падащото меню", "composer.uploading": "Качване на %1", + "composer.formatting.bold": "Получер", + "composer.formatting.italic": "Курсив", + "composer.formatting.list": "Списък", + "composer.formatting.strikethrough": "Зачертан", + "composer.formatting.link": "Връзка", + "composer.formatting.picture": "Снимка", + "composer.upload-picture": "Качване на изображение", + "composer.upload-file": "Качване на файл", + "composer.zen_mode": "Режим Дзен", + "composer.select_category": "Изберете категория", "bootbox.ok": "Добре", "bootbox.cancel": "Отказ", "bootbox.confirm": "Потвърждаване", diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json index d621942a50..22d1b170ff 100644 --- a/public/language/bg/notifications.json +++ b/public/language/bg/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> и %2 други гласуваха положително за Ваша публикация в <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> премести публикацията Ви в <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> премести <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> си запази отметка към Ваша публикация в <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> и <strong>%2</strong> си запазиха отметки към Ваша публикация в <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> и %2 други си запазиха отметки към Ваша публикация в <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> докладва Ваша публикация в <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> и <strong>%2</strong> докладваха Ваша публикация в <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> и %2 други докладваха Ваша публикация в <strong>%3</strong>", diff --git a/public/language/bg/pages.json b/public/language/bg/pages.json index 45e2861bdd..20002e1e12 100644 --- a/public/language/bg/pages.json +++ b/public/language/bg/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Потребители с най-много публикации", "users/sort-reputation": "Потребители с най-висока репутация", "users/banned": "Блокирани потребители", + "users/most-flags": "Най-докладвани потребители", "users/search": "Търсене на потребители", "notifications": "Известия", "tags": "Етикети", "tag": "Теми, отбелязани като „%1“", "register": "Регистрирайте акаунт", + "registration-complete": "Регистрацията е завършена", "login": "Влезте в акаунта си", "reset": "Подновете паролата за акаунта си", "categories": "Категории", @@ -28,14 +30,15 @@ "account/edit/password": "Редактиране на паролата на „%1“", "account/edit/username": "Редактиране на потребителското име на „%1“", "account/edit/email": "Редактиране на е-пощата на „%1“", + "account/info": "Информация за акаунта", "account/following": "Хора, които %1 следва", "account/followers": "Хора, които следват %1", "account/posts": "Публикации от %1", "account/topics": "Теми, създадени от %1", "account/groups": "Групите на %1", - "account/favourites": "Отметнатите публикации на %1", + "account/bookmarks": "Отметнатите публикации на %1", "account/settings": "Потребителски настройки", - "account/watched": "Теми, следени от %1", + "account/watched": "Теми, наблюдавани от %1", "account/upvoted": "Публикации, получили положителен глас от %1", "account/downvoted": "Публикации, получили отрицателен глас от %1", "account/best": "Най-добрите публикации от %1", diff --git a/public/language/bg/register.json b/public/language/bg/register.json index c62a14b3e9..0d4aa708c6 100644 --- a/public/language/bg/register.json +++ b/public/language/bg/register.json @@ -1,5 +1,6 @@ { "register": "Регистрация", + "cancel_registration": "Отказване на регистрацията", "help.email": "По подразбиране, Вашата е-поща ще бъде скрита за останалите.", "help.username_restrictions": "Уникално потребителско име с дължина между %1 и %2 символа. Другите ще могат да Ви споменават чрез @<span id='yourUsername'>потребител</span>.", "help.minimum_password_length": "Дължината на паролата Ви трябва да е поне %1 символа.", @@ -15,5 +16,8 @@ "alternative_registration": "Друг начин за регистриране", "terms_of_use": "Условия за ползване", "agree_to_terms_of_use": "Съгласен съм с условията за ползване", - "registration-added-to-queue": "Вашата регистрация беше добавена в опашката за одобрение. Ще получите е-писмо, когато тя бъде одобрена от администратор." + "terms_of_use_error": "Трябва да се съгласите с условията за ползване", + "registration-added-to-queue": "Вашата регистрация беше добавена в опашката за одобрение. Ще получите е-писмо, когато тя бъде одобрена от администратор.", + "interstitial.intro": "Нуждаем се от малко допълнителна информация, преди да можем да създадем акаунта Ви.", + "interstitial.errors-found": "Не можем да завършим Вашата регистрация:" } \ No newline at end of file diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index dbe44a1541..36500647ef 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -26,12 +26,30 @@ "tools": "Инструменти", "flag": "Докладване", "locked": "Заключена", - "bookmark_instructions": "Натиснете тук, за да се върнете на последната непрочетена публикация в тази тема.", + "pinned": "Закачена", + "moved": "Преместена", + "bookmark_instructions": "Щракнете тук, за да се върнете към последно прочетената публикация в тази тема.", "flag_title": "Докладване на тази публикация до модератор", "flag_success": "Тази публикация е била докладвана до модератор.", + "flag_manage_title": "Докладвана публикация в %1", + "flag_manage_history": "История на дейността", + "flag_manage_no_history": "Няма история на събитията", + "flag_manage_assignee": "Назначен", + "flag_manage_state": "Състояние", + "flag_manage_state_open": "Нов/отворен", + "flag_manage_state_wip": "В процес на работа", + "flag_manage_state_resolved": "Разрешен", + "flag_manage_state_rejected": "Отхвърлен", + "flag_manage_notes": "Споделени бележки", + "flag_manage_update": "Обновяване на състоянието на доклада", + "flag_manage_history_assignee": "Разпределен на %1", + "flag_manage_history_state": "Състоянието е променено на „%1“", + "flag_manage_history_notes": "Бележките към доклада са обновени", + "flag_manage_saved": "Подробностите за доклада са обновени", "deleted_message": "Тази тема е била изтрита. Само потребители с права за управление на темите могат да я видят.", "following_topic.message": "Вече ще получавате известия когато някой публикува коментар в тази тема.", - "not_following_topic.message": "Вече няма да получавате известия за тази тема.", + "not_following_topic.message": "Ще виждате тази тема в списъка с непрочетени теми, но няма да получавате известия, когато хората публикуват нещо в нея.", + "ignoring_topic.message": "Вече няма да виждате тази тема в списъка с непрочетени теми. Ще получите известие, когато някой Ви спомене или гласува положително за Ваша публикация.", "login_to_subscribe": "Моля, регистрирайте се или влезте, за да се абонирате за тази тема.", "markAsUnreadForAll.success": "Темата е отбелязана като непрочетена за всички.", "mark_unread": "Отбелязване като непрочетена", @@ -41,8 +59,14 @@ "watch.title": "Получавайте известия за новите отговори в тази тема", "unwatch.title": "Спрете да наблюдавате тази тема", "share_this_post": "Споделете тази публикация", + "watching": "Наблюдавате", + "not-watching": "Не наблюдавате", + "ignoring": "Пренебрегвате", + "watching.description": "Ще получавате известия за новите отговори.<br/>Темата ще се показва в списъка с непрочетени.", + "not-watching.description": "Няма да получавате известия за новите отговори.<br/>Темата ще се показва в списъка с непрочетени, само ако категорията не се пренебрегва.", + "ignoring.description": "Няма да получавате известия за новите отговори.<br/>Темата няма да се показва в списъка с непрочетени.", "thread_tools.title": "Инструменти за темата", - "thread_tools.markAsUnreadForAll": "Отбелязване като непрочетена", + "thread_tools.markAsUnreadForAll": "Отбелязване като непрочетена за всички", "thread_tools.pin": "Закачане на темата", "thread_tools.unpin": "Откачане на темата", "thread_tools.lock": "Заключване на темата", @@ -52,15 +76,15 @@ "thread_tools.fork": "Разделяне на темата", "thread_tools.delete": "Изтриване на темата", "thread_tools.delete-posts": "Изтриване на публикациите", - "thread_tools.delete_confirm": "Сигурен/а ли сте, че искате да изтриете тази тема?", + "thread_tools.delete_confirm": "Наистина ли искате да изтриете тази тема?", "thread_tools.restore": "Възстановяване на темата", - "thread_tools.restore_confirm": "Сигурен/а ли сте, че искате да възстановите тази тема?", + "thread_tools.restore_confirm": "Наистина ли искате да възстановите тази тема?", "thread_tools.purge": "Изчистване на темата", - "thread_tools.purge_confirm": "Сигурен/а ли сте, че искате да изчистите тази тема?", + "thread_tools.purge_confirm": "Наистина ли искате да изчистите тази тема?", "topic_move_success": "Темата беше преместена успешно в %1", - "post_delete_confirm": "Сигурен/а ли сте, че искате да изтриете тази публикация?", - "post_restore_confirm": "Сигурен/а ли сте, че искате да възстановите тази публикация?", - "post_purge_confirm": "Сигурен/а ли сте, че искате да изчистите тази публикация?", + "post_delete_confirm": "Наистина ли искате да изтриете тази публикация?", + "post_restore_confirm": "Наистина ли искате да възстановите тази публикация?", + "post_purge_confirm": "Наистина ли искате да изчистите тази публикация?", "load_categories": "Зареждане на категориите", "disabled_categories_note": "Изключените категории са засивени", "confirm_move": "Преместване", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Тази тема ще бъде преместена в категорията", "fork_topic_instruction": "Натиснете публикациите, които искате да отделите", "fork_no_pids": "Няма избрани публикации!", + "fork_pid_count": "Избрани публикации: %1", "fork_success": "Темата е разделена успешно! Натиснете тук, за да преминете към отделената тема.", "delete_posts_instruction": "Натиснете публикациите, които искате да изтриете/изчистите", "composer.title_placeholder": "Въведете заглавието на темата си тук...", diff --git a/public/language/bg/unread.json b/public/language/bg/unread.json index b80ddf87ef..7dc16f9cd3 100644 --- a/public/language/bg/unread.json +++ b/public/language/bg/unread.json @@ -6,5 +6,8 @@ "selected": "Избраните", "all": "Всички", "all_categories": "Всички категории", - "topics_marked_as_read.success": "Темите бяха отбелязани като прочетени!" + "topics_marked_as_read.success": "Темите бяха отбелязани като прочетени!", + "all-topics": "Всички теми", + "new-topics": "Нови теми", + "watched-topics": "Наблюдавани теми" } \ No newline at end of file diff --git a/public/language/bg/user.json b/public/language/bg/user.json index 6e16e70603..44b8689945 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -6,12 +6,13 @@ "postcount": "Брой публикации", "email": "Е-поща", "confirm_email": "Потвърдете е-пощата", + "account_info": "Информация за акаунта", "ban_account": "Блокиране на акаунта", "ban_account_confirm": "Наистина ли искате да блокирате този потребител?", "unban_account": "Отблокиране на акаунта", "delete_account": "Изтриване на акаунта", - "delete_account_confirm": "Сигурен/а ли сте, че искате да изтриете акаунта си? <br /><strong>Това действие е необратимо и няма да можете да възстановите нищо от данните си</strong><br /><br />Въведете потребителското си име, за да потвърдите, че искате да унищожите този акаунт.", - "delete_this_account_confirm": "Сигурен/а ли сте, че искате да изтриете този акаунт? <br /><strong>Това действие е необратимо и няма да можете да възстановите нищо от данните</strong><br /><br />", + "delete_account_confirm": "Наистина ли искате да изтриете акаунта си? <br /><strong>Това действие е необратимо и няма да можете да възстановите нищо от данните си</strong><br /><br />Въведете потребителското си име, за да потвърдите, че искате да унищожите този акаунт.", + "delete_this_account_confirm": "Наистина ли искате да изтриете този акаунт? <br /><strong>Това действие е необратимо и няма да можете да възстановите нищо от данните</strong><br /><br />", "account-deleted": "Акаунтът е изтрит", "fullname": "Цяло име", "website": "Уеб сайт", @@ -78,7 +79,7 @@ "follows_no_one": "Този потребител не следва никого :(", "has_no_posts": "Този потребител не е публикувал нищо досега.", "has_no_topics": "Този потребител не е създавал теми досега.", - "has_no_watched_topics": "Този потребител не е следил нито една тема досега.", + "has_no_watched_topics": "Този потребител не е наблюдавал нито една тема досега.", "has_no_upvoted_posts": "Този потребител не е гласувал положително досега.", "has_no_downvoted_posts": "Този потребител не е гласувал отрицателно досега.", "has_no_voted_posts": "Този потребител не е гласувал досега.", @@ -88,14 +89,21 @@ "topics_per_page": "Теми на страница", "posts_per_page": "Публикации на страница", "notification_sounds": "Изпълняване на звук, когато получите известие", + "notifications_and_sounds": "Известия и звуци", + "incoming-message-sound": "Звук за входящо съобщение", + "outgoing-message-sound": "Звук за изходящо съобщение", + "notification-sound": "Звук за известие", + "no-sound": "Без звук", "browsing": "Настройки за страниците", "open_links_in_new_tab": "Отваряне на външните връзки в нов подпрозорец", "enable_topic_searching": "Включване на търсенето в темите", "topic_search_help": "Ако е включено, търсенето в темата ще замени стандартното поведение на браузъра при търсене в страницата и ще Ви позволи да претърсвате цялата тема, а не само това, което се вижда на екрана", + "delay_image_loading": "Отлагане на зареждането на изображения", + "image_load_delay_help": "Ако е включено, изображенията в темите няма да бъдат зареждани, докато не превъртите страницата до тях", "scroll_to_my_post": "След публикуване на отговор, да се показва новата публикация", - "follow_topics_you_reply_to": "Следване на темите, на които отговаряте", - "follow_topics_you_create": "Следване на темите, които създавате", - "grouptitle": "Изберете заглавието на групата, което искате да се показва", + "follow_topics_you_reply_to": "Наблюдаване на темите, в които отговаряте", + "follow_topics_you_create": "Наблюдаване на темите, които създавате", + "grouptitle": "Заглавие на групата", "no-group-title": "Няма заглавие на група", "select-skin": "Изберете облик", "select-homepage": "Изберете начална страница", @@ -105,5 +113,17 @@ "custom_route_help": "Въведете името на пътя тук, без наклонена черта пред него (пример: „recent“ или „popular“)", "sso.title": "Услуги за еднократно вписване", "sso.associated": "Свързан с", - "sso.not-associated": "Натиснете тук, за да свържете с" + "sso.not-associated": "Натиснете тук, за да свържете с", + "info.latest-flags": "Последни доклади", + "info.no-flags": "Не са открити докладвани публикации", + "info.ban-history": "Скорошна история на блокиранията", + "info.no-ban-history": "Този потребител никога не е бил блокиран", + "info.banned-until": "Блокиран до %1", + "info.banned-permanently": "Блокиран за постоянно", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Няма посочена причина.", + "info.username-history": "История на потребителските имена", + "info.email-history": "Историята на е-пощите", + "info.moderation-note": "Модераторска бележка", + "info.moderation-note.success": "Модераторската бележка е запазена" } \ No newline at end of file diff --git a/public/language/bg/users.json b/public/language/bg/users.json index 3addffa9d9..39c4954aaa 100644 --- a/public/language/bg/users.json +++ b/public/language/bg/users.json @@ -2,6 +2,7 @@ "latest_users": "Последни потребители", "top_posters": "С най-много публикации", "most_reputation": "С най-много репутация", + "most_flags": "С най-много доклади", "search": "Търсене", "enter_username": "Въведете потребителско име, което да потърсите", "load_more": "Зареждане на още", diff --git a/public/language/bn/category.json b/public/language/bn/category.json index b76b0724d2..93309521f8 100644 --- a/public/language/bn/category.json +++ b/public/language/bn/category.json @@ -10,7 +10,11 @@ "share_this_category": "এই বিভাগটি অন্যের সাথে ভাগাভাগি করুন", "watch": "নজর রাখুন", "ignore": "উপেক্ষা করুন", - "watch.message": "আপনি এই বিভাগটিতে নজর রাখছেন", - "ignore.message": "আপনি এই বিভাগটির উপেক্ষা করছেন ", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "প্রেক্ষিত বিভাগসমূহ" } \ No newline at end of file diff --git a/public/language/bn/email.json b/public/language/bn/email.json index a2411a99a7..525460a206 100644 --- a/public/language/bn/email.json +++ b/public/language/bn/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "%1 এর থেকে নতুন মেসেজ এসেছে।", "notif.chat.cta": "কথপোকথন চালিয়ে যেতে এখানে ক্লিক করুন", "notif.chat.unsub.info": "আপনার সাবস্ক্রীপশন সেটিংসের কারনে আপনার এই নোটিফিকেশন পাঠানো হয়েছে", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index 7f363517cc..0e226b9097 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -14,20 +14,25 @@ "invalid-password": "ভুল পাসওয়ার্ড", "invalid-username-or-password": "অনুগ্রহ পূর্বক ইউজারনেম এবং পাসওয়ার্ড উভয়ই প্রদান করুন", "invalid-search-term": "অগ্রহনযোগ্য সার্চ টার্ম", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "ইউজারনেম আগেই ব্যবহৃত", "email-taken": "ইমেইল আগেই ব্যবহৃত", "email-not-confirmed": "আপনার ইমেইল এড্রেস নিশ্চিত করা হয় নি, নিশ্চিত করতে এখানে ক্লিক করুন।", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "খুব ছোট ইউজারনেম", "username-too-long": "ইউজারনেম বড় হয়ে গিয়েছে", "password-too-long": "Password too long", "user-banned": "ব্যবহারকারী নিষিদ্ধ", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "বিভাগটি খুজে পাওয়া যায় নি", "no-topic": "এই টপিক নেই", "no-post": "এই পোষ্ট নেই", @@ -38,10 +43,26 @@ "category-disabled": "বিভাগটি নিষ্ক্রিয়", "topic-locked": "টপিক বন্ধ", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "আপনি অন্য এ্যাডমিনদের নিষিদ্ধ করতে পারেন না!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-long": "Group name too long", "group-already-exists": "গ্রুপ ইতিমধ্যেই বিদ্যমান", "group-name-change-not-allowed": "গ্রুপের নাম পরিবর্তনের অনুমতি নেই", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে", "downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।", "not-enough-reputation-to-downvote": "আপনার এই পোস্ট downvote করার জন্য পর্যাপ্ত সম্মাননা নেই", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/bn/global.json b/public/language/bn/global.json index 9eb59053b1..c3892735fa 100644 --- a/public/language/bn/global.json +++ b/public/language/bn/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "পাওয়া যায়নি", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "অভ্যন্তরীণ ত্রুটি।", + "500.title": "Internal Error.", "500.message": "ওহো! কিছু ভুল হয়েছে মনে হচ্ছে!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "নিবন্ধন", "login": "প্রবেশ", "please_log_in": "অনুগ্রহ করে প্রবেশ করুন", @@ -17,6 +19,7 @@ "welcome_back": "আপনাকে স্বাগতম", "you_have_successfully_logged_in": "আপনি সফলভাবে প্রবেশ করেছেন", "save_changes": "পরিবর্তনগুলি সঞ্চয় করুন", + "save": "Save", "close": "বন্ধ", "pagination": "পাতা নং", "pagination.out_of": "%2 এর মাঝে %1", @@ -50,7 +53,9 @@ "topics": "টপিক", "posts": "পোস্টগুলি", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "দেখেছেন", "reputation": "সন্মাননা", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/bn/groups.json b/public/language/bn/groups.json index d2752a71e8..15be27e837 100644 --- a/public/language/bn/groups.json +++ b/public/language/bn/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/bn/login.json b/public/language/bn/login.json index 7ecec47587..447e2d0b5a 100644 --- a/public/language/bn/login.json +++ b/public/language/bn/login.json @@ -5,7 +5,8 @@ "remember_me": "মনে রাখুন", "forgot_password": "পাসওয়ার্ড ভুলে গিয়েছেন?", "alternative_logins": "বিকল্প প্রবেশ", - "failed_login_attempt": "প্রবেশ সফল হয় নি, আবার চেষ্টা করুন।", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "আপনি সফলভাবে প্রবেশ করেছেন!", - "dont_have_account": "কোন একাউন্ট নেই?" + "dont_have_account": "কোন একাউন্ট নেই?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json index 51c65a2a6a..9e705a29fc 100644 --- a/public/language/bn/modules.json +++ b/public/language/bn/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/bn/notifications.json b/public/language/bn/notifications.json index 497492e0db..f139fe03fc 100644 --- a/public/language/bn/notifications.json +++ b/public/language/bn/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/bn/pages.json b/public/language/bn/pages.json index d7d56d0e4a..877d2f2bfb 100644 --- a/public/language/bn/pages.json +++ b/public/language/bn/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "বিজ্ঞপ্তি", "tags": "ট্যাগসমূহ", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "বিভাগ", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/bn/register.json b/public/language/bn/register.json index 5e2340227f..b8f4eebafd 100644 --- a/public/language/bn/register.json +++ b/public/language/bn/register.json @@ -1,5 +1,6 @@ { "register": "নিবন্ধন", + "cancel_registration": "Cancel Registration", "help.email": "ডিফল্টভাবে, আপনার ইমেইল সর্বসাধারণ থেকে লুকানো থাকবে।", "help.username_restrictions": "%1 এবং %2 অক্ষরের মাঝে একটি অনন্য সদস্য নাম। বাকিরা আপনাকে @<span id='yourUsername'>নাম</span> দিয়ে উল্লেখ করতে পারবেন।", "help.minimum_password_length": "আপনার পাসওয়ার্ড এর দৈর্ঘ্য অন্তত %1 অক্ষরের হতে হবে।", @@ -15,5 +16,8 @@ "alternative_registration": "বিকল্প নিবন্ধন", "terms_of_use": "নিয়মাবলী", "agree_to_terms_of_use": "আমি নিয়মাবলী মেনে চলতে সম্মতি জানালাম", - "registration-added-to-queue": "আপনার নিবন্ধনটি এ্যাপ্লুভাল তালিকায় যুক্ত হয়েছে। একজন এডমিনিস্ট্রেটর কর্তৃক নিবন্ধন গৃহীত হলে আপনি একটি মেইল পাবেন। " + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "আপনার নিবন্ধনটি এ্যাপ্লুভাল তালিকায় যুক্ত হয়েছে। একজন এডমিনিস্ট্রেটর কর্তৃক নিবন্ধন গৃহীত হলে আপনি একটি মেইল পাবেন। ", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index 9fea972e71..6000265eaa 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -26,12 +26,30 @@ "tools": "টুলস", "flag": "ফ্ল্যাগ", "locked": "বন্ধ", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "মডারেশনের জন্য এই পোস্টটি ফ্ল্যাগ করুন", "flag_success": "এই পোস্টটি মডারেশনের জন্য ফ্ল্যাগ করা হয়েছে।", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "এই টপিকটি মুছে ফেলা হয়েছে। শুধুমাত্র টপিক ব্যবস্থাপনার ক্ষমতাপ্রাপ্ত সদস্যগণ এটি দেখতে পারবেন।", "following_topic.message": "এখন থেকে এই টপিকে অন্যকেউ পোস্ট করলে আপনি নোটিফিকেশন পাবেন।", - "not_following_topic.message": "এই টপিক থেকে আপনি আর নোটিফিকেশন পাবেন না।", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "এই টপিকে সাবস্ক্রাইব করতে চাইলে অনুগ্রহ করে নিবন্ধণ করুন অথবা প্রবেশ করুন।", "markAsUnreadForAll.success": "টপিকটি সবার জন্য অপঠিত হিসাবে মার্ক করুন।", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "এই টপিকে নতুন উত্তর এলে বিজ্ঞাপণের মাধ্যমে জানুন।", "unwatch.title": "এই টপিক দেখা বন্ধ করুন", "share_this_post": "এই পোষ্টটি শেয়ার করুন", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "টপিক সম্পর্কিত টুলস", - "thread_tools.markAsUnreadForAll": "\"অপঠিত\" হিসেবে চিহ্নিত করুন", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "টপিক পিন করুন", "thread_tools.unpin": "টপিক আনপিন করুন", "thread_tools.lock": "টপিক বন্ধ করুন", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "এই টপিকটি ক্যাটাগরীতে সরানো হবে", "fork_topic_instruction": "যে পোষ্টটি ফর্ক করতে চান সেটি ক্লিক করুন", "fork_no_pids": "কোন পোষ্ট সিলেক্ট করা হয় নি", + "fork_pid_count": "%1 post(s) selected", "fork_success": "টপিক ফর্ক করা হয়েছে। ফর্ক করা টপিকে যেতে এখানে ক্লিক করুন", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "আপনার টপিকের শিরোনাম দিন", diff --git a/public/language/bn/unread.json b/public/language/bn/unread.json index d7e42fb61b..578e246f3b 100644 --- a/public/language/bn/unread.json +++ b/public/language/bn/unread.json @@ -6,5 +6,8 @@ "selected": "নির্বাচিত", "all": "সবগুলো", "all_categories": "All categories", - "topics_marked_as_read.success": "পঠিত হিসেবে চিহ্নিত টপিকসমূহ" + "topics_marked_as_read.success": "পঠিত হিসেবে চিহ্নিত টপিকসমূহ", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/bn/user.json b/public/language/bn/user.json index 9e26660f70..b1d12ca4c6 100644 --- a/public/language/bn/user.json +++ b/public/language/bn/user.json @@ -6,6 +6,7 @@ "postcount": "সর্বমোট পোষ্ট", "email": "ইমেইল", "confirm_email": "ইমেইল নিশ্চিত করুন", + "account_info": "Account Info", "ban_account": "একাউন্ট নিষিদ্ধ করুন", "ban_account_confirm": "আপনি কি নিশ্চিত যে এই সদস্যকে নিষিদ্ধ করতে চান ?", "unban_account": "নিষেদ্ধাজ্ঞা তুলে নিন", @@ -88,14 +89,21 @@ "topics_per_page": "প্রতি পেজে কতগুলো টপিক থাকবে", "posts_per_page": "প্রতি পেইজে কতগুলো পোষ্ট থাকবে", "notification_sounds": "নোটিফিকেশনের জন্য নোটিফিকেশন সাউন্ড এনাবল করুন", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing সেটিংস", "open_links_in_new_tab": "আউটগোয়িং লিংকগুলো নতুন ট্যাবে খুলুন", "enable_topic_searching": "In-Topic সার্চ সক্রীয় করো", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/bn/users.json b/public/language/bn/users.json index 4fca6b9a27..2af7331a13 100644 --- a/public/language/bn/users.json +++ b/public/language/bn/users.json @@ -2,6 +2,7 @@ "latest_users": "সর্বশেষ নিবন্ধিত সদস্যরা:", "top_posters": "সর্বোচ্চ পোষ্টকারী", "most_reputation": "সর্বোচ্চ সম্মাননাধারী", + "most_flags": "Most Flags", "search": "খুঁজুন", "enter_username": "ইউজারনেম এর ভিত্তিতে সার্চ করুন", "load_more": "আরো লোড করুন", diff --git a/public/language/cs/category.json b/public/language/cs/category.json index d91e9ebda0..4a0009323c 100644 --- a/public/language/cs/category.json +++ b/public/language/cs/category.json @@ -1,16 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", + "category": "Kategorie", + "subcategories": "Podkategorie", "new_topic_button": "Nové téma", - "guest-login-post": "Log in to post", + "guest-login-post": "Přihlásit se pro přispívání", "no_topics": "<strong>V této kategorii zatím nejsou žádné příspěvky.</strong><br />Můžeš být první!", "browsing": "prohlíží", "no_replies": "Nikdo ještě neodpověděl", - "no_new_posts": "No new posts.", + "no_new_posts": "Žádné nové příspěvky", "share_this_category": "Share this category", - "watch": "Watch", + "watch": "Sledovat", "ignore": "Ignorovat", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", - "watched-categories": "Watched categories" + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Sledované kategorie" } \ No newline at end of file diff --git a/public/language/cs/email.json b/public/language/cs/email.json index bcb35b58ae..9c456dd30e 100644 --- a/public/language/cs/email.json +++ b/public/language/cs/email.json @@ -1,34 +1,35 @@ { "password-reset-requested": "Požadována obnova hesla - %1!", "welcome-to": "Vítejte v %1", - "invite": "Invitation from %1", + "invite": "Pozvánka od %1", "greeting_no_name": "Dobrý den", "greeting_with_name": "Dobrý den %1", - "welcome.text1": "Děkujeme vám za registraci s %1!", + "welcome.text1": "Děkujeme vám za registraci na %1!", "welcome.text2": "Pro úplnou aktivaci vašeho účtu potřebujeme ověřit vaší emailovou adresu.", - "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.text3": "Administrátor právě potvrdil vaší registraci. Nyní se můžete přihlásit jménem a heslem.", "welcome.cta": "Klikněte zde pro potvrzení vaší emailové adresy", - "invitation.text1": "%1 has invited you to join %2", - "invitation.ctr": "Click here to create your account.", + "invitation.text1": "%1 Vás pozval abyste se připojil k %2", + "invitation.ctr": "Klikněte zde pro vytvoření vašeho účtu", "reset.text1": "Obdrželi jsme požadavek na obnovu hesla, pravděpodobně kvůli tomu, že jste ho zapomněli. Pokud to není tento případ, ignorujte, prosím, tento email.", "reset.text2": "Přejete-li si pokračovat v obnově vašeho hesla, klikněte, prosím, na následující odkaz:", "reset.cta": "Klikněte zde, chcete-li obnovit vaše heslo", - "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": "Heslo úspěšně změněno", + "reset.notify.text1": "Informujeme Vás, že na %1 vaše heslo bylo úspěšně změněno.", + "reset.notify.text2": "Pokud jste to neschválil, prosíme neprodleně kontaktujte správce.", "digest.notifications": "Máte tu nepřečtená oznámení od %1:", "digest.latest_topics": "Nejnovější témata od %1", "digest.cta": "Kliknutím zde navštívíte %1", "digest.unsub.info": "Tento výtah vám byl odeslán, protože jste si to nastavili ve vašich odběrech.", "digest.no_topics": "Dosud tu nebyly žádné aktivní témata %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "den", + "digest.week": "týden", + "digest.month": "měsíc", + "digest.subject": "Výběr pro %1", "notif.chat.subject": "Nová zpráva z chatu od %1", "notif.chat.cta": "Chcete-li pokračovat v konverzaci, klikněte zde.", "notif.chat.unsub.info": "Toto oznámení z chatu vám bylo zasláno, protože jste si to nastavili ve vašich odběrech.", - "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": "Klikněte zde pro přečtené celého tématu", + "notif.post.unsub.info": "Toto oznámení Vám bylo odesláno na základě vašeho nastavení odběru.", "test.text1": "Tento testovací email slouží k ověření, že mailer je správně nastaven. NodeBB.", "unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.", "closing": "Díky!" diff --git a/public/language/cs/error.json b/public/language/cs/error.json index dfec525151..204707feab 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -14,20 +14,25 @@ "invalid-password": "Neplatné heslo", "invalid-username-or-password": "Stanovte, prosím, oboje, jak uživatelské jméno, tak heslo", "invalid-search-term": "Neplatný výraz pro vyhledávání", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Uživatelské jméno je již použito", "email-taken": "Email je již použit", "email-not-confirmed": "Vaše emailová adresa zatím nebyla potvrzena. Kliknutím zde svůj email potvrdíte.", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", - "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "confirm-email-already-sent": "Potvrzovací email již byl odeslán. Vyčkejte %1 minut pokud chcete odeslat další.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Uživatelské jméno je příliš krátké", "username-too-long": "Uživatelské jméno je příliš dlouhé", - "password-too-long": "Password too long", + "password-too-long": "Heslo je příliš dlouhé", "user-banned": "Uživatel byl zakázán", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategorie neexistuje", "no-topic": "Téma neexistuje", "no-post": "Příspěvek neexistuje", @@ -38,10 +43,26 @@ "category-disabled": "Kategorie zakázána", "topic-locked": "Téma uzamčeno", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Nemůžete zakazovat ostatní administrátory!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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": "Název skupiny je příliš krátký", + "group-name-too-long": "Group name too long", "group-already-exists": "Skupina už exstuje", "group-name-change-not-allowed": "Změna názvu skupiny není povolena", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Systém reputací je zakázán.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -93,11 +117,15 @@ "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": "Chyba při registraci", "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", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "wrong-login-type-email": "Použijte prosím Váš e-mail pro přihlášení", + "wrong-login-type-username": "Použijte prosím Váše přihlašovací jméno pro přihlášení", + "invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).", "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "Nemůžete vyhodit sami sebe ze kupiny", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/cs/global.json b/public/language/cs/global.json index 1f1900a0f6..f491e40a0a 100644 --- a/public/language/cs/global.json +++ b/public/language/cs/global.json @@ -3,12 +3,14 @@ "search": "Hledat", "buttons.close": "Zavřít", "403.title": "Přístup odepřen", - "403.message": "You seem to have stumbled upon a page that you do not have access to.", - "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", + "403.message": "Zdá se, že jste narazil/a na stránky na které nemáte přístup.", + "403.login": "Možná byste měli se <a href='%1/login'>zkusit přihlásit</a>?", "404.title": "Stránka nenalezena", - "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Neznámá chyba", + "404.message": "Zdá se, že jste narazil/a na stránku která neexistuje. Vrátit se zpět na <a href='%1/'>domovskou stránku</a>.", + "500.title": "Internal Error.", "500.message": "Jejda, vypadá to, že se něco pokazilo.", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registrovat", "login": "Přihlásit se", "please_log_in": "Přihlašte se, prosím", @@ -17,45 +19,48 @@ "welcome_back": "Vítejte zpět", "you_have_successfully_logged_in": "Vaše přihlášení proběhlo úspěšně", "save_changes": "Uložit změny", + "save": "Save", "close": "Zrušit", "pagination": "Stránkování", "pagination.out_of": "%1 z %2", "pagination.enter_index": "Enter index", "header.admin": "Administrace", - "header.categories": "Categories", - "header.recent": "Aktuality", + "header.categories": "Kategorie", + "header.recent": "Nejnovější", "header.unread": "Nepřečtené", "header.tags": "Tagy", "header.popular": "Populární", "header.users": "Uživatelé", - "header.groups": "Groups", - "header.chats": "Chats", + "header.groups": "Skupiny", + "header.chats": "Chaty", "header.notifications": "Oznámení", "header.search": "Hledat", "header.profile": "Můj profil", - "header.navigation": "Navigation", + "header.navigation": "Navigace", "notifications.loading": "Načítání upozornění", - "chats.loading": "Načítání grafů", + "chats.loading": "Načítání chatů", "motd.welcome": "Vítejte na NodeBB, diskusní platforma buducnosti.", "previouspage": "Předchozí stránka", "nextpage": "Další stránka", - "alert.success": "Success", + "alert.success": "Úspěch", "alert.error": "Chyba", "alert.banned": "Banned", "alert.banned.message": "You have just been banned, you will now be logged out.", - "alert.unfollow": "You are no longer following %1!", - "alert.follow": "You are now following %1!", + "alert.unfollow": "Již nesledujete %1!", + "alert.follow": "Nyní sledujete %1!", "online": "Online", "users": "Uživatelé", "topics": "Témata", "posts": "Příspěvky", - "best": "Best", + "best": "Nejlepší", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Zobrazení", - "reputation": "Reputation", - "read_more": "read more", - "more": "More", + "reputation": "Reputace", + "read_more": "čtěte více", + "more": "Více", "posted_ago_by_guest": "posted %1 by Guest", "posted_ago_by": "posted %1 by %2", "posted_ago": "posted %1", @@ -69,9 +74,9 @@ "norecentposts": "Žádné nedávné příspěvky", "norecenttopics": "Žádné nedávné témata", "recentposts": "Nedávné příspěvky", - "recentips": "Recently Logged In IPs", + "recentips": "Naposledy zaznamenané IP adresy", "away": "Pryč", - "dnd": "Do not disturb", + "dnd": "Nevyrušovat", "invisible": "Neviditelný", "offline": "Offline", "email": "Email", @@ -80,15 +85,18 @@ "guests": "Hosté", "updated.title": "Fórum zaktualizováno", "updated.message": "Toto fórum bylo právě aktualizováno na poslední verzi. Klikněte zde a obnovte tuto stránku.", - "privacy": "Privacy", - "follow": "Follow", - "unfollow": "Unfollow", + "privacy": "Soukromí", + "follow": "Sledovat", + "unfollow": "Prestat sledovat", "delete_all": "Vymazat vše", - "map": "Map", + "map": "Mapa", "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "ip_address": "IP adresa", + "enter_page_number": "Zadejte číslo stránky", + "upload_file": "Nahrár soubor", + "upload": "Nahrát", + "allowed-file-types": "Povolené typy souborů jsou %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/cs/groups.json b/public/language/cs/groups.json index 5bfd7ed3ec..4715b0aefa 100644 --- a/public/language/cs/groups.json +++ b/public/language/cs/groups.json @@ -1,54 +1,56 @@ { "groups": "Skupiny", "view_group": "Prohlédnout skupinu", - "owner": "Group Owner", - "new_group": "Create New Group", - "no_groups_found": "There are no groups to see", - "pending.accept": "Accept", - "pending.reject": "Reject", - "pending.accept_all": "Accept All", - "pending.reject_all": "Reject All", - "pending.none": "There are no pending members at this time", - "invited.none": "There are no invited members at this time", - "invited.uninvite": "Rescind Invitation", - "invited.search": "Search for a user to invite to this group", - "invited.notification_title": "You have been invited to join <strong>%1</strong>", - "request.notification_title": "Group Membership Request from <strong>%1</strong>", - "request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>", - "cover-save": "Save", - "cover-saving": "Saving", - "details.title": "podrobnosti skupiny", + "owner": "Vlastník skupiny", + "new_group": "Vytvořit novou skupinu", + "no_groups_found": "Žádné skupiny k prohlížení", + "pending.accept": "Přijmout", + "pending.reject": "Odmítnout", + "pending.accept_all": "Přijmout vše", + "pending.reject_all": "Odmítnout vše", + "pending.none": "Žádní čekající členové v tuto chvíli", + "invited.none": "Žádní pozvaní členové v tuto chvíli", + "invited.uninvite": "Zrušit pozvánku", + "invited.search": "Hledat uživatele k pozvání do této skupiny", + "invited.notification_title": "Byl jste pozván abyste se připojil/a k <strong>%1</strong>", + "request.notification_title": "Požadavek na členství ve skupině od <strong>%1</strong>", + "request.notification_text": "<strong>%1</strong> požádál o členství v <strong>%2</strong>", + "cover-save": "Uložit", + "cover-saving": "Ukládám", + "details.title": "Podrobnosti skupiny", "details.members": "Seznam členů", - "details.pending": "Pending Members", - "details.invited": "Invited Members", + "details.pending": "Čekající členové", + "details.invited": "Pozvaní členové", "details.has_no_posts": "Členové této skupiny dosud neodeslali ani jeden příspěvek.", "details.latest_posts": "Nejnovější příspěvky", - "details.private": "Private", - "details.disableJoinRequests": "Disable join requests", - "details.grant": "Grant/Rescind Ownership", - "details.kick": "Kick", - "details.owner_options": "Group Administration", - "details.group_name": "Group Name", - "details.member_count": "Member Count", - "details.creation_date": "Creation Date", - "details.description": "Description", - "details.badge_preview": "Badge Preview", - "details.change_icon": "Change Icon", - "details.change_colour": "Change Colour", - "details.badge_text": "Badge Text", - "details.userTitleEnabled": "Show Badge", - "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", - "details.delete_group": "Delete Group", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", - "event.updated": "Group details have been updated", - "event.deleted": "The group \"%1\" has been deleted", - "membership.accept-invitation": "Accept Invitation", - "membership.invitation-pending": "Invitation Pending", - "membership.join-group": "Join Group", - "membership.leave-group": "Leave Group", - "membership.reject": "Reject", - "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "details.private": "Soukromé", + "details.disableJoinRequests": "Zakázat žádosti o připojení", + "details.grant": "Přidat/Zrušit vlastnictví", + "details.kick": "Vyhodit", + "details.owner_options": "Administrátor skupiny", + "details.group_name": "Název skupiny", + "details.member_count": "Počet členů", + "details.creation_date": "Datum vytvoření", + "details.description": "Popis", + "details.badge_preview": "Náhled odznaku", + "details.change_icon": "Změnit ikonu", + "details.change_colour": "Změnit barvu", + "details.badge_text": "Text odznaku", + "details.userTitleEnabled": "Zobrazit odznak", + "details.private_help": "Pokud je povoleno, připojování do skupin vyžaduje schválení od vlastníka skupiny", + "details.hidden": "Skrytý", + "details.hidden_help": "Pokud je povoleno, tato skupina nebude zobrazena v seznamu skupin, uživatelé budou muset být pozváni manuálně", + "details.delete_group": "Odstranit skupinu", + "details.private_system_help": "Soukromé skupiny jsou zakázáné na systémové úrovni, tato možnost nic nedělá", + "event.updated": "Podrobnosti skupiny byly aktualizovány", + "event.deleted": "Skupina \"%1\" byla odstraněna", + "membership.accept-invitation": "Přijmout pozvání", + "membership.invitation-pending": "Čekající pozvání", + "membership.join-group": "Vstoupit do skupiny", + "membership.leave-group": "Opustit skupinu", + "membership.reject": "Odmítnout", + "new-group.group_name": "Název skupiny:", + "upload-group-cover": "Nahrát titulní obrázek skupiny", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/cs/login.json b/public/language/cs/login.json index 3308fbdde6..c949cf14ac 100644 --- a/public/language/cs/login.json +++ b/public/language/cs/login.json @@ -1,11 +1,12 @@ { - "username-email": "Username / Email", - "username": "Username", + "username-email": "Uživatel / Email", + "username": "Uživatel", "email": "Email", "remember_me": "Zapamatovat si mě?", "forgot_password": "Zapomněli jste heslo?", "alternative_logins": "Další způsoby přihlášení", - "failed_login_attempt": "Přihlášení se nezdařilo, zkuste to prosím znovu.", + "failed_login_attempt": "Přihlášení neúspěšné", "login_successful": "Přihlášení proběhlo úspěšně!", - "dont_have_account": "Nemáte účet?" + "dont_have_account": "Nemáte účet?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index cb37aa7539..38bcd30ba9 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -4,34 +4,44 @@ "chat.send": "Odeslat", "chat.no_active": "Nemáte žádné aktivní konverzace.", "chat.user_typing": "%1 píše ...", - "chat.user_has_messaged_you": "%1 has messaged you.", - "chat.see_all": "See all chats", - "chat.mark_all_read": "Mark all chats read", - "chat.no-messages": "Please select a recipient to view chat message history", - "chat.no-users-in-room": "No users in this room", - "chat.recent-chats": "Recent Chats", + "chat.user_has_messaged_you": "%1 Vám napsal.", + "chat.see_all": "Prohlédnout všechny chaty", + "chat.mark_all_read": "Označit vše jako přečtené", + "chat.no-messages": "Prosím vyberte příjemce k prohlédnutí historie zpráv.", + "chat.no-users-in-room": "Žádní uživatelé v místnosti.", + "chat.recent-chats": "Aktuální chaty", "chat.contacts": "Kontakty", "chat.message-history": "Historie zpráv", - "chat.pop-out": "Pop out chat", + "chat.pop-out": "Skrýt chat", "chat.maximize": "Maximalizovat", "chat.seven_days": "7 dní", "chat.thirty_days": "30 dní", "chat.three_months": "3 měsíce", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", - "composer.compose": "Compose", - "composer.show_preview": "Show Preview", - "composer.hide_preview": "Hide Preview", - "composer.user_said_in": "%1 said in %2:", - "composer.user_said": "%1 said:", - "composer.discard": "Are you sure you wish to discard this post?", - "composer.submit_and_lock": "Submit and Lock", + "chat.delete_message_confirm": "Jste si jisti že chcete odstranit tuto zprávu?", + "chat.roomname": "Místnost %1", + "chat.add-users-to-room": "Přidat uživatele do místnosti", + "composer.compose": "Napsat", + "composer.show_preview": "Ukázat náhled", + "composer.hide_preview": "Skrýt náhled", + "composer.user_said_in": "%1 řekl v %2:", + "composer.user_said": "%1 řekl:", + "composer.discard": "Jste si jisti, že chcete zrušit tento příspěvek?", + "composer.submit_and_lock": "Potvrdit a uzamknout", "composer.toggle_dropdown": "Toggle Dropdown", - "composer.uploading": "Uploading %1", + "composer.uploading": "Odesílám %1", + "composer.formatting.bold": "Tučné", + "composer.formatting.italic": "Kurzíva", + "composer.formatting.list": "Seznam", + "composer.formatting.strikethrough": "Přeškrtnutí", + "composer.formatting.link": "Odkaz", + "composer.formatting.picture": "Obrázek", + "composer.upload-picture": "Nahrát obrázek", + "composer.upload-file": "Nahrát soubor", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", - "bootbox.cancel": "Cancel", - "bootbox.confirm": "Confirm", + "bootbox.cancel": "Zrušit", + "bootbox.confirm": "Potvrdit", "cover.dragging_title": "Cover Photo Positioning", "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", "cover.saved": "Cover photo image and position saved" diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index 0137a973e3..631bdfd5e0 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -1,24 +1,21 @@ { "title": "Upozornění", - "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", + "no_notifs": "Nemáte žádná nová upozornění.", + "see_all": "Zobrazit všechna upozornění", + "mark_all_read": "Označit všechna upozornění jako přečtená", + "back_to_home": "Zpět na %1", "outgoing_link": "Odkaz mimo fórum", - "outgoing_link_message": "You are now leaving %1", - "continue_to": "Continue to %1", - "return_to": "Return to %1", - "new_notification": "New Notification", - "you_have_unread_notifications": "You have unread notifications.", - "new_message_from": "New message from <strong>%1</strong>", + "outgoing_link_message": "Opouštíte %1", + "continue_to": "Pokračovat na %1", + "return_to": "Vrátit na %1", + "new_notification": "Nové upozornění", + "you_have_unread_notifications": "Máte nepřečtená upozornění.", + "new_message_from": "Nová zpráva od <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> has upvoted your post in <strong>%2</strong>.", "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", @@ -31,8 +28,8 @@ "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", "new_register": "<strong>%1</strong> sent a registration request.", "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", - "email-confirmed": "Email Confirmed", - "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", - "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", - "email-confirm-sent": "Confirmation email sent." + "email-confirmed": "Email potvrzen", + "email-confirmed-message": "Děkujeme za ověření Vaší emailové adresy. Váš účet je nyní aktivován.", + "email-confirm-error-message": "Nastal problém s ověřením Vaší emailové adresy. Pravděpodobně neplatný nebo expirovaný kód.", + "email-confirm-sent": "Ověřovací email odeslán." } \ No newline at end of file diff --git a/public/language/cs/pages.json b/public/language/cs/pages.json index 285aa30017..fa26c71136 100644 --- a/public/language/cs/pages.json +++ b/public/language/cs/pages.json @@ -1,46 +1,49 @@ { - "home": "Home", - "unread": "Unread Topics", - "popular-day": "Popular topics today", - "popular-week": "Popular topics this week", - "popular-month": "Popular topics this month", - "popular-alltime": "All time popular topics", - "recent": "Recent Topics", - "flagged-posts": "Flagged Posts", - "users/online": "Online Users", - "users/latest": "Latest Users", - "users/sort-posts": "Users with the most posts", - "users/sort-reputation": "Users with the most reputation", - "users/banned": "Banned Users", - "users/search": "User Search", - "notifications": "Notifications", - "tags": "Tags", - "tag": "Topics tagged under \"%1\"", - "register": "Register an account", - "login": "Login to your account", - "reset": "Reset your account password", - "categories": "Categories", - "groups": "Groups", - "group": "%1 group", - "chats": "Chats", - "chat": "Chatting with %1", + "home": "Domů", + "unread": "Nepřečtená témata", + "popular-day": "Dnešní oblíbená témata", + "popular-week": "Oblíbená témata pro tento týden", + "popular-month": "Oblíbená témata pro tento měsíc", + "popular-alltime": "Oblíbená témata za celou dobu", + "recent": "Aktuální témata", + "flagged-posts": "Označené příspěvky", + "users/online": "Uživatelé online", + "users/latest": "Nejnovější uživatelé", + "users/sort-posts": "Uživatelé s nejvíce příspěvky", + "users/sort-reputation": "Uživatelé s nejlepší reputací", + "users/banned": "Zabanovaní uživatelé", + "users/most-flags": "Most flagged users", + "users/search": "Hledání uživatele", + "notifications": "Oznámení", + "tags": "Tagy", + "tag": "Téma označeno pod \"%1\"", + "register": "Zaregistrovat účet", + "registration-complete": "Registration complete", + "login": "Přihlásit se ke svému účtu", + "reset": "Obnovit heslo k účtu", + "categories": "Kategorie", + "groups": "Skupiny", + "group": "%1 skupina", + "chats": "Chaty", + "chat": "Chatovat s %1", "account/edit": "Editing \"%1\"", "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", - "account/followers": "People who follow %1", - "account/posts": "Posts made by %1", - "account/topics": "Topics created by %1", - "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", - "account/settings": "User Settings", + "account/followers": "Lidé kteří sledují %1", + "account/posts": "Příspěvky od %1", + "account/topics": "Příspěvky vytvořeny uživatelem %1", + "account/groups": "%1's skupiny", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Uživatelské nastavení", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "account/best": "Nejlepší příspěvky od %1", + "confirm": "Email potvrzen", "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", "maintenance.messageIntro": "Additionally, the administrator has left this message:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "throttled.text": "%1 je v současnou chvíli nedostupný pro velkou zátěž. Prosíme zkuste to za chvíli." } \ No newline at end of file diff --git a/public/language/cs/recent.json b/public/language/cs/recent.json index 13e8b2284e..68867fe0a5 100644 --- a/public/language/cs/recent.json +++ b/public/language/cs/recent.json @@ -3,17 +3,17 @@ "day": "Den", "week": "Týden", "month": "Měsíc", - "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.", - "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." + "year": "Rok", + "alltime": "Pořád", + "no_recent_topics": "Nebyly nalezeny žádné nové téma.", + "no_popular_topics": "Žádná oblíbená téma.", + "there-is-a-new-topic": "K dispozici je nová téma.", + "there-is-a-new-topic-and-a-new-post": "K dispozici je nové téma a nový příspěvěk.", + "there-is-a-new-topic-and-new-posts": "K dispozici je nové téma a %1 nových příspěvků.", + "there-are-new-topics": "K dispozici je %1 nových témat.", + "there-are-new-topics-and-a-new-post": "K dispozici je %1 nových témat a jeden nový příspěvek.", + "there-are-new-topics-and-new-posts": "K dispozici je %1 nových témat a %2 nových příspěvků.", + "there-is-a-new-post": "K dispozici je nový příspěvek.", + "there-are-new-posts": "K dispozici je %1 nových příspěvků.", + "click-here-to-reload": "Kliknutím sem znovu načtete." } \ No newline at end of file diff --git a/public/language/cs/register.json b/public/language/cs/register.json index db7a689039..6aa6c9d5b9 100644 --- a/public/language/cs/register.json +++ b/public/language/cs/register.json @@ -1,5 +1,6 @@ { "register": "Registrace", + "cancel_registration": "Cancel Registration", "help.email": "Váš email nebude bez vašeho svolení zveřejněn.", "help.username_restrictions": "Jedinečné uživatelské jméno dlouhé %1 až %2 znaků. Ostatní uživatelé Vás mohou zmínit jako @<span id='yourUsername'>uživatelské-jméno</span>.", "help.minimum_password_length": "Délka vašeho hesla musí být alespoň %1 znaků.", @@ -15,5 +16,8 @@ "alternative_registration": "Jiný způsob registrace", "terms_of_use": "Podmínky", "agree_to_terms_of_use": "Souhlasím s Podmínkami", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Vaše registrace byla přidána do fronty. Obdržíte e-mail až ji správce schválí.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/cs/reset_password.json b/public/language/cs/reset_password.json index 566b746435..87e85611d1 100644 --- a/public/language/cs/reset_password.json +++ b/public/language/cs/reset_password.json @@ -11,7 +11,7 @@ "enter_email_address": "Zadejte emailovou adresu", "password_reset_sent": "Obnova hesla odeslána", "invalid_email": "Špatný email / Email neexistuje!", - "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_expired": "Your password has expired, please choose a new password" + "password_too_short": "Zadané heslo je příliš krátké, zvolte si prosím jiné.", + "passwords_do_not_match": "Vámi zadaná hesla se neshodují.", + "password_expired": "Platnost Vašeho hesla vypršela, zvolte si prosím nové." } \ No newline at end of file diff --git a/public/language/cs/search.json b/public/language/cs/search.json index 277c0a32bc..93fae4f92d 100644 --- a/public/language/cs/search.json +++ b/public/language/cs/search.json @@ -1,7 +1,7 @@ { "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", "no-matches": "No matches found", - "advanced-search": "Advanced Search", + "advanced-search": "Pokročilé hledání", "in": "In", "titles": "Titles", "titles-posts": "Titles and Posts", @@ -12,28 +12,28 @@ "at-least": "At least", "at-most": "At most", "post-time": "Post time", - "newer-than": "Newer than", - "older-than": "Older than", + "newer-than": "Novější než", + "older-than": "Starší než", "any-date": "Any date", - "yesterday": "Yesterday", - "one-week": "One week", - "two-weeks": "Two weeks", - "one-month": "One month", + "yesterday": "Včera", + "one-week": "Jeden týden", + "two-weeks": "Dva týdny", + "one-month": "Jeden měsíc", "three-months": "Three months", - "six-months": "Six months", - "one-year": "One year", - "sort-by": "Sort by", + "six-months": "Šest měsíců", + "one-year": "Jeden rok", + "sort-by": "Řadit dle", "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", + "username": "Uživatelské jméno", + "category": "Kategorie", "descending": "In descending order", "ascending": "In ascending order", - "save-preferences": "Save preferences", - "clear-preferences": "Clear preferences", + "save-preferences": "Uložit nastavení", + "clear-preferences": "Vymazat nastavení", "search-preferences-saved": "Search preferences saved", "search-preferences-cleared": "Search preferences cleared", "show-results-as": "Show results as" diff --git a/public/language/cs/success.json b/public/language/cs/success.json index fde8a77044..e9dec2704e 100644 --- a/public/language/cs/success.json +++ b/public/language/cs/success.json @@ -1,6 +1,6 @@ { - "success": "Success", - "topic-post": "You have successfully posted.", - "authentication-successful": "Authentication Successful", - "settings-saved": "Settings saved!" + "success": "Úspěch", + "topic-post": "Úspěšně umístěno.", + "authentication-successful": "Úspěšné přihlášení", + "settings-saved": "Nastavení byla uložena!" } \ No newline at end of file diff --git a/public/language/cs/tags.json b/public/language/cs/tags.json index 8fc07c7da2..59b7f40c43 100644 --- a/public/language/cs/tags.json +++ b/public/language/cs/tags.json @@ -1,7 +1,7 @@ { "no_tag_topics": "Není zde žádné téma s tímto tagem.", "tags": "Tagy", - "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here": "Zde vložte tagy, každý o délce %1 až %2 znaků.", "enter_tags_here_short": "Vložte tagy ...", "no_tags": "Zatím tu není žádný tag." } \ No newline at end of file diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index 3bced7f182..62964c42dc 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -5,55 +5,79 @@ "no_topics_found": "Nebyla nalezena žádná témata!", "no_posts_found": "Nebyly nalezeny žádné příspěvky!", "post_is_deleted": "Tento příspěvek je vymazán!", - "topic_is_deleted": "This topic is deleted!", + "topic_is_deleted": "Toto téma je smazané!", "profile": "Profil", - "posted_by": "Posted by %1", - "posted_by_guest": "Posted by Guest", + "posted_by": "Přidal %1", + "posted_by_guest": "Přidal Host", "chat": "Chat", "notify_me": "Sledovat toto téma", "quote": "Citovat", "reply": "Odpovědět", - "reply-as-topic": "Reply as topic", - "guest-login-reply": "Log in to reply", + "reply-as-topic": "Odpovědět jako Téma", + "guest-login-reply": "Přihlásit se pro odpověď", "edit": "Upravit", "delete": "Smazat", - "purge": "Purge", - "restore": "Restore", + "purge": "Vypráznit", + "restore": "Obnovit", "move": "Přesunout", "fork": "Rozdělit", "link": "Odkaz", "share": "Sdílet", "tools": "Nástroje", "flag": "Flag", - "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "locked": "Uzamčeno", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", "markAsUnreadForAll.success": "Topic marked as unread for all.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", - "watch": "Watch", + "mark_unread": "Označ za nepřečtené", + "mark_unread.success": "Téma označeno jako nepřečtené", + "watch": "Sledovat", "unwatch": "Unwatch", "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", - "share_this_post": "Share this Post", - "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Označit jako nepřečtené", - "thread_tools.pin": "Pin Topic", - "thread_tools.unpin": "Unpin Topic", - "thread_tools.lock": "Lock Topic", - "thread_tools.unlock": "Unlock Topic", - "thread_tools.move": "Move Topic", - "thread_tools.move_all": "Move All", - "thread_tools.fork": "Fork Topic", - "thread_tools.delete": "Delete Topic", - "thread_tools.delete-posts": "Delete Posts", - "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", - "thread_tools.restore": "Restore Topic", + "share_this_post": "Sdílet toto téma", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", + "thread_tools.title": "Správa tématu", + "thread_tools.markAsUnreadForAll": "Mark unread for all", + "thread_tools.pin": "Připnout téma", + "thread_tools.unpin": "Odepnout téma", + "thread_tools.lock": "Zamknout téma", + "thread_tools.unlock": "Odemknout téma", + "thread_tools.move": "Přesunout téma", + "thread_tools.move_all": "Přesunout vše", + "thread_tools.fork": "Větvit téma", + "thread_tools.delete": "Odstranit téma", + "thread_tools.delete-posts": "Odstranit přispěvky", + "thread_tools.delete_confirm": "Opravdu chcete smazat toto téma.", + "thread_tools.restore": "Obnovit téma", "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", "thread_tools.purge": "Purge Topic", "thread_tools.purge_confirm": "Are you sure you want to purge this topic?", @@ -65,48 +89,49 @@ "disabled_categories_note": "Vypnuté (disabled) kategorie jsou šedé.", "confirm_move": "Přesunout", "confirm_fork": "Rozdělit", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Záložka", + "favourites": "Záložky", + "favourites.has_no_favourites": "Zatím jste do záložek nepřidal žádné příspěvky.", "loading_more_posts": "Načítání více příspěvků", "move_topic": "Přesunout téma", - "move_topics": "Move Topics", + "move_topics": "Přesunout témata", "move_post": "Přesunout příspěvek", - "post_moved": "Post moved!", + "post_moved": "Příspěvek přesunut!", "fork_topic": "Rozdělit příspěvek", "topic_will_be_moved_to": "Toto téma bude přesunuto do kategorie", "fork_topic_instruction": "Vyber příspěvky, které chceš oddělit", "fork_no_pids": "Žádné příspěvky nebyly vybrány!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", - "composer.title_placeholder": "Enter your topic title here...", - "composer.handle_placeholder": "Name", - "composer.discard": "Discard", - "composer.submit": "Submit", + "composer.title_placeholder": "Zadejte název tématu...", + "composer.handle_placeholder": "Jméno", + "composer.discard": "Zrušit", + "composer.submit": "Odeslat", "composer.replying_to": "Replying to %1", - "composer.new_topic": "New Topic", - "composer.uploading": "uploading...", - "composer.thumb_url_label": "Paste a topic thumbnail URL", - "composer.thumb_title": "Add a thumbnail to this topic", + "composer.new_topic": "Nové téma", + "composer.uploading": "nahrávání...", + "composer.thumb_url_label": "Vložit URL náhled tématu", + "composer.thumb_title": "Přidat k tématu náhled", "composer.thumb_url_placeholder": "http://example.com/thumb.png", - "composer.thumb_file_label": "Or upload a file", - "composer.thumb_remove": "Clear fields", + "composer.thumb_file_label": "Nebo nahrajte soubor", + "composer.thumb_remove": "Vymazat pole", "composer.drag_and_drop_images": "Drag and Drop Images Here", "more_users_and_guests": "%1 more user(s) and %2 guest(s)", "more_users": "%1 more user(s)", "more_guests": "%1 more guest(s)", "users_and_others": "%1 and %2 others", - "sort_by": "Sort by", - "oldest_to_newest": "Oldest to Newest", - "newest_to_oldest": "Newest to Oldest", - "most_votes": "Most votes", - "most_posts": "Most posts", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", + "sort_by": "Řadit dle", + "oldest_to_newest": "Od nejstarších po nejnovější", + "newest_to_oldest": "Od nejnovějších po nejstarší", + "most_votes": "Nejvíce hlasů", + "most_posts": "Nejvíce příspěvků", + "stale.title": "Přesto vytvořit nové téma", + "stale.warning": "Reagujete na starší téma. Nechcete raději vytvořit téma nové a na původní v něm odkázat?", + "stale.create": "Vytvořit nové téma", + "stale.reply_anyway": "Přesto reagovat na toto téma", "link_back": "Re: [%1](%2)", "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "offensive": "Urážlivé", + "custom-flag-reason": "Vložte důvod oznámení" } \ No newline at end of file diff --git a/public/language/cs/unread.json b/public/language/cs/unread.json index 1ded2636c9..0c6ee0319f 100644 --- a/public/language/cs/unread.json +++ b/public/language/cs/unread.json @@ -2,9 +2,12 @@ "title": "Nepřečtené", "no_unread_topics": "Nejsou zde žádné nepřečtené témata.", "load_more": "Načíst další", - "mark_as_read": "Označit jako přeštené", + "mark_as_read": "Označit jako přečtené", "selected": "Vybrané", "all": "Vše", - "all_categories": "All categories", - "topics_marked_as_read.success": "Téma bylo označeno jako přečtené!" + "all_categories": "Všechny kategorie", + "topics_marked_as_read.success": "Téma bylo označeno jako přečtené!", + "all-topics": "Všechna témata", + "new-topics": "Nová témata", + "watched-topics": "Sledovaná témata" } \ No newline at end of file diff --git a/public/language/cs/uploads.json b/public/language/cs/uploads.json index 1622cb5693..0adbf0d9f0 100644 --- a/public/language/cs/uploads.json +++ b/public/language/cs/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", + "uploading-file": "Nahrávání souboru...", + "select-file-to-upload": "Vyberte soubor pro nahrání!", + "upload-success": "Soubor byl úspěšně nahrán!", "maximum-file-size": "Maximum %1 kb" } \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 35fdc4db9b..24fde19720 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -6,13 +6,14 @@ "postcount": "Počet příspěvků", "email": "Email", "confirm_email": "Potvrdit email", + "account_info": "Account Info", "ban_account": "Zablokovat účet", "ban_account_confirm": "Opravdu chcete zablokovat tohoto uživatele?", "unban_account": "Odblokovat účet", "delete_account": "Vymazat účet", "delete_account_confirm": "Opravdu chcete smazat váš účet? <br /><strong>Tato akce je nevratná a nebude možné obnovit žádné vaše data.</strong><br /><br /> Pro potvrzení smazání účtu napište vaše uživatelské jméno.", - "delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", - "account-deleted": "Account deleted", + "delete_this_account_confirm": "Skutečně chcete zrušit tento účet? <br /><strong>Tato akce je nevratná a již nebude žádná možnost obnovení vašich dat</strong><br /><br />", + "account-deleted": "Účet smazán", "fullname": "Jméno a příjmení", "website": "Webové stránky", "location": "Poloha", @@ -22,7 +23,7 @@ "profile": "Profil", "profile_views": "Zobrazení profilu", "reputation": "Reputace", - "favourites": "Bookmarks", + "favourites": "Záložky", "watched": "Sledován", "followers": "Sledují ho", "following": "Sleduje", @@ -30,17 +31,17 @@ "signature": "Podpis", "birthday": "Datum narození", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Chatovat s %1", "follow": "Sledovat", "unfollow": "Nesledovat", "more": "Více", "profile_update_success": "Profil byl úspěšně aktualizován!", "change_picture": "Změnit obrázek", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Změnit uživatelské jméno", + "change_email": "Změnit email", "edit": "Upravit", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Editovat profil", + "default_picture": "Výchozí ikonka", "uploaded_picture": "Nahraný obrázek", "upload_new_picture": "Nahrát nový obrázek", "upload_new_picture_from_url": "Nahrát nový obrázek z URL", @@ -54,23 +55,23 @@ "change_password_success": "Heslo je aktualizované!", "confirm_password": "Potvrzení hesla", "password": "Heslo", - "username_taken_workaround": "The username you requested was already taken, so we have altered it slightly. You are now known as <strong>%1</strong>", - "password_same_as_username": "Your password is the same as your username, please select another password.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "username_taken_workaround": "Zvolené uživatelské jméno je již zabrané, takže jsme ho trochu upravili. Nyní jste znám jako <strong>%1</strong>", + "password_same_as_username": "Vaše heslo je stejné jako vaše přihlašovací jméno. Zvolte si prosím jiné heslo.", + "password_same_as_email": "Vaše heslo je stejné jako email. Vyberte si prosím jiné heslo.", "upload_picture": "Nahrát obrázek", "upload_a_picture": "Nahrát obrázek", - "remove_uploaded_picture": "Remove Uploaded Picture", - "upload_cover_picture": "Upload cover picture", + "remove_uploaded_picture": "Odstranit nahraný obrázek", + "upload_cover_picture": "Náhrát titulní obrázek", "settings": "Nastavení", "show_email": "Zobrazovat můj email v profilu", "show_fullname": "Zobrazovat celé jméno", - "restrict_chats": "Only allow chat messages from users I follow", - "digest_label": "Subscribe to Digest", + "restrict_chats": "Povolit chatovací zprávy pouze od uživatelů, které sleduji.", + "digest_label": "Odebírat přehled", "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", - "digest_off": "Off", - "digest_daily": "Daily", - "digest_weekly": "Weekly", - "digest_monthly": "Monthly", + "digest_off": "Vypnuto", + "digest_daily": "Denně", + "digest_weekly": "Týdně", + "digest_monthly": "Měsíčně", "send_chat_notifications": "Send an email if a new chat message arrives and I am not online", "send_post_notifications": "Send an email when replies are made to topics I am subscribed to", "settings-require-reload": "Some setting changes require a reload. Click here to reload the page.", @@ -85,25 +86,44 @@ "email_hidden": "Skrytý email", "hidden": "skrytý", "paginate_description": "Paginate topics and posts instead of using infinite scroll", - "topics_per_page": "Topics per Page", - "posts_per_page": "Posts per Page", - "notification_sounds": "Play a sound when you receive a notification", + "topics_per_page": "Témat na stránce", + "posts_per_page": "Příspěvků na stránce", + "notification_sounds": "Přehrát zvuk když dostanete notifikaci", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", - "no-group-title": "No group title", - "select-skin": "Select a Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Nadpis skupiny", + "no-group-title": "Žádný nadpis skupiny", + "select-skin": "Vybrat skin", + "select-homepage": "Vybrat domovskou stránku", + "homepage": "Domovská stránka", "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", "custom_route": "Custom Homepage Route", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/cs/users.json b/public/language/cs/users.json index 6145c20f2a..217e4e3db1 100644 --- a/public/language/cs/users.json +++ b/public/language/cs/users.json @@ -2,19 +2,20 @@ "latest_users": "Nejnovější uživatelé", "top_posters": "Nejaktivnější", "most_reputation": "Nejváženější", + "most_flags": "Most Flags", "search": "Vyhledávat", "enter_username": "Zadej uživatelské jméno k hledání", "load_more": "Načíst další", - "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", - "filter-by": "Filter By", - "online-only": "Online only", - "invite": "Invite", - "invitation-email-sent": "An invitation email has been sent to %1", - "user_list": "User List", - "recent_topics": "Recent Topics", - "popular_topics": "Popular Topics", - "unread_topics": "Unread Topics", - "categories": "Categories", - "tags": "Tags", - "no-users-found": "No users found!" + "users-found-search-took": "Nalezeno %1 uživatel(ů) za %2 vteřiny.", + "filter-by": "Filtrovat dle", + "online-only": "Pouze online", + "invite": "Pozvat", + "invitation-email-sent": "E-mailová pozvánka byla odeslána na adresu %1", + "user_list": "Seznam uživatelů", + "recent_topics": "Poslední témata", + "popular_topics": "Oblíbená témata", + "unread_topics": "Nepřečtená témata", + "categories": "Kategorie", + "tags": "Tagy", + "no-users-found": "Nebyly nalezeny žádní uživatelé!" } \ No newline at end of file diff --git a/public/language/da/category.json b/public/language/da/category.json index 96579ce8a3..e40ba1bc55 100644 --- a/public/language/da/category.json +++ b/public/language/da/category.json @@ -10,7 +10,11 @@ "share_this_category": "Del denne kategori", "watch": "Overvåg", "ignore": "Ignorer", - "watch.message": "Du overvåger nu opdateringer fra denne kategori", - "ignore.message": "Du ignorere nu opdateringer fra denne kategori", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Fulgte kategorier" } \ No newline at end of file diff --git a/public/language/da/email.json b/public/language/da/email.json index 0ed8b77a53..d5591d698d 100644 --- a/public/language/da/email.json +++ b/public/language/da/email.json @@ -24,6 +24,7 @@ "digest.day": "dag", "digest.week": "uge", "digest.month": "måned", + "digest.subject": "Digest for %1", "notif.chat.subject": "Ny chat besked modtaget fra %1", "notif.chat.cta": "Klik her for at forsætte med samtalen", "notif.chat.unsub.info": "Denne chat notifikation blev sendt til dig pga. indstillingerne i dit abonnement.", diff --git a/public/language/da/error.json b/public/language/da/error.json index 148f312c98..eff66f2cd1 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ugyldig Adgangskode", "invalid-username-or-password": "Venligst angiv både brugernavn og adgangskode", "invalid-search-term": "Ugyldig søgeterm", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Ugyldig side værdi, skal mindst være %1 og maks. %2", "username-taken": "Brugernavn optaget", "email-taken": "Emailadresse allerede i brug", "email-not-confirmed": "Din email adresse er ikke blevet bekræftet endnu, venligst klik her for at bekrætige den.", "email-not-confirmed-chat": "Du kan ikke chatte før din email er bekræftet, klik her for at bekræfte din email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Dette forum kræver bekræftelse af din email, klik her for at indtaste en email", "email-confirm-failed": "Vi kunne ikke bekræfte din email, prøv igen senere.", "confirm-email-already-sent": "Bekræftelses email er allerede afsendt, vent venligt %1 minut(ter) for at sende endnu en.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Brugernavn er for kort", "username-too-long": "Brugernavn er for langt", "password-too-long": "Kodeord er for langt", "user-banned": "Bruger er bortvist", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Beklager, du er nødt til at vente %1 sekund(er) før du opretter dit indlæg", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategorien eksisterer ikke", "no-topic": "Tråden eksisterer ikke", "no-post": "Indlægget eksisterer ikke", @@ -38,10 +43,26 @@ "category-disabled": "Kategorien er deaktiveret", "topic-locked": "Tråden er låst", "post-edit-duration-expired": "Du kan kun redigere indlæg i %1 sekund(er) efter indlæg", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Venligst indtast et længere indlæg. Indlægget skal mindst indeholde %1 karakter(er).", "content-too-long": "Venligt indtast et kortere indlæg. Indlæg kan ikke være længere end %1 karakter(er).", "title-too-short": "Venligst indtast en længere titel. Titlen skal mindst indeholde %1 karakter(er).", "title-too-long": "Venligst indtast en kortere titel. Titlen kan ikke indeholde flere end %1 karakter(er).", + "category-not-selected": "Category not selected.", "too-many-posts": "Du kan højest skrive et indlæg hver %1 sekund(er) - venligst vent et øjeblik før næste indlæg", "too-many-posts-newbie": "Som ny bruger kan du kun skrive et indlæg engang hvert %1. sekund() indtil du har optjent %2 omdømme point - venligst vent et øjeblik før næste indlæg.", "tag-too-short": "Indtast et længere tag. Tags skal indeholde mindst %1 karakter(er).", @@ -55,10 +76,12 @@ "already-unfavourited": "Du har allerede fjernet dette indlæg fra bogmærker", "cant-ban-other-admins": "Du kan ikke udlukke andre administatrorer!", "cant-remove-last-admin": "Du er den eneste administrator. Tilføj en anden bruger som administrator før du fjerner dig selv som administrator", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Invalid billed type. De tilladte typer er: %1", "invalid-image-extension": "Forkert billede filnavnsendelse", "invalid-file-type": "Invalid fil type. Tilladte typer er: %1", "group-name-too-short": "Gruppe navn for kort", + "group-name-too-long": "Group name too long", "group-already-exists": "Gruppen eksisterer allerede", "group-name-change-not-allowed": "Ændring af gruppe navn er ikke tilladt", "group-already-member": "Allerede medlem af denne gruppe", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Du har ikke tilladelse til at redigere denne besked", "cant-remove-last-user": "Du kan ikke fjerne den sidste bruger", "cant-delete-chat-message": "Du har ikke tilladelse til at slette denne besked", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Vurderingssystem er slået fra.", "downvoting-disabled": "Nedvurdering er slået fra", "not-enough-reputation-to-downvote": "Du har ikke nok omdømme til at nedstemme dette indlæg", @@ -99,5 +123,9 @@ "no-session-found": "Ingen login session kan findes!", "not-in-room": "Bruger er ikke i rummet", "no-users-in-room": "Ingen brugere i rummet", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/da/global.json b/public/language/da/global.json index 5990eb4da5..c3fb639f3f 100644 --- a/public/language/da/global.json +++ b/public/language/da/global.json @@ -7,8 +7,10 @@ "403.login": "Måske du skulle <a href='%1/login'>prøve og logge ind</a>?", "404.title": "Ikke fundet", "404.message": "Det ser ud til du er stødt på en side der ikke finder. Retuner til <a href='%1/'> forsiden</a>.", - "500.title": "Intern fejl.", + "500.title": "Internal Error.", "500.message": "Ups! Ser ud til at noget gik galt!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Tilmeld", "login": "Log ind", "please_log_in": "Venligst log ind", @@ -17,6 +19,7 @@ "welcome_back": "Velkommen tilbage", "you_have_successfully_logged_in": "Du er nu logget ind", "save_changes": "Gem ændringer", + "save": "Save", "close": "Luk", "pagination": "Sidetal", "pagination.out_of": "%1 ud af %2", @@ -50,7 +53,9 @@ "topics": "Emner", "posts": "Indlæg", "best": "Bedste", + "upvoters": "Upvoters", "upvoted": "Syntes godt om", + "downvoters": "Downvoters", "downvoted": "Syntes ikke godt om", "views": "Visninger", "reputation": "Omdømme", @@ -90,5 +95,8 @@ "enter_page_number": "Indsæt sideantal", "upload_file": "Upload fil", "upload": "Upload", - "allowed-file-types": "Tilladte filtyper er %1" + "allowed-file-types": "Tilladte filtyper er %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/da/groups.json b/public/language/da/groups.json index f02ec79938..94f8ec881e 100644 --- a/public/language/da/groups.json +++ b/public/language/da/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Forlad Gruppe", "membership.reject": "Afvis", "new-group.group_name": "Gruppe Navn:", - "upload-group-cover": "Upload Gruppe coverbillede" + "upload-group-cover": "Upload Gruppe coverbillede", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/da/login.json b/public/language/da/login.json index 1aabf6ec75..2008338218 100644 --- a/public/language/da/login.json +++ b/public/language/da/login.json @@ -5,7 +5,8 @@ "remember_me": "Husk mig?", "forgot_password": "Glemt kodeord?", "alternative_logins": "alternative logins", - "failed_login_attempt": "Login mislykkedes, venligt prøv igen.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Du har successfuldt logged in!", - "dont_have_account": "Har du ikke en konto?" + "dont_have_account": "Har du ikke en konto?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/da/modules.json b/public/language/da/modules.json index 31ea913abe..ce13f1ec67 100644 --- a/public/language/da/modules.json +++ b/public/language/da/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Send og lås", "composer.toggle_dropdown": "Skift mellem dropdown", "composer.uploading": "Uploader %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Annuller", "bootbox.confirm": "Bekræft", diff --git a/public/language/da/notifications.json b/public/language/da/notifications.json index 0e36cb2149..d05a19fa4b 100644 --- a/public/language/da/notifications.json +++ b/public/language/da/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> og %2 andre har syntes godt om dit indlæg i<strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> har flyttet dit indlæg til <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> har flyttet <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> har bogmærket dit indlæg i <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> og <strong>%2</strong> har bogmærket dit indlæg i <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> og %2 andre har bogmærket dit indlæg i <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> har anmeldt et indlæg i <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> og <strong>%2</strong> har anmeldt et indlæg i <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> og %2 andre har anmeldt et indlæg i <strong>%3</strong>", diff --git a/public/language/da/pages.json b/public/language/da/pages.json index cfc7678ce8..6c90eafaa2 100644 --- a/public/language/da/pages.json +++ b/public/language/da/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Brugere med de fleste indlæg", "users/sort-reputation": "Brugere med mest omdømme", "users/banned": "Banlyste Brugere", + "users/most-flags": "Most flagged users", "users/search": "Bruger søgning", "notifications": "Notifikationer", "tags": "Tags", "tag": "Tråde tagget under \"%1\"", "register": "Registre en konto", + "registration-complete": "Registration complete", "login": "Login til din konto", "reset": "Nulstil din adgangskode", "categories": "Kategorier", @@ -28,12 +30,13 @@ "account/edit/password": "Redigerer adgangskode for \"%1\"", "account/edit/username": "Redigerer brugernavn for \"%1\"", "account/edit/email": "Redigerer email for \"%1\"", + "account/info": "Account Info", "account/following": "Personer som %1 følger", "account/followers": "Personer som følger %1", "account/posts": "Indlæg oprettet af %1", "account/topics": "Tråde lavet af %1", "account/groups": "%1s grupper", - "account/favourites": "%1's Bogmærkede Indlæg", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Bruger instillinger", "account/watched": "Tråde fulgt af %1", "account/upvoted": "Indlæg syntes godt om af %1", diff --git a/public/language/da/register.json b/public/language/da/register.json index b1f69c4245..a63dae03aa 100644 --- a/public/language/da/register.json +++ b/public/language/da/register.json @@ -1,5 +1,6 @@ { "register": "Tilmeld", + "cancel_registration": "Cancel Registration", "help.email": "Den email er skjult som standard.", "help.username_restrictions": "Et unikt brugernavn mellem %1 og %2 karakterer. Andre kan nævne dig med @<span id='yourUsername'>brugernavn</span>.", "help.minimum_password_length": "Din adgangskode skal være på mindst %1 karakterer.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternativ registrering", "terms_of_use": "Betingelser for brug", "agree_to_terms_of_use": "Jeg acceptere betingelserne for brug", - "registration-added-to-queue": "Din registrering er blevet tilføjet til godkendelses køen. Du vil mostage en email når du er blevet accepteret af en administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Din registrering er blevet tilføjet til godkendelses køen. Du vil mostage en email når du er blevet accepteret af en administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/da/topic.json b/public/language/da/topic.json index 888384ced3..7d74d3ee19 100644 --- a/public/language/da/topic.json +++ b/public/language/da/topic.json @@ -26,12 +26,30 @@ "tools": "Værktøjer", "flag": "Marker", "locked": "Låst", - "bookmark_instructions": "Klik her for at returnere til det seneste ulæste indlæg i denne tråd.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Meld dette indlæg til moderation", "flag_success": "Dette indlæg er blevet meldt til moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Denne tråd er blevet slettet. Kun brugere med emne behandlings privilegier kan se den.", "following_topic.message": "Du vil nu modtage notifikationer når nogle skriver et indlæg i dette emne.", - "not_following_topic.message": "Du vil ikke længere modtage notifikationer fra dette emne.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Venligt registrer eller login for at abbonere på dette emne.", "markAsUnreadForAll.success": "Emnet er market ulæst for alle.", "mark_unread": "Marker ulæste", @@ -41,8 +59,14 @@ "watch.title": "Bliv notificeret ved nye indlæg i dette emne", "unwatch.title": "Fjern overvågning af dette emne", "share_this_post": "Del dette indlæg", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Emne værktøjer", - "thread_tools.markAsUnreadForAll": "Marker som ulæst", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Fastgør tråd", "thread_tools.unpin": "Frigør tråd", "thread_tools.lock": "Lås tråd", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Denne tråd vil blive flyttet til katagorien", "fork_topic_instruction": "Klik på indlæg du ønsker at fraskille", "fork_no_pids": "Ingen indlæg valgt", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Tråden blev fraskilt! Klik her for at gå til den fraskilte tråd.", "delete_posts_instruction": "Klik på de indlæg du vil slette/rense", "composer.title_placeholder": "Angiv din trådtittel her ...", diff --git a/public/language/da/unread.json b/public/language/da/unread.json index e44b6e42fd..dbb45d6024 100644 --- a/public/language/da/unread.json +++ b/public/language/da/unread.json @@ -6,5 +6,8 @@ "selected": "Valgte", "all": "Alle", "all_categories": "Alle kategorier", - "topics_marked_as_read.success": "Emner markeret som læst!" + "topics_marked_as_read.success": "Emner markeret som læst!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/da/user.json b/public/language/da/user.json index ebc1cba887..6ebeb2e391 100644 --- a/public/language/da/user.json +++ b/public/language/da/user.json @@ -6,6 +6,7 @@ "postcount": "Antal indlæg", "email": "Email", "confirm_email": "Bekræft email", + "account_info": "Account Info", "ban_account": "Ban Konto", "ban_account_confirm": "Ønsker du virkelig at banne denne konto?", "unban_account": "Afban Konto", @@ -88,14 +89,21 @@ "topics_per_page": "Emner per side", "posts_per_page": "Indlæg per side", "notification_sounds": "Afspil en lyd når du modtager en notifikation", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Gennemsenings indstillinger", "open_links_in_new_tab": "Åben udgående link i en ny tab", "enable_topic_searching": "Slå In-Topic søgning til", "topic_search_help": "Hvis slået til, så vil in-topic søgning overskrive browserens almindelige søge function og tillade dig at søge hele emnet, istedet for kun det der er vist på skærmen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Følg emner du har skrevet indlæg i", - "follow_topics_you_create": "Følg emner du opretter", - "grouptitle": "Vælg gruppe titlen du gerne vil fremvise", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "Ingen gruppe titel", "select-skin": "Vælg et skin", "select-homepage": "Vælg en hjemmeside", @@ -105,5 +113,17 @@ "custom_route_help": "Indtast et rute navn her, uden nogle foregående skråstreg (f.eks. \"nyligt\" eller \"populært\")", "sso.title": "Enkeltgangs Sign-on Servicer", "sso.associated": "Forbundet med", - "sso.not-associated": "Klik her for at forbinde med" + "sso.not-associated": "Klik her for at forbinde med", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/da/users.json b/public/language/da/users.json index 71e4aef9f7..ea51cc7398 100644 --- a/public/language/da/users.json +++ b/public/language/da/users.json @@ -2,6 +2,7 @@ "latest_users": "Seneste brugere", "top_posters": "Top Postere", "most_reputation": "Mest Omdømme", + "most_flags": "Most Flags", "search": "Søg", "enter_username": "Indtast brugernavn for at søge", "load_more": "Indlæs mere", diff --git a/public/language/de/category.json b/public/language/de/category.json index 3ca129e561..1f245fae7a 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -2,7 +2,7 @@ "category": "Kategorie", "subcategories": "Unterkategorien", "new_topic_button": "Neues Thema", - "guest-login-post": "Anmelden, um einen Beitrag zu erstellen", + "guest-login-post": "Melde dich an, um einen Beitrag zu erstellen", "no_topics": "<strong>Es gibt noch keine Themen in dieser Kategorie.</strong><br />Warum beginnst du nicht eins?", "browsing": "Aktiv", "no_replies": "Niemand hat geantwortet", @@ -10,7 +10,11 @@ "share_this_category": "Teile diese Kategorie", "watch": "Beobachten", "ignore": "Ignorieren", - "watch.message": "Du beobachtest jetzt Änderungen in dieser Kategorie", - "ignore.message": "Du ignorierst jetzt Änderungen in dieser Kategorie", + "watching": "Beobachte", + "ignoring": "Ignoriere", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Zuletzt angesehen Kategorien" } \ No newline at end of file diff --git a/public/language/de/email.json b/public/language/de/email.json index 459cb40156..7ba24003a7 100644 --- a/public/language/de/email.json +++ b/public/language/de/email.json @@ -24,6 +24,7 @@ "digest.day": "des letzten Tages", "digest.week": "der letzten Woche", "digest.month": "des letzen Monats", + "digest.subject": "Übersicht für %1", "notif.chat.subject": "Neue Chatnachricht von %1 erhalten", "notif.chat.cta": "Klicke hier, um die Unterhaltung fortzusetzen", "notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", diff --git a/public/language/de/error.json b/public/language/de/error.json index 40615e0e2e..0b99880516 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ungültiges Passwort", "invalid-username-or-password": "Bitte gebe einen Benutzernamen und ein Passwort an", "invalid-search-term": "Ungültige Suchanfrage", + "csrf-invalid": "Dein Login war nicht erfolgreich da wahrscheinlich deine Sitzung abgelaufen ist. Bitte versuche es noch einmal", "invalid-pagination-value": "Ungültige Seitennummerierung, muss mindestens %1 und maximal %2 sein", "username-taken": "Der Benutzername ist bereits vergeben", "email-taken": "Die E-Mail-Adresse ist bereits vergeben", "email-not-confirmed": "Deine E-Mail wurde noch nicht bestätigt, bitte klicke hier, um deine E-Mail zu bestätigen.", "email-not-confirmed-chat": "Du kannst denn Chat erst nutzen wenn deine E-Mail bestätigt wurde, bitte klicke hier, um deine E-Mail zu bestätigen.", + "email-not-confirmed-email-sent": "Deine E-Mail wurde noch nicht bestätigt, bitte schaue in deinem Posteingang nach der Bestätigungsmail.", "no-email-to-confirm": "Dieses Forum setzt eine E-Mail-Bestätigung voraus, bitte klicke hier um eine E-Mail-Adresse einzugeben.", "email-confirm-failed": "Wir konnten deine E-Mail-Adresse nicht bestätigen, bitte versuch es später noch einmal", "confirm-email-already-sent": "Die Bestätigungsmail wurde verschickt, bitte warte %1 Minute(n) um eine Weitere zu verschicken.", + "sendmail-not-found": "Sendmail wurde nicht gefunden. Bitte stelle sicher, dass es installiert ist und durch den Benutzer unter dem NodeBB läuft ausgeführt werden kann.", "username-too-short": "Benutzername ist zu kurz", "username-too-long": "Benutzername ist zu lang", "password-too-long": "Passwort ist zu lang", "user-banned": "Benutzer ist gesperrt", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Entschuldigung, du musst %1 Sekunde(n) warten, bevor du deinen ersten Beitrag schreiben kannst.", "blacklisted-ip": "Deine IP-Adresse ist für diese Plattform gesperrt. Sollte dies ein Irrtum sein, dann kontaktiere bitte einen Administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Die Kategorie existiert nicht", "no-topic": "Das Thema existiert nicht", "no-post": "Der Beitrag existiert nicht", @@ -38,10 +43,26 @@ "category-disabled": "Kategorie ist deaktiviert", "topic-locked": "Thema ist gesperrt", "post-edit-duration-expired": "Entschuldigung, du darfst Beiträge nur %1 Sekunde(n) nach dem Veröffentlichen editieren.", + "post-edit-duration-expired-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Minuten/n nach dem Erstellen editieren", + "post-edit-duration-expired-minutes-seconds": "Du darfst Beiträge lediglich innerhalb von %1 Minuten/n und %2 Sekunden nach dem Erstellen editieren", + "post-edit-duration-expired-hours": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n nach dem Erstellen editieren", + "post-edit-duration-expired-hours-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n und %2 Minute/n nach dem Erstellen editieren", + "post-edit-duration-expired-days": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en nach dem Erstellen editieren", + "post-edit-duration-expired-days-hours": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en und %2 Stunde/n nach dem Erstellen editieren", + "post-delete-duration-expired": "Du darfst Beiträge lediglich innerhalb von %1 Sekunden nach dem Erstellen löschen", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n nach dem Erstellen löschen", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Bitte schreibe einen längeren Beitrag. Beiträge sollten mindestens %1 Zeichen enthalten.", "content-too-long": "Bitte schreibe einen kürzeren Beitrag. Beiträge können nicht länger als %1 Zeichen sein.", "title-too-short": "Bitte gebe einen längeren Titel ein. Ein Titel muss mindestens %1 Zeichen enthalten.", "title-too-long": "Bitten gebe einen kürzeren Titel ein. Ein Titel darf nicht mehr als %1 Zeichen enthalten.", + "category-not-selected": "Category not selected.", "too-many-posts": "Du kannst nur einen Beitrag innerhalb von %1 Sekunden erstellen - Bitte warte bevor Du erneut einen Beitrag erstellst.", "too-many-posts-newbie": "Als neuer Benutzer kannst du nur einen Beitrag innerhalb von %1 Sekunden erstellen bis dein Ansehen %2 erreicht hat - Bitte warte bevor du erneut einen Beitrag erstellst.", "tag-too-short": "Bitte gebe ein längeres Schlagwort ein. Tags sollten mindestens %1 Zeichen enthalten.", @@ -55,10 +76,12 @@ "already-unfavourited": "Du hast diesen Beitrag bereits aus deinen Lesezeichen entfernt", "cant-ban-other-admins": "Du kannst andere Administratoren nicht sperren!", "cant-remove-last-admin": "Du bist der einzige Administrator. Füge zuerst einen anderen Administrator hinzu, bevor du dich selbst als Administrator entfernst", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Falsche Bildart. Erlaubte Arten sind: %1", "invalid-image-extension": "Ungültige Dateinamenerweiterung", "invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1", "group-name-too-short": "Gruppenname zu kurz", + "group-name-too-long": "Gruppenname zu lang", "group-already-exists": "Gruppe existiert bereits", "group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern", "group-already-member": "Bereits Teil dieser Gruppe", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Du darfst diese Nachricht nicht ändern", "cant-remove-last-user": "Du kannst den letzten Benutzer nicht entfernen", "cant-delete-chat-message": "Du darfst diese Nachricht nicht löschen", + "already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.", "reputation-system-disabled": "Das Reputationssystem ist deaktiviert.", "downvoting-disabled": "Downvotes sind deaktiviert.", "not-enough-reputation-to-downvote": "Dein Ansehen ist zu niedrig, um diesen Beitrag negativ zu bewerten.", @@ -99,5 +123,9 @@ "no-session-found": "Keine Login-Sitzung gefunden!", "not-in-room": "Benutzer nicht im Raum", "no-users-in-room": "In diesem Raum befinden sich keine Benutzer.", - "cant-kick-self": "Du kannst dich nicht selber aus der Gruppe entfernen." + "cant-kick-self": "Du kannst dich nicht selber aus der Gruppe entfernen.", + "no-users-selected": "Keine Benutzer ausgewählt", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/de/global.json b/public/language/de/global.json index 82179436b4..bbd3695b3f 100644 --- a/public/language/de/global.json +++ b/public/language/de/global.json @@ -7,8 +7,10 @@ "403.login": "Du solltest Dich <a href='%1/login'>anmelden</a>.", "404.title": " Nicht Gefunden", "404.message": "Diese Seite existiert nicht. Zur <a href='%1/'>Homepage</a> zurückkehren.", - "500.title": "Interner Fehler.", + "500.title": "Internal Error.", "500.message": "Ups! Scheint als wäre etwas schief gelaufen!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registrieren", "login": "Anmelden", "please_log_in": "Bitte anmelden", @@ -17,6 +19,7 @@ "welcome_back": "Willkommen zurück", "you_have_successfully_logged_in": "Du hast dich erfolgreich angemeldet", "save_changes": "Speichere Änderungen", + "save": "Save", "close": "Schließen", "pagination": "Seitennummerierung", "pagination.out_of": "%1 von %2", @@ -50,7 +53,9 @@ "topics": "Themen", "posts": "Beiträge", "best": "Bestbewertet", + "upvoters": "Upvoters", "upvoted": "Positiv bewertet", + "downvoters": "Downvoters", "downvoted": "Negativ bewertet", "views": "Aufrufe", "reputation": "Ansehen", @@ -90,5 +95,8 @@ "enter_page_number": "Seitennummer eingeben", "upload_file": "Datei hochladen", "upload": "Hochladen", - "allowed-file-types": "Erlaubte Dateitypen sind %1" + "allowed-file-types": "Erlaubte Dateitypen sind %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/de/groups.json b/public/language/de/groups.json index 5d328ac70d..89c33fee3c 100644 --- a/public/language/de/groups.json +++ b/public/language/de/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Gruppe verlassen", "membership.reject": "Ablehnen", "new-group.group_name": "Gruppenname:", - "upload-group-cover": "Gruppentitelbild hochladen" + "upload-group-cover": "Gruppentitelbild hochladen", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/de/login.json b/public/language/de/login.json index 8551266044..94ad907a1e 100644 --- a/public/language/de/login.json +++ b/public/language/de/login.json @@ -5,7 +5,8 @@ "remember_me": "Eingeloggt bleiben?", "forgot_password": "Passwort vergessen?", "alternative_logins": "Alternative Logins", - "failed_login_attempt": " Anmeldeversuch fehlgeschlagen, versuche es erneut.", + "failed_login_attempt": "Login fehlgeschlagen", "login_successful": "Du hast dich erfolgreich eingeloggt!", - "dont_have_account": "Du hast noch kein Konto?" + "dont_have_account": "Du hast noch kein Konto?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 2210c035fd..d54cea21ab 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Einreichen und Sperren", "composer.toggle_dropdown": "Menu aus-/einblenden", "composer.uploading": "Lade %1 hoch", + "composer.formatting.bold": "Fett", + "composer.formatting.italic": "Kursiv", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Durchstreichen", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Bild", + "composer.upload-picture": "Bild hochladen", + "composer.upload-file": "Datei hochladen", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Abbrechen", "bootbox.confirm": "Bestätigen", diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index 51cb09b719..18cc3b5e77 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> und %2 andere Nutzer haben deinen Beitrag in <strong>%3</strong> positiv bewertet.", "moved_your_post": "<strong>%1</strong> hat deinen Beitrag nach <strong>%2</strong> verschoben.", "moved_your_topic": "<strong>%1</strong> hat <strong>%2</strong> verschoben.", - "favourited_your_post_in": "<strong>%1</strong> hat deinen Beitrag in <strong>%2</strong> als Lesezeichen gespeichert.", - "favourited_your_post_in_dual": "<strong>%1</strong> und <strong>%2</strong> haben deinen Beitrag in <strong>%3</strong> als Lesezeichen gespeichert.", - "favourited_your_post_in_multiple": "<strong>%1</strong> und %2 andere Nutzer haben deinen Beitrag in <strong>%3</strong> als Lesezeichen gespeichert.", "user_flagged_post_in": "<strong>%1</strong> hat einen Beitrag in </strong>%2</strong> gemeldet", "user_flagged_post_in_dual": "<strong>%1</strong> und <strong>%2</strong> haben einen Beitrag in <strong>%3</strong> gemeldet", "user_flagged_post_in_multiple": "<strong>%1</strong> und %2 andere Nutzer haben einen Beitrag in <strong>%3</strong> gemeldet", diff --git a/public/language/de/pages.json b/public/language/de/pages.json index 743f286bd4..6cc5b29272 100644 --- a/public/language/de/pages.json +++ b/public/language/de/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Benutzer mit den meisten Beiträgen", "users/sort-reputation": "Benutzer mit dem höchsten Ansehen", "users/banned": "Gesperrte Benutzer", + "users/most-flags": "Most flagged users", "users/search": "Benutzer Suche", "notifications": "Benachrichtigungen", "tags": "Markierungen", "tag": "Themen markiert unter \"%1\"", "register": "Einen Benutzer erstellen", + "registration-complete": "Registration complete", "login": "Einloggen", "reset": "Passwort zurücksetzen", "categories": "Kategorien", @@ -28,12 +30,13 @@ "account/edit/password": "Bearbeite Passwort von \"%1\"", "account/edit/username": "Bearbeite Benutzernamen von \"%1\"", "account/edit/email": "Bearbeite E-Mail von \"%1\"", + "account/info": "Account Info", "account/following": "Nutzer, denen %1 folgt", "account/followers": "Nutzer, die %1 folgen", "account/posts": "Beiträge von %1", "account/topics": "Von %1 verfasste Themen", "account/groups": "Gruppen von %1", - "account/favourites": "Lesezeichen von %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Benutzer-Einstellungen", "account/watched": "Von %1 beobachtete Themen", "account/upvoted": "Von %1 positiv bewertete Beiträge", diff --git a/public/language/de/register.json b/public/language/de/register.json index 9314539d46..9d1b7ca049 100644 --- a/public/language/de/register.json +++ b/public/language/de/register.json @@ -1,5 +1,6 @@ { "register": "Registrieren", + "cancel_registration": "Registrierungsvorgang abbrechen", "help.email": "Deine E-Mail Adresse ist standardmäßig nicht öffentlich sichtbar.", "help.username_restrictions": "Einen einmaligen Benutzernamen. %1-%2 Zeichen. Andere Benutzer können dich mit @<span id='yourUsername'>Benutzername</span> anschreiben.", "help.minimum_password_length": "Dein Passwort muss mindestens %1 Zeichen lang sein.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternative Registrierung", "terms_of_use": "Nutzungsbedingungen", "agree_to_terms_of_use": "Ich stimme den Nutzungsbedingungen zu", - "registration-added-to-queue": "Deine Registration wurde abgeschickt. Du wirst eine E-Mail erhalten, sobald sie von einem Administrator akzeptiert wird." + "terms_of_use_error": "Du musst den Nutzungsbedingungen zustimmen", + "registration-added-to-queue": "Deine Registration wurde abgeschickt. Du wirst eine E-Mail erhalten, sobald sie von einem Administrator akzeptiert wird.", + "interstitial.intro": "Wir benötigen ein wenig mehr Informationen bevor wir deinen Account erstellen können.", + "interstitial.errors-found": "Wir konnten deinen Registrierungsvorgang nicht abschließen:" } \ No newline at end of file diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 2b34c42911..8da8b5111a 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -26,12 +26,30 @@ "tools": "Werkzeuge", "flag": "Markieren", "locked": "Gesperrt", - "bookmark_instructions": "Klicke hier um zum letzten ungelesenen Beitrag in diesem Thema zu springen.", + "pinned": "Pinned", + "moved": "Verschoben", + "bookmark_instructions": "Klicke hier, um zum letzten gelesenen Beitrag des Themas zurückzukehren.", "flag_title": "Diesen Beitrag zur Moderation markieren", "flag_success": "Dieser Beitrag wurde erfolgreich für die Moderation markiert.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.", "following_topic.message": "Du erhälst nun eine Benachrichtigung, wenn jemand einen Beitrag zu diesem Thema verfasst.", - "not_following_topic.message": "Du erhälst keine weiteren Benachrichtigungen zu diesem Thema mehr.", + "not_following_topic.message": "Ungelesene Beiträge in diesem Thema werden angezeigt, aber du erhältst keine Benachrichtigung wenn jemand einen Beitrag zu diesem Thema verfasst.", + "ignoring_topic.message": "Ungelesene Beiträge in diesem Thema werden nicht mehr angezeigt. Du erhältst eine Benachrichtigung wenn du in diesem Thema erwähnt wirst oder deine Beiträge positiv bewertet werden.", "login_to_subscribe": "Bitte registrieren oder einloggen um dieses Thema zu abonnieren", "markAsUnreadForAll.success": "Thema für Alle als ungelesen markiert.", "mark_unread": "Als ungelesen markieren", @@ -41,8 +59,14 @@ "watch.title": "Bei neuen Antworten benachrichtigen", "unwatch.title": "Dieses Thema nicht mehr beobachten", "share_this_post": "Diesen Beitrag teilen", + "watching": "Beobachtet", + "not-watching": "Nicht beobachtet", + "ignoring": "Ignoriert", + "watching.description": "Benachrichtigung bei neuen Beiträgen.<br/>Ungelesen Beiträge anzeigen.", + "not-watching.description": "Keine Benachrichtigung bei neuen Beiträgen.<br/>Ungelesen Beiträge anzeigen wenn die Kategorie nicht ignoriert wird.", + "ignoring.description": "Keine Benachrichtigung bei neuen Beiträgen.<br/>Ungelesene Beiträge nicht anzeigen.", "thread_tools.title": "Themen-Werkzeuge", - "thread_tools.markAsUnreadForAll": "Als ungelesen markieren", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Thema anheften", "thread_tools.unpin": "Thema nicht mehr anheften", "thread_tools.lock": "Thema schließen", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Dieses Thema wird verschoben nach", "fork_topic_instruction": "Klicke auf die Beiträge, die aufgespaltet werden sollen", "fork_no_pids": "Keine Beiträge ausgewählt!", + "fork_pid_count": "%1 Beiträge ausgewählt", "fork_success": "Thema erfolgreich aufgespalten! Klicke hier, um zum aufgespalteten Thema zu gelangen.", "delete_posts_instruction": "Wähle die zu löschenden Beiträge aus", "composer.title_placeholder": "Hier den Titel des Themas eingeben...", diff --git a/public/language/de/unread.json b/public/language/de/unread.json index b9796b7c41..3c7cabe4ae 100644 --- a/public/language/de/unread.json +++ b/public/language/de/unread.json @@ -6,5 +6,8 @@ "selected": "Ausgewählte", "all": "Alle", "all_categories": "Alle Kategorien", - "topics_marked_as_read.success": "Themen als gelesen markiert!" + "topics_marked_as_read.success": "Themen als gelesen markiert!", + "all-topics": "Alle Themen", + "new-topics": "Neue Themen", + "watched-topics": "Beobachtete Themen" } \ No newline at end of file diff --git a/public/language/de/user.json b/public/language/de/user.json index e4fd8b8f8e..3be8c26959 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -6,6 +6,7 @@ "postcount": "Beiträge", "email": "E-Mail", "confirm_email": "E-Mail bestätigen", + "account_info": "Kontoinformationen", "ban_account": "Konto sperren", "ban_account_confirm": "Bist du sicher, dass du diesen Benutzer sperren möchtest?", "unban_account": "Konto entsperren", @@ -88,14 +89,21 @@ "topics_per_page": "Themen pro Seite", "posts_per_page": "Beiträge pro Seite", "notification_sounds": "Ton abspielen, wenn du eine Benachrichtigung erhältst", + "notifications_and_sounds": "Benachrichtigungen & Klänge", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing", "open_links_in_new_tab": "Ausgehende Links in neuem Tab öffnen", "enable_topic_searching": "Suchen innerhalb von Themen aktivieren", "topic_search_help": "Wenn aktiviert, ersetzt die im-Thema-Suche die Standardsuche des Browsers. Dadurch kannst du im ganzen Thema suchen, nicht nur im sichtbaren Abschnitt.", + "delay_image_loading": "Bilder nachladen", + "image_load_delay_help": "Wenn aktiviert, werden Bilder in Themen erst dann geladen, wenn sie in den sichtbaren Bereich gescrollt werden", "scroll_to_my_post": "Zeige eigene Antwort nach dem Erstellen im Thema an", - "follow_topics_you_reply_to": "Themen folgen, in denen auf dich geantwortet wird", - "follow_topics_you_create": "Themen folgen, die du erstellst", - "grouptitle": "Wähle den anzuzeigenden Gruppen Titel aus", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Gruppentitel", "no-group-title": "Kein Gruppentitel", "select-skin": "Einen Skin auswählen", "select-homepage": "Startseite", @@ -105,5 +113,17 @@ "custom_route_help": "Gib hier einen Pfadnamen ohne vorangehenden Slash ein (z.B. \"recent\" oder \"popular\")", "sso.title": "Single Sign-on Dienste", "sso.associated": "Verbunden mit", - "sso.not-associated": "Verbinde dich mit" + "sso.not-associated": "Verbinde dich mit", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Grund", + "info.banned-no-reason": "Kein Grund angegeben.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/de/users.json b/public/language/de/users.json index 9a25653e0d..4b06e85635 100644 --- a/public/language/de/users.json +++ b/public/language/de/users.json @@ -2,6 +2,7 @@ "latest_users": "Neuste Benutzer", "top_posters": "meiste Beiträge", "most_reputation": "höchstes Ansehen", + "most_flags": "Most Flags", "search": "Suchen", "enter_username": "Benutzer durchsuchen", "load_more": "mehr laden", diff --git a/public/language/el/category.json b/public/language/el/category.json index 261edb2541..33820bce02 100644 --- a/public/language/el/category.json +++ b/public/language/el/category.json @@ -10,7 +10,11 @@ "share_this_category": "Μοιράσου αυτή την κατηγορία", "watch": "Watch", "ignore": "Αγνόηση", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/el/email.json b/public/language/el/email.json index 936eabdd43..3f5adb3021 100644 --- a/public/language/el/email.json +++ b/public/language/el/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Νέο μήνυμα συνομιλίας από τον/την %1", "notif.chat.cta": "Κάνε κλικ εδώ για να πας στην συνομιλία", "notif.chat.unsub.info": "Αυτή η ειδοποίηση για συνομιλία σου στάλθηκε λόγω των ρυθμίσεών σου. ", diff --git a/public/language/el/error.json b/public/language/el/error.json index 388ec06c89..54f4e8700f 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -14,20 +14,25 @@ "invalid-password": "Άκυρος Κωδικός", "invalid-username-or-password": "Παρακαλώ γράψε το όνομα χρήστη και τον κωδικό", "invalid-search-term": "Άκυρος όρος αναζήτησης", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Το όνομα χρήστη είναι πιασμένο", "email-taken": "Το email είναι πιασμένο", "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Το όνομα χρήστη είναι πολύ μικρό", "username-too-long": "Το όνομα χρήστη είναι πολύ μεγάλο", "password-too-long": "Password too long", "user-banned": "Ο Χρήστης είναι αποκλεισμένος/η", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", "no-post": "Post does not exist", @@ -38,10 +43,26 @@ "category-disabled": "Η κατηγορία έχει απενεργοποιηθεί", "topic-locked": "Το θέμα έχει κλειδωθεί", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Δεν μπορείς να αποκλείσεις άλλους διαχειριστές!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-long": "Group name too long", "group-already-exists": "Το όνομα της ομάδας υπάρχει ήδη", "group-name-change-not-allowed": "Αλλαγή του ονόματος της ομάδας δεν επιτρέπεται", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.", "downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί", "not-enough-reputation-to-downvote": "Δεν έχεις αρκετή φήμη για να καταψηφίσεις αυτή την δημοσίευση", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/el/global.json b/public/language/el/global.json index d8be67df72..43b3b73e75 100644 --- a/public/language/el/global.json +++ b/public/language/el/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "Δεν βρέθηκε", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Εσωτερικό σφάλμα.", + "500.title": "Internal Error.", "500.message": "Ουπς! Φαίνεται πως κάτι πήγε στραβά!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Εγγραφή", "login": "Σύνδεση", "please_log_in": "Παρακαλώ Συνδέσου", @@ -17,6 +19,7 @@ "welcome_back": "Καλωσόρισες Πάλι", "you_have_successfully_logged_in": "Συνδέθηκες με επιτυχία", "save_changes": "Αποθήκευση Αλλαγών", + "save": "Save", "close": "Κλείσιμο", "pagination": "Σελιδοποίηση", "pagination.out_of": "%1 από %2", @@ -50,7 +53,9 @@ "topics": "Θέματα", "posts": "Δημοσιεύσεις", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Εμφανίσεις", "reputation": "Φήμη", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/el/groups.json b/public/language/el/groups.json index 29ae716a22..d921ff9ff5 100644 --- a/public/language/el/groups.json +++ b/public/language/el/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/el/login.json b/public/language/el/login.json index 6c10fb3de8..126c06b283 100644 --- a/public/language/el/login.json +++ b/public/language/el/login.json @@ -5,7 +5,8 @@ "remember_me": "Απομνημόνευση;", "forgot_password": "Ξέχασες τον κωδικό σου;", "alternative_logins": "Εναλλακτικά Login", - "failed_login_attempt": "Η προσπάθεια σύνδεσης απέτυχε, παρακαλώ προσπάθησε ξανά.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Συνδέθηκες επιτυχώς!", - "dont_have_account": "Δεν έχεις λογαριασμό;" + "dont_have_account": "Δεν έχεις λογαριασμό;", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/el/modules.json b/public/language/el/modules.json index 4b1dc9e48a..ede8e8cd3c 100644 --- a/public/language/el/modules.json +++ b/public/language/el/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/el/notifications.json b/public/language/el/notifications.json index 53cc1e7fce..00903465f5 100644 --- a/public/language/el/notifications.json +++ b/public/language/el/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "Ο/Η <strong>%1</strong> επεσήμανε μια δημοσίευσή σου στο <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/el/pages.json b/public/language/el/pages.json index 092f813826..0e5de4d880 100644 --- a/public/language/el/pages.json +++ b/public/language/el/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Ειδοποιήσεις", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/el/register.json b/public/language/el/register.json index 875a507d52..9de3dacaaa 100644 --- a/public/language/el/register.json +++ b/public/language/el/register.json @@ -1,5 +1,6 @@ { "register": "Εγγραφή", + "cancel_registration": "Cancel Registration", "help.email": "Από προεπιλογή, το email σου θα είναι κρυμμένο από την κοινή θέα.", "help.username_restrictions": "Ένα μοναδικό όνομα χρήστη μεταξύ %1 και %2 χαρακτήρων. Άλλα άτομα μπορούν να σε αναφέρουν με το @<span id='yourUsername'>username</span> σου.", "help.minimum_password_length": "Το μήκος του κωδικού σου πρέπει να είναι τουλάχιστον %1 χαρακτήρες.", @@ -15,5 +16,8 @@ "alternative_registration": "Εναλλακτική Εγγραφή", "terms_of_use": "Όροι Χρήσης", "agree_to_terms_of_use": "Συμφωνώ με τους Όρους Χρήσης", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/el/topic.json b/public/language/el/topic.json index a2f1d0fcd7..1cee055532 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -26,12 +26,30 @@ "tools": "Εργαλεία", "flag": "Σημαία", "locked": "Κλειδωμένο", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Επισήμανση αυτής της δημοσίευσης για συντονισμό", "flag_success": "Αυτή η δημοσίευση έχει επισημανθεί για συντονισμό.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Το θέμα αυτό έχει διαγραφεί. Μόνο οι χρήστες με δικαιώματα διαχειριστή θεμάτων μπορούν να το δουν.", "following_topic.message": "Θα λαμβάνεις ειδοποιήσεις όποτε κάποιος δημοσιεύει κάτι σε αυτό το θέμα.", - "not_following_topic.message": "Δεν θα λαμβάνεις άλλες ειδοποιήσεις από αυτό το θέμα.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Παρακαλώ εγγράψου ή συνδέσου για για γραφτείς σε αυτό το θέμα.", "markAsUnreadForAll.success": "Το θέμα σημειώθηκε ως μη αναγνωσμένο για όλους.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Να ειδοποιούμαι για νέες απαντήσεις σε αυτό το θέμα", "unwatch.title": "Να μην παρακολουθώ αυτό το θέμα", "share_this_post": "Μοιράσου αυτή την Δημοσίευση", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Εργαλεία Θέματος", - "thread_tools.markAsUnreadForAll": "Σημείωση ως μη αναγνωσμέν", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Καρφίτσωμα Θέματος", "thread_tools.unpin": "Ξεκαρφίτσωμα Θέματος", "thread_tools.lock": "Κλείδωμα Θέματος", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Το θέμα θα μετακινηθεί στην κατηγορία", "fork_topic_instruction": "Κάνε κλικ στις δημοσιεύσεις που θέλεις να διαχωρίσεις", "fork_no_pids": "Δεν έχουν επιλεχθεί δημοσιεύσεις!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Εισαγωγή του τίτλου του θέματος εδώ...", diff --git a/public/language/el/unread.json b/public/language/el/unread.json index 98aea93e27..803596873f 100644 --- a/public/language/el/unread.json +++ b/public/language/el/unread.json @@ -6,5 +6,8 @@ "selected": "Επιλεγμένα", "all": "Όλα", "all_categories": "All categories", - "topics_marked_as_read.success": "Τα θέματα σημειώθηκαν ως αναγνωσμένα!" + "topics_marked_as_read.success": "Τα θέματα σημειώθηκαν ως αναγνωσμένα!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/el/user.json b/public/language/el/user.json index 7b6eed3269..c312841409 100644 --- a/public/language/el/user.json +++ b/public/language/el/user.json @@ -6,6 +6,7 @@ "postcount": "Post Count", "email": "Emai", "confirm_email": "Επιβεβαίωση Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Θέματα ανά σελίδα", "posts_per_page": "Δημοσιεύσεις ανά σελίδα", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Επιλογές Περιήγησης", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/el/users.json b/public/language/el/users.json index 39bba616e6..be5f770f99 100644 --- a/public/language/el/users.json +++ b/public/language/el/users.json @@ -2,6 +2,7 @@ "latest_users": "Πρόσφατοι Χρήστες", "top_posters": "Top Δημοσιεύοντες", "most_reputation": "Υψηλότερη Φήμη", + "most_flags": "Most Flags", "search": "Αναζήτηση", "enter_username": "Γράψε ένα όνομα χρήστη προς αναζήτηση", "load_more": "Φόρτωση περισσότερων", diff --git a/public/language/en@pirate/category.json b/public/language/en@pirate/category.json index 631d322e1a..fa7ffd33e8 100644 --- a/public/language/en@pirate/category.json +++ b/public/language/en@pirate/category.json @@ -7,10 +7,14 @@ "browsing": "browsin'", "no_replies": "No one has replied to ye message", "no_new_posts": "Thar be no new posts.", - "share_this_category": "Share this category", - "watch": "Watch", - "ignore": "Ignore", - "watch.message": "Ye now be watchin' updates from 'tis category", - "ignore.message": "Ye now be ignorin' updates from 'tis category", - "watched-categories": "Watched categories" + "share_this_category": "Share 'tis category", + "watch": "Be watchin'", + "ignore": "Be ignorin'", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Categories ye be watchin'" } \ No newline at end of file diff --git a/public/language/en@pirate/email.json b/public/language/en@pirate/email.json index 1b8d512945..1ffc2ef7a7 100644 --- a/public/language/en@pirate/email.json +++ b/public/language/en@pirate/email.json @@ -1,14 +1,14 @@ { "password-reset-requested": "Password Reset Requested - %1!", - "welcome-to": "Welcome to %1", - "invite": "Invitation from %1", + "welcome-to": "Ahoy thar %1!", + "invite": "Ye be invited by %1", "greeting_no_name": "Hello", "greeting_with_name": "Hello %1", "welcome.text1": "Thank you for registering with %1!", "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.cta": "Click here to confirm your email address", - "invitation.text1": "%1 has invited you to join %2", + "invitation.text1": "%1 be invitin' ye to join %2", "invitation.ctr": "Click here to create your account.", "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", "reset.text2": "To continue with the password reset, please click on the following link:", @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "New chat message received from %1", "notif.chat.cta": "Click here to continue the conversation", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/en@pirate/error.json b/public/language/en@pirate/error.json index 0709e823b6..076a5c0345 100644 --- a/public/language/en@pirate/error.json +++ b/public/language/en@pirate/error.json @@ -14,20 +14,25 @@ "invalid-password": "Invalid Password", "invalid-username-or-password": "Please specify both a username and password", "invalid-search-term": "Invalid search term", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Username taken", "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username too short", "username-too-long": "Username too long", "password-too-long": "Password too long", "user-banned": "User banned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", "no-post": "Post does not exist", @@ -38,10 +43,26 @@ "category-disabled": "Category disabled", "topic-locked": "Topic Locked", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-name-too-long": "Group name too long", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/en@pirate/global.json b/public/language/en@pirate/global.json index 58eba7f42d..13a2d3d7e6 100644 --- a/public/language/en@pirate/global.json +++ b/public/language/en@pirate/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "T'ere be nut'in 'ere", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Broken beam.", + "500.title": "Internal Error.", "500.message": "Looks like we've got somethin' in th' sails.", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Register", "login": "Login", "please_log_in": "Please Log In", @@ -17,6 +19,7 @@ "welcome_back": "Welcome Back", "you_have_successfully_logged_in": "Ye have successfully logged in", "save_changes": "Save yer Changes", + "save": "Save", "close": "Shoot down", "pagination": "Pagination", "pagination.out_of": "%1 out of %2", @@ -50,7 +53,9 @@ "topics": "Topics", "posts": "Messages", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Views", "reputation": "Reputation", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/en@pirate/groups.json b/public/language/en@pirate/groups.json index 3c4f6ce638..8af283d60e 100644 --- a/public/language/en@pirate/groups.json +++ b/public/language/en@pirate/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/en@pirate/login.json b/public/language/en@pirate/login.json index 6e7b7a36c0..5096672f97 100644 --- a/public/language/en@pirate/login.json +++ b/public/language/en@pirate/login.json @@ -5,7 +5,8 @@ "remember_me": "Remember Me?", "forgot_password": "My mind be a scatt'rbrain, help a matey out!", "alternative_logins": "Oth'r gangplanks", - "failed_login_attempt": "Failed login attempt, please give it a go' again.", + "failed_login_attempt": "Ye be refused boardin'", "login_successful": "Welcome on board, matey!", - "dont_have_account": "Don't have an account?" + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/en@pirate/modules.json b/public/language/en@pirate/modules.json index 0ae551df59..ec634a0258 100644 --- a/public/language/en@pirate/modules.json +++ b/public/language/en@pirate/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/en@pirate/notifications.json b/public/language/en@pirate/notifications.json index cee3aa994b..c6dedd3eb0 100644 --- a/public/language/en@pirate/notifications.json +++ b/public/language/en@pirate/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/en@pirate/pages.json b/public/language/en@pirate/pages.json index 285aa30017..e906ebeca1 100644 --- a/public/language/en@pirate/pages.json +++ b/public/language/en@pirate/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notifications", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/en@pirate/register.json b/public/language/en@pirate/register.json index 381aaef8b5..45217b265c 100644 --- a/public/language/en@pirate/register.json +++ b/public/language/en@pirate/register.json @@ -1,5 +1,6 @@ { "register": "Board the ship", + "cancel_registration": "Cancel Registration", "help.email": "By default, your email will be hidden from the public.", "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @<span id='yourUsername'>username</span>.", "help.minimum_password_length": "Your password's length must be at least %1 characters.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternative Registration", "terms_of_use": "Terms of Use", "agree_to_terms_of_use": "I agree to the Terms of Use", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/en@pirate/topic.json b/public/language/en@pirate/topic.json index c7ce76e07b..a6bf44624a 100644 --- a/public/language/en@pirate/topic.json +++ b/public/language/en@pirate/topic.json @@ -26,12 +26,30 @@ "tools": "Tools", "flag": "Flag", "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", "markAsUnreadForAll.success": "Topic marked as unread for all.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Mark Unread", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin Topic", "thread_tools.unpin": "Unpin Topic", "thread_tools.lock": "Lock Topic", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "This topic will be moved to the category", "fork_topic_instruction": "Click the posts you want to fork", "fork_no_pids": "No posts selected!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Enter your topic title here...", diff --git a/public/language/en@pirate/unread.json b/public/language/en@pirate/unread.json index 994ee938f1..37133f87ef 100644 --- a/public/language/en@pirate/unread.json +++ b/public/language/en@pirate/unread.json @@ -6,5 +6,8 @@ "selected": "Selected", "all": "All", "all_categories": "All categories", - "topics_marked_as_read.success": "Topics marked as read!" + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/en@pirate/uploads.json b/public/language/en@pirate/uploads.json index 1622cb5693..f124670d4e 100644 --- a/public/language/en@pirate/uploads.json +++ b/public/language/en@pirate/uploads.json @@ -1,6 +1,6 @@ { "uploading-file": "Uploading the file...", "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", + "upload-success": "Ye file be uploaded!", "maximum-file-size": "Maximum %1 kb" } \ No newline at end of file diff --git a/public/language/en@pirate/user.json b/public/language/en@pirate/user.json index 0b5959462f..37d68aa70c 100644 --- a/public/language/en@pirate/user.json +++ b/public/language/en@pirate/user.json @@ -6,6 +6,7 @@ "postcount": "Post Count", "email": "Email", "confirm_email": "Confirm Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Topics per Page", "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/en@pirate/users.json b/public/language/en@pirate/users.json index 738f85d740..d1213c8f1f 100644 --- a/public/language/en@pirate/users.json +++ b/public/language/en@pirate/users.json @@ -2,6 +2,7 @@ "latest_users": "Land lubbers", "top_posters": "Top mateys", "most_reputation": "Most Reputation", + "most_flags": "Most Flags", "search": "Search", "enter_username": "Gimme y'er handle", "load_more": "Load More", diff --git a/public/language/en_GB/category.json b/public/language/en_GB/category.json index f8b02f32f8..d581c8277d 100644 --- a/public/language/en_GB/category.json +++ b/public/language/en_GB/category.json @@ -13,9 +13,13 @@ "share_this_category": "Share this category", "watch": "Watch", "ignore": "Ignore", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 65e2c584df..5a0cc50e52 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -17,6 +17,7 @@ "invalid-password": "Invalid Password", "invalid-username-or-password": "Please specify both a username and password", "invalid-search-term": "Invalid search term", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", @@ -24,17 +25,21 @@ "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username too short", "username-too-long": "Username too long", "password-too-long": "Password too long", "user-banned": "User banned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", @@ -48,12 +53,30 @@ "topic-locked": "Topic Locked", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", "invalid-title": "Invalid title!", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -65,17 +88,19 @@ "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-name-too-long": "Group name too long", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "Already part of this group", @@ -128,5 +153,10 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } diff --git a/public/language/en_GB/global.json b/public/language/en_GB/global.json index a7959aa349..69dddaf752 100644 --- a/public/language/en_GB/global.json +++ b/public/language/en_GB/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "Not Found", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Internal error.", + "500.title": "Internal Error.", "500.message": "Oops! Looks like something went wrong!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Register", "login": "Login", @@ -21,6 +23,7 @@ "you_have_successfully_logged_in": "You have successfully logged in", "save_changes": "Save Changes", + "save": "Save", "close": "Close", "pagination": "Pagination", @@ -63,7 +66,9 @@ "topics": "Topics", "posts": "Posts", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Views", "reputation": "Reputation", @@ -89,6 +94,8 @@ "recentposts": "Recent Posts", "recentips": "Recently Logged In IPs", + "moderator_tools": "Moderator Tools", + "online": "Online", "away": "Away", "dnd": "Do not disturb", @@ -114,5 +121,9 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index 8d129fe376..2efc9a69fc 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -59,5 +59,7 @@ "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/en_GB/login.json b/public/language/en_GB/login.json index 02abac6371..b03ee269cf 100644 --- a/public/language/en_GB/login.json +++ b/public/language/en_GB/login.json @@ -5,7 +5,8 @@ "remember_me": "Remember Me?", "forgot_password": "Forgot Password?", "alternative_logins": "Alternative Logins", - "failed_login_attempt": "Failed login attempt, please try again.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "You have successfully logged in!", - "dont_have_account": "Don't have an account?" + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json index a3db35f6e3..556ef2fbf4 100644 --- a/public/language/en_GB/modules.json +++ b/public/language/en_GB/modules.json @@ -18,7 +18,6 @@ "chat.thirty_days": "30 Days", "chat.three_months": "3 Months", "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", "chat.add-users-to-room": "Add users to room", "composer.compose": "Compose", @@ -30,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 07ec757374..5a2ed58908 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -18,9 +18,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/en_GB/pages.json b/public/language/en_GB/pages.json index bbdf3cdc47..801b28edea 100644 --- a/public/language/en_GB/pages.json +++ b/public/language/en_GB/pages.json @@ -7,18 +7,21 @@ "popular-alltime": "All time popular topics", "recent": "Recent Topics", "flagged-posts": "Flagged Posts", + "ip-blacklist": "IP Blacklist", "users/online": "Online Users", "users/latest": "Latest Users", "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notifications", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -33,12 +36,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/en_GB/register.json b/public/language/en_GB/register.json index dcbd4bb03a..81b20421d4 100644 --- a/public/language/en_GB/register.json +++ b/public/language/en_GB/register.json @@ -1,5 +1,6 @@ { "register": "Register", + "cancel_registration": "Cancel Registration", "help.email": "By default, your email will be hidden from the public.", "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @<span id='yourUsername'>username</span>.", "help.minimum_password_length": "Your password's length must be at least %1 characters.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternative Registration", "terms_of_use": "Terms of Use", "agree_to_terms_of_use": "I agree to the Terms of Use", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/en_GB/search.json b/public/language/en_GB/search.json index 304134918b..fde9db35f6 100644 --- a/public/language/en_GB/search.json +++ b/public/language/en_GB/search.json @@ -23,7 +23,6 @@ "six-months": "Six months", "one-year": "One year", "sort-by": "Sort by", - "post-time": "Post time", "last-reply-time": "Last reply time", "topic-title": "Topic title", "number-of-replies": "Number of replies", diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 6299b8aac1..af49c4763e 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -29,15 +29,34 @@ "tools": "Tools", "flag": "Flag", "locked": "Locked", + "pinned": "Pinned", + "moved": "Moved", "bookmark_instructions" : "Click here to return to the last read post in this thread.", "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", @@ -50,9 +69,15 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Mark Unread", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin Topic", "thread_tools.unpin": "Unpin Topic", "thread_tools.lock": "Lock Topic", @@ -79,9 +104,9 @@ "confirm_move": "Move", "confirm_fork": "Fork", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", "loading_more_posts": "Loading More Posts", "move_topic": "Move Topic", @@ -92,6 +117,7 @@ "topic_will_be_moved_to": "This topic will be moved to the category", "fork_topic_instruction": "Click the posts you want to fork", "fork_no_pids": "No posts selected!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", @@ -125,7 +151,7 @@ "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", "stale.create": "Create a new topic", "stale.reply_anyway": "Reply to this topic anyway", - + "link_back": "Re: [%1](%2)\n\n", "spam": "Spam", diff --git a/public/language/en_GB/unread.json b/public/language/en_GB/unread.json index 0af9a2cdf6..4a449a53f5 100644 --- a/public/language/en_GB/unread.json +++ b/public/language/en_GB/unread.json @@ -6,5 +6,8 @@ "selected": "Selected", "all": "All", "all_categories": "All categories", - "topics_marked_as_read.success": "Topics marked as read!" + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/en_GB/user.json b/public/language/en_GB/user.json index 07c85aa19f..fd6f11e250 100644 --- a/public/language/en_GB/user.json +++ b/public/language/en_GB/user.json @@ -7,6 +7,7 @@ "email": "Email", "confirm_email": "Confirm Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -24,7 +25,7 @@ "profile": "Profile", "profile_views": "Profile views", "reputation": "Reputation", - "favourites":"Bookmarks", + "bookmarks":"Bookmarks", "watched": "Watched", "followers": "Followers", "following": "Following", @@ -97,6 +98,11 @@ "posts_per_page": "Posts per Page", "notification_sounds" : "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", @@ -104,12 +110,15 @@ "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", + "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", - "grouptitle": "Select the group title you would like to display", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", @@ -122,5 +131,18 @@ "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } diff --git a/public/language/en_GB/users.json b/public/language/en_GB/users.json index e693bf6333..5c9d8b93a4 100644 --- a/public/language/en_GB/users.json +++ b/public/language/en_GB/users.json @@ -2,6 +2,7 @@ "latest_users": "Latest Users", "top_posters": "Top Posters", "most_reputation": "Most Reputation", + "most_flags": "Most Flags", "search": "Search", "enter_username": "Enter a username to search", "load_more": "Load More", diff --git a/public/language/en_US/category.json b/public/language/en_US/category.json index 81ef13b626..ec91137d81 100644 --- a/public/language/en_US/category.json +++ b/public/language/en_US/category.json @@ -10,7 +10,11 @@ "share_this_category": "Share this category", "watch": "Watch", "ignore": "Ignore", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/en_US/email.json b/public/language/en_US/email.json index 1b8d512945..691e6309a2 100644 --- a/public/language/en_US/email.json +++ b/public/language/en_US/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "New chat message received from %1", "notif.chat.cta": "Click here to continue the conversation", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/en_US/error.json b/public/language/en_US/error.json index 0709e823b6..076a5c0345 100644 --- a/public/language/en_US/error.json +++ b/public/language/en_US/error.json @@ -14,20 +14,25 @@ "invalid-password": "Invalid Password", "invalid-username-or-password": "Please specify both a username and password", "invalid-search-term": "Invalid search term", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Username taken", "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username too short", "username-too-long": "Username too long", "password-too-long": "Password too long", "user-banned": "User banned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", "no-post": "Post does not exist", @@ -38,10 +43,26 @@ "category-disabled": "Category disabled", "topic-locked": "Topic Locked", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-name-too-long": "Group name too long", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/en_US/global.json b/public/language/en_US/global.json index a206afd7cf..9b01509159 100644 --- a/public/language/en_US/global.json +++ b/public/language/en_US/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "Not Found", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Internal error.", + "500.title": "Internal Error.", "500.message": "Oops! Looks like something went wrong!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Register", "login": "Login", "please_log_in": "Please Log In", @@ -17,6 +19,7 @@ "welcome_back": "Welcome Back", "you_have_successfully_logged_in": "You have successfully logged in", "save_changes": "Save Changes", + "save": "Save", "close": "Close", "pagination": "Pagination", "pagination.out_of": "%1 out of %2", @@ -50,7 +53,9 @@ "topics": "Topics", "posts": "Posts", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Views", "reputation": "Reputation", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/en_US/groups.json b/public/language/en_US/groups.json index ffc0224c5e..593eb34a4b 100644 --- a/public/language/en_US/groups.json +++ b/public/language/en_US/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/en_US/login.json b/public/language/en_US/login.json index 70b7b98dd5..a09f4ea897 100644 --- a/public/language/en_US/login.json +++ b/public/language/en_US/login.json @@ -5,7 +5,8 @@ "remember_me": "Remember Me?", "forgot_password": "Forgot Password?", "alternative_logins": "Alternative Logins", - "failed_login_attempt": "Failed login attempt, please try again.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "You have successfully logged in!", - "dont_have_account": "Don't have an account?" + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/en_US/modules.json b/public/language/en_US/modules.json index eb5e513640..d00b009403 100644 --- a/public/language/en_US/modules.json +++ b/public/language/en_US/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/en_US/notifications.json b/public/language/en_US/notifications.json index 28d35e65fa..580e697548 100644 --- a/public/language/en_US/notifications.json +++ b/public/language/en_US/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/en_US/pages.json b/public/language/en_US/pages.json index 285aa30017..e906ebeca1 100644 --- a/public/language/en_US/pages.json +++ b/public/language/en_US/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notifications", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/en_US/register.json b/public/language/en_US/register.json index 72cd580910..e58d572e5c 100644 --- a/public/language/en_US/register.json +++ b/public/language/en_US/register.json @@ -1,5 +1,6 @@ { "register": "Register", + "cancel_registration": "Cancel Registration", "help.email": "By default, your email will be hidden from the public.", "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @<span id='yourUsername'>username</span>.", "help.minimum_password_length": "Your password's length must be at least %1 characters.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternative Registration", "terms_of_use": "Terms of Use", "agree_to_terms_of_use": "I agree to the Terms of Use", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/en_US/topic.json b/public/language/en_US/topic.json index bdf6d77f91..a6bf44624a 100644 --- a/public/language/en_US/topic.json +++ b/public/language/en_US/topic.json @@ -26,12 +26,30 @@ "tools": "Tools", "flag": "Flag", "locked": "Locked", + "pinned": "Pinned", + "moved": "Moved", "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", "markAsUnreadForAll.success": "Topic marked as unread for all.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Mark Unread", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin Topic", "thread_tools.unpin": "Unpin Topic", "thread_tools.lock": "Lock Topic", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "This topic will be moved to the category", "fork_topic_instruction": "Click the posts you want to fork", "fork_no_pids": "No posts selected!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Enter your topic title here...", diff --git a/public/language/en_US/unread.json b/public/language/en_US/unread.json index d723f6e8c9..c3050ba93e 100644 --- a/public/language/en_US/unread.json +++ b/public/language/en_US/unread.json @@ -6,5 +6,8 @@ "selected": "Selected", "all": "All", "all_categories": "All categories", - "topics_marked_as_read.success": "Topics marked as read!" + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/en_US/user.json b/public/language/en_US/user.json index 86d31e818d..ce8c78f283 100644 --- a/public/language/en_US/user.json +++ b/public/language/en_US/user.json @@ -6,6 +6,7 @@ "postcount": "Post Count", "email": "Email", "confirm_email": "Confirm Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Topics per Page", "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behavior and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/en_US/users.json b/public/language/en_US/users.json index ae39f26f66..4d0bd5c53e 100644 --- a/public/language/en_US/users.json +++ b/public/language/en_US/users.json @@ -2,6 +2,7 @@ "latest_users": "Latest Users", "top_posters": "Top Posters", "most_reputation": "Most Reputation", + "most_flags": "Most Flags", "search": "Search", "enter_username": "Enter a username to search", "load_more": "Load More", diff --git a/public/language/es/category.json b/public/language/es/category.json index 5b0b5aee30..c1d8fb3d7c 100644 --- a/public/language/es/category.json +++ b/public/language/es/category.json @@ -2,7 +2,7 @@ "category": "Categoría", "subcategories": "Subcategorías", "new_topic_button": "Nuevo tema", - "guest-login-post": "Acceder para poder escribir un mensaje", + "guest-login-post": "Accede para escribir un mensaje", "no_topics": "<strong>No hay temas en esta categoría.</strong><br />¿Por qué no te animas y publicas uno?", "browsing": "viendo ahora", "no_replies": "Nadie ha respondido aún", @@ -10,7 +10,11 @@ "share_this_category": "Compartir esta categoría", "watch": "Seguir", "ignore": "Ignorar", - "watch.message": "Ahora estás viendo actualizaciones de esta categoría", - "ignore.message": "Ahora estás ignorando las actualizaciones de esta categoría", + "watching": "Siguiendo", + "ignoring": "Ignorando", + "watching.description": "Mostrar temas en no leidos", + "ignoring.description": "No mostrar temas en no leidos", + "watch.message": "Ahora puede ver la actualizaciones de esta categoría y todas las subcategorías", + "ignore.message": "Dejar de ver las actualizaciones de esta categoría y todas las subcategorías", "watched-categories": "Categorías seguidas" } \ No newline at end of file diff --git a/public/language/es/email.json b/public/language/es/email.json index 04c27bd68f..6bb5dd06f8 100644 --- a/public/language/es/email.json +++ b/public/language/es/email.json @@ -24,6 +24,7 @@ "digest.day": "día", "digest.week": "semana", "digest.month": "mes", + "digest.subject": "Resumen de %1", "notif.chat.subject": "Nuevo mensaje de chat recibido de %1", "notif.chat.cta": "Haz click aquí para continuar la conversación", "notif.chat.unsub.info": "Esta notificación de chat se te envió debido a tus ajustes de suscripción.", diff --git a/public/language/es/error.json b/public/language/es/error.json index 630475a16f..569a35f539 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -14,20 +14,25 @@ "invalid-password": "Contraseña no válida", "invalid-username-or-password": "Por favor especifica tanto un usuario como contraseña", "invalid-search-term": "Término de búsqueda inválido", + "csrf-invalid": "El acceso ha fallado porque tu sesión ha expirado. Por favor prueba otra vez.", "invalid-pagination-value": "Número de página inválido, debe estar entre %1 y %2", "username-taken": "Nombre de usuario ocupado", "email-taken": "Correo electrónico ocupado", "email-not-confirmed": "Su cuenta de correo electrónico no ha sido confirmada aún, por favor haga click aquí para confirmarla.", "email-not-confirmed-chat": "No puedes usar el chat hasta que confirmes tu dirección de correo electrónico, por favor haz click aquí para confirmar tu correo.", + "email-not-confirmed-email-sent": "Tu correo electrónico está sin confirmar, por favor busca el correo de confirmación en tu bandeja de entrada.", "no-email-to-confirm": "Este foro requiere confirmación de su email, por favor pulse aquí para introducir un email", "email-confirm-failed": "No se ha podido confirmar su email, por favor inténtelo de nuevo más tarde.", "confirm-email-already-sent": "El email de confirmación ya ha sido enviado, por favor espera %1 minuto(s) para enviar otro.", + "sendmail-not-found": "El ejecutable \"sendmail\" no ha sido encontrado, por favor asegúrate de que esta instalado en tu sistema y es accesible por el usuario que ejecuta NodeBB. ", "username-too-short": "Nombre de usuario es demasiado corto", "username-too-long": "Nombre de usuario demasiado largo", "password-too-long": "Contraseña muy corta", "user-banned": "Usuario baneado", + "user-banned-reason": "Usuario Baneado (Reason: %1)", "user-too-new": "Lo sentimos, es necesario que esperes %1 segundo(s) antes poder hacer tu primera publicación", "blacklisted-ip": "Lo sentimos, tu dirección IP ha sido baneada de esta comunidad. Si crees que debe de haber un error, por favor contacte con un administrador.", + "ban-expiry-missing": "Por favor pon una fecha de fin del ban", "no-category": "La categoría no existe", "no-topic": "El tema no existe", "no-post": "La publicación no existe", @@ -37,11 +42,27 @@ "no-privileges": "No tienes suficientes privilegios para realizar esta acción.", "category-disabled": "Categoría deshabilitada", "topic-locked": "Tema bloqueado", - "post-edit-duration-expired": "Sólo puedes editar mensajes durante %1 segundo(s) después de haberlo escrito", + "post-edit-duration-expired": "No puedes editar mensajes hasta pasado %1 segundo(s) después de haberlo escrito", + "post-edit-duration-expired-minutes": "No puedes editar mensajes hasta pasado %1 minuto(s) después de haberlo escrito", + "post-edit-duration-expired-minutes-seconds": "No puedes editar mensajes hasta pasado %1 minuto(s) y %2 segundo(s) después de haberlo escrito", + "post-edit-duration-expired-hours": "No puedes editar mensajes hasta pasado %1 hora(s) después de haberlo escrito", + "post-edit-duration-expired-hours-minutes": "No puedes editar mensajes hasta pasado %1 hora(s) y %2 minuto(s) después de haberlo escrito", + "post-edit-duration-expired-days": "No puedes editar mensajes hasta pasado %1 día(s) después de haberlo escrito", + "post-edit-duration-expired-days-hours": "No puedes editar mensajes hasta pasado %1 día(s) y %2 hora(s) después de haberlo escrito", + "post-delete-duration-expired": "No puedes borrar mensajes tras pasar %1 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-minutes": "No puedes borrar mensajes tras pasar %1 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-minutes-seconds": "No puedes borrar mensajes tras pasar %1 minuto(s) y %2 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-hours": "No puedes borrar mensajes tras pasar %1 hora(s) después de haberlo escrito", + "post-delete-duration-expired-hours-minutes": "No puedes borrar mensajes tras pasar %1 hora(s) y %2 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-days": "No puedes borrar mensajes tras pasar %1 día(s) después de haberlo escrito", + "post-delete-duration-expired-days-hours": "No puedes borrar mensajes tras pasar %1 día(s) y %2 hora(s) después de haberlo escrito", + "cant-delete-topic-has-reply": "No puedes borrar tu tema después de que tenga respuestas", + "cant-delete-topic-has-replies": "No puedes borrar tu tema despues de que tenga ℅1 respuestas", "content-too-short": "Por favor introduzca una publicación más larga. Las publicaciones deben contener al menos %1 caractere(s).", "content-too-long": "Por favor introduzca un mensaje más corto. Los mensajes no pueden exceder los %1 caractere(s).", "title-too-short": "Por favor introduzca un título más largo. Los títulos deben contener al menos %1 caractere(s).", "title-too-long": "Por favor, introduce un título más corto, que no sobrepase los %1 caractere(s).", + "category-not-selected": "Categoría no seleccionada.", "too-many-posts": "Solo puedes publicar una vez cada %1 segundo(s) - por favor espere antes de volver a publicar", "too-many-posts-newbie": "Como nuevo usuario, solo puedes publicar una vez cada %1 segundo(s) hasta hayas ganado una reputación de %2 - por favor espera antes de volver a publicar", "tag-too-short": "Por favor introduce una etiqueta más larga. Las etiquetas deben contener por lo menos %1 caractere(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "Ya habías desguardado este post.", "cant-ban-other-admins": "¡No puedes expulsar a otros administradores!", "cant-remove-last-admin": "Tu eres el unico administrador. Añade otro usuario como administrador antes de eliminarte a ti mismo.", + "cant-delete-admin": "Quitar privilegios de administrador de ésta cuenta antes de intentar borrarla", "invalid-image-type": "Tipo de imagen inválido. Los tipos permitidos son: %1", "invalid-image-extension": "Extensión de imagen inválida", "invalid-file-type": "Tipo de fichero inválido. Los tipos permitidos son: %1", "group-name-too-short": "Nombre del grupo es demasiado corto.", + "group-name-too-long": "Nombre de grupo demasiado largo", "group-already-exists": "El grupo ya existe.", "group-name-change-not-allowed": "El nombre del grupo deseado no está permitido.", "group-already-member": "Ya eres miembro de este grupo", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "No tienes permiso para editar este mensaje", "cant-remove-last-user": "No puedes eliminar el último usuario", "cant-delete-chat-message": "No tienes permiso para eliminar este mensaje", + "already-voting-for-this-post": "Ya has votado a este mensaje.", "reputation-system-disabled": "El sistema de reputación está deshabilitado.", "downvoting-disabled": "La votación negativa está deshabilitada.", "not-enough-reputation-to-downvote": "No tienes suficiente reputación para votar negativo este post", @@ -99,5 +123,9 @@ "no-session-found": "¡No se ha encontrado ningún inicio de sesión!", "not-in-room": "El usuario no está en la sala", "no-users-in-room": "No hay usuarios en esta sala", - "cant-kick-self": "No te puedes expulsar a ti mismo del grupo" + "cant-kick-self": "No te puedes expulsar a ti mismo del grupo", + "no-users-selected": "Ningun usuario(s) seleccionado", + "invalid-home-page-route": "Ruta de página de inicio invalida", + "invalid-session": "No concuerdan los datos de sesión", + "invalid-session-text": "Parece que su sesión ha expirado o no concuerda con el servidor. Por favor vuelva a cargar la página." } \ No newline at end of file diff --git a/public/language/es/global.json b/public/language/es/global.json index d81c10ca88..da2a21b1fa 100644 --- a/public/language/es/global.json +++ b/public/language/es/global.json @@ -7,8 +7,10 @@ "403.login": "¿Quizás deberías <a href='%1/login'> intentar acceder</a>?", "404.title": "No encontrado", "404.message": "Al parecer has llegado a una página a la cual no tienes permisos para acceder. Volver a la <a href='%1/'> página de inicio </a>.", - "500.title": "Error Interno.", + "500.title": "Error interno.", "500.message": "¡Ooops! ¡Parece que algo salió mal! No te preocupes, ¡nuestros simios hiperinteligentes lo solucionarán!", + "400.title": "Petición incorrecta.", + "400.message": "Parece que la dirección es errónea, por favor compruébala y prueba otra vez. En caso contrario vuelve al <a href='%1/'>inicio</a>.", "register": "Registrarse", "login": "Conectarse", "please_log_in": "Por favor, identifíquese.", @@ -17,6 +19,7 @@ "welcome_back": "¡Bienvenido de nuevo!", "you_have_successfully_logged_in": "Identificado satisfactoriamente", "save_changes": "Guardar cambios", + "save": "Guardar", "close": "Cerrar", "pagination": "Paginación", "pagination.out_of": "%1 de %2", @@ -36,7 +39,7 @@ "header.navigation": "Navegación", "notifications.loading": "Cargando notificaciones", "chats.loading": "Cargando chats", - "motd.welcome": "Bienvenido a NodeBB, la plataforma de debate del el futuro.", + "motd.welcome": "Bienvenido a NodeBB, la plataforma de debate del futuro.", "previouspage": "Página anterior", "nextpage": "Página siguiente", "alert.success": "¡Éxito!", @@ -48,9 +51,11 @@ "online": "Conectado", "users": "Usuarios", "topics": "Temas", - "posts": "Posts", - "best": "Mejor", + "posts": "Mensajes", + "best": "Mejor valorados", + "upvoters": "Positivos", "upvoted": "Votado positivamente", + "downvoters": "Negativos", "downvoted": "Votado negativamente", "views": "Visitas", "reputation": "Reputación", @@ -59,7 +64,7 @@ "posted_ago_by_guest": "publicado %1 por Invitado", "posted_ago_by": "publicado %1 por %2", "posted_ago": "publicado %1", - "posted_in": "Publicado %1", + "posted_in": "publicado en %1", "posted_in_by": "publicado en %1 por %2", "posted_in_ago": "publicado en %1 %2", "posted_in_ago_by": "publicado en %1 %2 por %3", @@ -90,5 +95,8 @@ "enter_page_number": "Escribe el número de página", "upload_file": "Subir archivo", "upload": "Subir", - "allowed-file-types": "Los tipos de archivos permitidos son: %1" + "allowed-file-types": "Los tipos de archivos permitidos son: %1", + "unsaved-changes": "Tienes cambios sin guardar. Seguro que quieres salir?", + "reconnecting-message": "Has perdido la conexión. Reconectando a %1.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/es/groups.json b/public/language/es/groups.json index 657fe67599..415f9e1ed8 100644 --- a/public/language/es/groups.json +++ b/public/language/es/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Dejar el grupo", "membership.reject": "Rechazar", "new-group.group_name": "Nombre de Grupo:", - "upload-group-cover": "Cargar foto para el grupo" + "upload-group-cover": "Cargar foto para el grupo", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/es/login.json b/public/language/es/login.json index 448ce31f43..0413471640 100644 --- a/public/language/es/login.json +++ b/public/language/es/login.json @@ -4,8 +4,9 @@ "email": "Correo Electrónico", "remember_me": "¿Recordarme?", "forgot_password": "¿Olvidaste tu contraseña?", - "alternative_logins": "Métodos alternativos", - "failed_login_attempt": "Error al iniciar sesión, inténtalo otra vez.", + "alternative_logins": "Accesos alternativos", + "failed_login_attempt": "Error al iniciar sesión", "login_successful": "¡Identificado satisfactoriamente!", - "dont_have_account": "¿Aún no tienes cuenta?" + "dont_have_account": "¿Aún no tienes cuenta?", + "logged-out-due-to-inactivity": "Debido a la inactividad has sido deslogueado del Panel de Control de Administradores" } \ No newline at end of file diff --git a/public/language/es/modules.json b/public/language/es/modules.json index f0e34b04a1..14114aa88c 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Enviar y Bloquear", "composer.toggle_dropdown": "Alternar desplegable", "composer.uploading": "Subiendo %1", + "composer.formatting.bold": "Negrita", + "composer.formatting.italic": "Itálica", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Tachado", + "composer.formatting.link": "Enlace", + "composer.formatting.picture": "Foto", + "composer.upload-picture": "Subir foto", + "composer.upload-file": "Subir archivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancelar", "bootbox.confirm": "Confirmar", diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json index 1effb38fee..b0391106db 100644 --- a/public/language/es/notifications.json +++ b/public/language/es/notifications.json @@ -15,10 +15,7 @@ "upvoted_your_post_in_dual": "<strong>%1</strong> y <strong>%2</strong> han votado positivamente tu respuesta en <strong>%3</strong>.", "upvoted_your_post_in_multiple": "<strong>%1</strong> y otras %2 personas han votado positivamente tu respuesta en <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> su tema ha sido movido a <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> se ha movido <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> se ha guardado su post en <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> y <strong>%2</strong> se han guardado su post en <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> y otros %2 usuarios se han guardado su post en <strong>%3</strong>.", + "moved_your_topic": "<strong>%1</strong> ha movido <strong>%2</strong>", "user_flagged_post_in": "<strong>%1</strong> ha reportado una respuesta en <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> y <strong>%2</strong> han reportado un post en <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> y otras %2 personas han reportado un post en <strong>%3</strong>", diff --git a/public/language/es/pages.json b/public/language/es/pages.json index 4a79f28222..2ca1263ff9 100644 --- a/public/language/es/pages.json +++ b/public/language/es/pages.json @@ -12,28 +12,31 @@ "users/sort-posts": "Top por mensajes", "users/sort-reputation": "Más reputados", "users/banned": "Usuarios baneados", + "users/most-flags": "Usuarios mas reportados", "users/search": "Buscar", "notifications": "Notificaciones", "tags": "Etiquetas", "tag": "Temas etiquetados en \"%1\"", "register": "Registrar una cuenta", + "registration-complete": "Registration complete", "login": "Acceder a tu cuenta", "reset": "Restablecer contraseña", "categories": "Categorías", "groups": "Grupos", "group": "Grupo de %1", "chats": "Chats", - "chat": "Chateando con %1", - "account/edit": "Editando \"%1\"", + "chat": "Chatear con %1", + "account/edit": "Editar \"%1\"", "account/edit/password": "Editar contraseña de \"%1\"", "account/edit/username": "Editar nombre de usuario de \"%1\"", "account/edit/email": "Editar email \"%1\"", + "account/info": "Información de cuenta", "account/following": "Gente que sigue %1", "account/followers": "Seguidores de %1", "account/posts": "Publicados por %1", "account/topics": "Temas creados por %1", "account/groups": "Grupos de %1", - "account/favourites": "Publicaciones favoritas de %1 ", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Preferencias", "account/watched": "Temas seguidos por %1", "account/upvoted": "Publicaciones votadas positivamente %1", diff --git a/public/language/es/recent.json b/public/language/es/recent.json index fbfb6238c0..8c959ca0d5 100644 --- a/public/language/es/recent.json +++ b/public/language/es/recent.json @@ -6,7 +6,7 @@ "year": "Año", "alltime": "Siempre", "no_recent_topics": "No hay publicaciones recientes.", - "no_popular_topics": "No hay tópicos populares", + "no_popular_topics": "No hay publicaciones populares", "there-is-a-new-topic": "Hay una nueva publicación.", "there-is-a-new-topic-and-a-new-post": "hay una nueva publicación y un nuevo mensaje.", "there-is-a-new-topic-and-new-posts": "Hay una nueva publicación y %1 nuevos mensajes.", @@ -15,5 +15,5 @@ "there-are-new-topics-and-new-posts": "Hay %1 nuevas publicaciones y %2 nuevos mensajes.", "there-is-a-new-post": "Hay un nuevo mensaje.", "there-are-new-posts": "Hay %1 nuevos mensajes.", - "click-here-to-reload": "Cliquea aquí para recargar." + "click-here-to-reload": "Click para recargar." } \ No newline at end of file diff --git a/public/language/es/register.json b/public/language/es/register.json index ef1102ea8a..a6e2d4bc76 100644 --- a/public/language/es/register.json +++ b/public/language/es/register.json @@ -1,5 +1,6 @@ { "register": "Registrarse", + "cancel_registration": "Cancelar registro", "help.email": "Por defecto, tu cuenta de correo electrónico estará oculta al publico.", "help.username_restrictions": "El nombre de usuario debe tener entre %1 y %2 carácteres. Los miembros pueden responderte escribiendo @<span id='yourUsername'>usuario</span>.", "help.minimum_password_length": "Tu contraseña debe tener al menos %1 carácteres.", @@ -15,5 +16,8 @@ "alternative_registration": "Métodos de registro alternativos", "terms_of_use": "Términos y Condiciones de uso", "agree_to_terms_of_use": "Acepto los Términos y Condiciones de uso", - "registration-added-to-queue": "Tu registro se ha añadido a la cola de aprobación,. Recibirás un correo cuando este sea aceptado por un administrador. " + "terms_of_use_error": "Debes aceptar los términos de uso", + "registration-added-to-queue": "Tu registro se ha añadido a la cola de aprobación,. Recibirás un correo cuando este sea aceptado por un administrador. ", + "interstitial.intro": "Se requiere de información adicional antes de crear tu cuenta.", + "interstitial.errors-found": "No pudimos completar tu registro:" } \ No newline at end of file diff --git a/public/language/es/topic.json b/public/language/es/topic.json index 1f13a9e8e9..0f7acfaca0 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -26,23 +26,47 @@ "tools": "Herramientas", "flag": "Reportar", "locked": "Cerrado", - "bookmark_instructions": "Clic aquí para regresar al último aporte sin leer en este hilo.", + "pinned": "Fijo", + "moved": "Movido", + "bookmark_instructions": "Haz click aquí para volver a tu último mensaje leído en este tema", "flag_title": "Reportar este mensaje", "flag_success": "Este mensaje ha sido reportado para moderación.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Este tema ha sido borrado. Solo los usuarios que tengan privilegios de administración de temas pueden verlo.", "following_topic.message": "Ahora recibiras notificaciones cuando alguien publique en este tema.", - "not_following_topic.message": "No recibiras notificaciones de este tema.", + "not_following_topic.message": "Podras ver este tema en la lista de no leidos, pero no recibirás notificaciones cuando alguien escriba en él.", + "ignoring_topic.message": "Ya no verás este tema en no leídos. Serás notificado si te mencionan o te votan.", "login_to_subscribe": "Por favor, conéctate para subscribirte a este tema.", "markAsUnreadForAll.success": "Publicación marcada como no leída para todos.", - "mark_unread": "Marcar como no leído", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "Marcar no leído", + "mark_unread.success": "Tema marcado como no leído.", "watch": "Seguir", "unwatch": "Dejar de seguir", "watch.title": "Serás notificado cuando haya nuevas respuestas en este tema", "unwatch.title": "Dejar de seguir este tema", "share_this_post": "Compartir este post", - "thread_tools.title": "Herramientas del tema", - "thread_tools.markAsUnreadForAll": "Marcar como no leído", + "watching": "Siguiendo", + "not-watching": "No siguiendo", + "ignoring": "Ignorando", + "watching.description": "Notificarme de nuevas respuestas.<br/>Mostrar tema en no leídos. ", + "not-watching.description": "No notificarme de nuevas respuestas.<br/>Mostrar tema en no leídos si sigo esa categoría. ", + "ignoring.description": "No notificarme de nuevas respuestas.<br/>No mostrar tema en no leídos. ", + "thread_tools.title": "Herramientas", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Adherir tema", "thread_tools.unpin": "Despegar tema", "thread_tools.lock": "Cerrar tema", @@ -65,18 +89,19 @@ "disabled_categories_note": "Las categorías deshabilitadas están en gris", "confirm_move": "Mover", "confirm_fork": "Dividir", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Marcador", + "favourites": "Marcadores", + "favourites.has_no_favourites": "No has marcado ningún tema aún.", "loading_more_posts": "Cargando más publicaciones", "move_topic": "Mover tema", "move_topics": "Mover temas", - "move_post": "Mover publicación", + "move_post": "Mover mensaje", "post_moved": "¡Publicación movida correctamente!", "fork_topic": "Dividir tema", "topic_will_be_moved_to": "Este tema será movido a la categoría", "fork_topic_instruction": "Pulsa en los mensajes que quieres dividir", "fork_no_pids": "¡No has seleccionado ningún mensaje!", + "fork_pid_count": "%1 mensaje(s) seleccionados", "fork_success": "¡Se ha creado un nuevo tema a partir del original! Haz click aquí para ir al nuevo tema.", "delete_posts_instruction": "Haz click en los mensajes que quieres eliminar/limpiar", "composer.title_placeholder": "Ingresa el título de tu tema...", @@ -96,7 +121,7 @@ "more_users": "%1 usuario(s) más", "more_guests": "%1 invitado(s) más", "users_and_others": "%1 y otros %2", - "sort_by": "Ordenar por", + "sort_by": "Ordenar", "oldest_to_newest": "Más antiguo a más nuevo", "newest_to_oldest": "Más nuevo a más antiguo", "most_votes": "Mayor número de votos", diff --git a/public/language/es/unread.json b/public/language/es/unread.json index d77a5f7979..a451185530 100644 --- a/public/language/es/unread.json +++ b/public/language/es/unread.json @@ -6,5 +6,8 @@ "selected": "Seleccionados", "all": "Todos", "all_categories": "Todos los foros", - "topics_marked_as_read.success": "¡Temas marcados como leídos!" + "topics_marked_as_read.success": "¡Temas marcados como leídos!", + "all-topics": "Todos los Temas", + "new-topics": "Temas Nuevos", + "watched-topics": "Temas Suscritos" } \ No newline at end of file diff --git a/public/language/es/uploads.json b/public/language/es/uploads.json index 1622cb5693..9dac954194 100644 --- a/public/language/es/uploads.json +++ b/public/language/es/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Subiendo el archivo...", + "select-file-to-upload": "¡Selecciona un archivo para subir!", + "upload-success": "¡Archivo subido correctamente!", + "maximum-file-size": "Máximo %1 kb" } \ No newline at end of file diff --git a/public/language/es/user.json b/public/language/es/user.json index b518f7c38c..e605cf72a0 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -2,13 +2,14 @@ "banned": "Baneado", "offline": "Desconectado", "username": "Nombre de usuario", - "joindate": "Fecha de Registro", + "joindate": "Fecha de registro", "postcount": "Número De Publicaciones", "email": "Correo electrónico", "confirm_email": "Confirmar correo electrónico", - "ban_account": "Banear Cuenta", + "account_info": "Información de cuenta", + "ban_account": "Banear cuenta", "ban_account_confirm": "Quieres confirmar el baneo de este usuario?", - "unban_account": "Desbanear Cuenta", + "unban_account": "Desbanear cuenta", "delete_account": "Eliminar cuenta", "delete_account_confirm": "¿Estás seguro de que quieres eliminar tu cuenta? <br /><strong>Esta acción es irreversible y no podrás recuperar ninguno de tus datos.</strong><br /><br />Introduce tu nombre de usuario para confirmar la eliminación de la cuenta.", "delete_this_account_confirm": "Esta seguro de eliminar este usuario? <br /><strong>Esta acción de irreversible y no podrás recuperar ningún dato</strong><br /><br />", @@ -22,7 +23,7 @@ "profile": "Perfil", "profile_views": "Visitas", "reputation": "Reputación", - "favourites": "Bookmarks", + "favourites": "Marcadores", "watched": "Suscritos", "followers": "Seguidores", "following": "Siguiendo", @@ -30,7 +31,7 @@ "signature": "Firma", "birthday": "Cumpleaños", "chat": "Chat", - "chat_with": "Chateando con %1", + "chat_with": "Chatear con %1", "follow": "Seguir", "unfollow": "Dejar de seguir", "more": "Más", @@ -39,7 +40,7 @@ "change_username": "Cambiar nombre de usuario", "change_email": "Cambiar email", "edit": "Editar", - "edit-profile": "Edit Profile", + "edit-profile": "Editar Perfil", "default_picture": "Icono por defecto", "uploaded_picture": "Imagen subida", "upload_new_picture": "Subir nueva imagen", @@ -56,11 +57,11 @@ "password": "Contraseña", "username_taken_workaround": "El nombre de usuario que has solicitada ya está siendo usado, por tanto lo hemos alterado ligeramente. Ahora eres conocido como <strong>%1</strong>.", "password_same_as_username": "Tu Constraseña es igual al nombre de Usuario, por favor seleccione otra Constraseña.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "Tu contraseña es igual que tu dirección de correo, por favor elige otra contraseña.", "upload_picture": "Subir foto", "upload_a_picture": "Subir una foto", "remove_uploaded_picture": "Borrar Imagen subida", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Subir imagen de portada", "settings": "Opciones", "show_email": "Mostrar mi correo electrónico", "show_fullname": "Mostrar mi nombre completo", @@ -88,22 +89,41 @@ "topics_per_page": "Temas por página", "posts_per_page": "Post por página", "notification_sounds": "Reproducir un sonido al recibir una notificación", + "notifications_and_sounds": "Notificaciones y Sonidos", + "incoming-message-sound": "Sonido del mensaje entrante", + "outgoing-message-sound": "Sonido del mensaje saliente", + "notification-sound": "Sonido de notificación", + "no-sound": "No sound", "browsing": "Preferencias de navegación.", "open_links_in_new_tab": "Abrir los enlaces externos en una nueva pestaña", - "enable_topic_searching": "Activar la búsqueda \"in-topic\"", - "topic_search_help": "Si está activada, la búsqueda 'in-topic' sustituirá el comportamiento por defecto del navegador y le permitirá buscar en el tema al completo, en vez de hacer una búsqueda únicamente sobre el contenido mostrado en pantalla", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Seguir los temas en los que respondes", - "follow_topics_you_create": "Seguir publicaciones que creas", - "grouptitle": "Selecciona el título del grupo que deseas visualizar", + "enable_topic_searching": "Activar la búsqueda \"dentro del tema\"", + "topic_search_help": "Si está activada, la búsqueda 'dentro del tema' al usar el buscador de la barra de navegación automáticamente buscaras solo en el tema actual.", + "delay_image_loading": "Retrasar la carga de imágenes", + "image_load_delay_help": "Si se habilita, las imágenes no cargarán hasta que se vean en pantalla", + "scroll_to_my_post": "Luego de enviar una respuesta, mostrar el nuevo mensaje", + "follow_topics_you_reply_to": "Seguir temas a los que respondes", + "follow_topics_you_create": "Seguir temas creados por ti", + "grouptitle": "Título del grupo", "no-group-title": "Sin título de grupo", "select-skin": "Seleccionar una plantilla", - "select-homepage": "Seleccione una Página de inicio", + "select-homepage": "Seleccione una página de inicio", "homepage": "Página de inicio", "homepage_description": "Seleccione una página para su uso habitual como la página principal del foro o 'Ninguno' para utilizar la página de inicio.", - "custom_route": "Modificar página de ruta", + "custom_route": "Pagina de inicio personalizada", "custom_route_help": "Introduzca un nombre de ruta aquí, sin ninguna barra anterior (por ejemplo, \"reciente\", o \"popular\")", "sso.title": "Servicios de Inicio de sesión Único", "sso.associated": "Asociado con", - "sso.not-associated": "Da clic aquí para asociarse con" + "sso.not-associated": "Da clic aquí para asociarse con", + "info.latest-flags": "Ultimos reportes", + "info.no-flags": "Ningun mensaje reportado encontrado", + "info.ban-history": "Histórico reciente de bans", + "info.no-ban-history": "Este usuario nunca ha sido baneado", + "info.banned-until": "Baneado hasta %1", + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Motivo no especificado", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/es/users.json b/public/language/es/users.json index cf8bdaefff..5b96abeef0 100644 --- a/public/language/es/users.json +++ b/public/language/es/users.json @@ -2,6 +2,7 @@ "latest_users": "Últimos usuarios", "top_posters": "Top por mensajes", "most_reputation": "Más reputados", + "most_flags": "Más Reportados", "search": "Buscar", "enter_username": "Ingresa el nombre de usuario que quieres buscar", "load_more": "Cargar más", @@ -16,5 +17,5 @@ "unread_topics": "Temas no leídos", "categories": "Categorías ", "tags": "Etiquetas", - "no-users-found": "No users found!" + "no-users-found": "¡No se encontraron usuarios!" } \ No newline at end of file diff --git a/public/language/et/category.json b/public/language/et/category.json index e5ffefd5d1..3202f90ab4 100644 --- a/public/language/et/category.json +++ b/public/language/et/category.json @@ -10,7 +10,11 @@ "share_this_category": "Jaga seda kategooriat", "watch": "Vaata", "ignore": "Ignoreeri", - "watch.message": "Jälgid nüüdsest teateid sellest kategooriast", - "ignore.message": "Ignoreerid nüüdsest teateid sellest kategooriast", + "watching": "Vaatab", + "ignoring": "Ignoreerib", + "watching.description": "Näita teemasid lugemata teemade hulgas", + "ignoring.description": "Ära näita teemasid lugemata teemade hulgas", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Jälgitavad kategooriad" } \ No newline at end of file diff --git a/public/language/et/email.json b/public/language/et/email.json index a1d3d4371b..03fa17fc9f 100644 --- a/public/language/et/email.json +++ b/public/language/et/email.json @@ -24,6 +24,7 @@ "digest.day": "päev", "digest.week": "nädal", "digest.month": "kuu", + "digest.subject": "Ima 1% jaoks", "notif.chat.subject": "Sulle on saabunud uus sõnum kasutajalt %1", "notif.chat.cta": "Vajuta siia, et jätkata vestlusega", "notif.chat.unsub.info": "See chat teavitus on saadetud teile tellimuse seadistuse tõttu.", diff --git a/public/language/et/error.json b/public/language/et/error.json index 013db49d04..be7a5dcd48 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -14,20 +14,25 @@ "invalid-password": "Vigane parool", "invalid-username-or-password": "Palun täpsusta kasutajanime ja parooli", "invalid-search-term": "Vigane otsingusõna", + "csrf-invalid": "Me ei saanud Sind sisse logida, võimalik, et tänu aegunud sessioonile, palun proovi uuesti", "invalid-pagination-value": "Väär lehekülje numeratsioon, peab olema vähemalt %1 ja kõige rohkem %2", "username-taken": "Kasutajanimi on juba võetud", "email-taken": "Email on võetud", "email-not-confirmed": "Su emaili aadress ei ole kinnitatud, vajuta siia et kinnitada.", "email-not-confirmed-chat": "Sõnumeid ei ole võimalik enne saata kui sinu email on kinnitatud. Kinnitamiseks vajuta siia.", + "email-not-confirmed-email-sent": "Su email ei ole veel kinnitatud, palun kontrolli oma e-posti ning kinnita email.", "no-email-to-confirm": "See foorum nõuab emaili kinnitust, palun vajuta siia, et sisestada email", "email-confirm-failed": "Meil ei õnnestunud sinu emaili kinnitada, proovi hiljem uuesti.", "confirm-email-already-sent": "Kinnituskiri on juba saadetud, palun oota %1 minut(it) uue kirja saatmiseks.", + "sendmail-not-found": "Sendmail'i käivitatavat ei leitud, palun tee kindlaks, et see on installeeritud ja on käivitatav kasutaja poolt, kes käitab NodeBB't.", "username-too-short": "Kasutajanimi on liiga lühike", "username-too-long": "Kasutajanimi on liiga pikk", "password-too-long": "Parool liiga pikk", "user-banned": "Kasutaja bannitud", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Vabandust, te peate ootama %1 sekund(it) enne esimese postituse loomist.", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Vabandust! Sinu IP-aadress on siin kogukonnas keelatud. Kui arvad, et see on eksitus, palun leia kontakti administraatoriga.", + "ban-expiry-missing": "Palun sisesta keelu lõpukuupäev", "no-category": "Kategooriat ei eksisteeri", "no-topic": "Teemat ei eksisteeri", "no-post": "Postitust ei eksisteeri", @@ -38,10 +43,26 @@ "category-disabled": "Kategooria keelatud", "topic-locked": "Teema lukustatud", "post-edit-duration-expired": "Te peate ootama %1 sekund(it), enne kui oma postitust muudate.", + "post-edit-duration-expired-minutes": "Teil on lubatud muuta oma postitusi vaid %1 minuti jooksul peale postitamist", + "post-edit-duration-expired-minutes-seconds": "Teil on lubatud muuta oma postitusi vaid %1 minuti %2 sekundi jooksul peale postitamist", + "post-edit-duration-expired-hours": "Teil on lubatud muuta oma postitusi vaid %1 tunni jooksul peale postitamist", + "post-edit-duration-expired-hours-minutes": "Teil on lubatud muuta oma postitusi vaid %1 tunni %2 minuti jooksul peale postitamist", + "post-edit-duration-expired-days": "Teil on lubatud muuta oma postitusi vaid %1 päeva jooksul peale postitamist", + "post-edit-duration-expired-days-hours": "Teil on lubatud muuta oma postitusi vaid %1 päeva %2 tunni jooksul peale postitamist", + "post-delete-duration-expired": "Teil on lubatud kustutada oma postitusi vaid %1 sekundi jooksul peale postitamist", + "post-delete-duration-expired-minutes": "Teil on lubatud kustutada oma postitusi vaid %1 minuti jooksul peale postitamist", + "post-delete-duration-expired-minutes-seconds": "Teil on lubatud kustutada oma postitusi vaid %1 minuti %2 sekundi jooksul peale postitamist", + "post-delete-duration-expired-hours": "Teil on lubatud kustutada oma postitusi vaid %1 tunni jooksul peale postitamist", + "post-delete-duration-expired-hours-minutes": "Teil on lubatud kustutada oma postitusi vaid %1 tunni %2 minuti jooksul peale postitamist", + "post-delete-duration-expired-days": "Teil on lubatud kustutada oma postitusi vaid %1 päeva jooksul peale postitamist", + "post-delete-duration-expired-days-hours": "Teil on lubatud kustutada oma postitusi vaid %1 päeva %2 tunni jooksul peale postitamist", + "cant-delete-topic-has-reply": "Sa ei saa oma postitust kustutada, kui sellele on vastatud", + "cant-delete-topic-has-replies": "Sa ei saa oma postitust kustutada pärast seda, kui sellel on %1 vastust", "content-too-short": "Palun tehke pikem postitus. Postituse pikkus peab olema vähemalt %1 tähemärk(i).", "content-too-long": "Palun tehke lühem postitus. Postituse pikkus peab olema vähem kui %1 tähemärk(i).", "title-too-short": "Palun sisesta pikem pealkiri. Pealkirjad ei saa olla lühemad kui %1 tähemärk(i).", "title-too-long": "Palun sisesta lühem pealkiri. Pealkirjad ei saa olla pikemad kui %1 tähemärk(i).", + "category-not-selected": "Category not selected.", "too-many-posts": "Te saate postitada %1 sekundi tagant - palun oodake enne uue postituse tegemist.", "too-many-posts-newbie": "Uue kasutajana saadte postitada vaid iga %1 sekundi tagant, seniks kuni olete teeninud vähemalt %2 reputatsiooni - palun oodake enne uue postituse tegemist.", "tag-too-short": "Palun sisestage pikem märksõna. Märksõna pikkus peab olema vähemalt %1 tähemärk(i).", @@ -51,14 +72,16 @@ "still-uploading": "Palun oota, kuni üleslaadimised on laetud.", "file-too-big": "Maksimaalne üleslaetava faili suurus on %1 kB - valige väiksema mahuga fail.", "guest-upload-disabled": "Külaliste üleslaadimine on keelatud.", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "already-favourited": "Sa oled juba selle postituse järjehoidjatesse pannud.", + "already-unfavourited": "Sa oled juba selle postituse järjehoidjatest ära võtnud.", "cant-ban-other-admins": "Sa ei saa bannida teisi administraatoreid!", "cant-remove-last-admin": "Te olete ainus administraator. Lisage keegi teine administraatoriks, enne kui eemaldate endalt administraatori.", + "cant-delete-admin": "Eemalda sellelt kasutajalt administraatori õigused enne selle kustutamist", "invalid-image-type": "Vigane pildi formaat. Lubatud formaadid on: %1", "invalid-image-extension": "Vigane pildi formaat", "invalid-file-type": "Vigane faili formaat. Lubatud formaadid on: %1", "group-name-too-short": "Grupi nimi liiga lühike", + "group-name-too-long": "Grupi nimi liiga pikk", "group-already-exists": "Grupp juba eksisteerib", "group-name-change-not-allowed": "Grupi nimevahetus ei ole lubatud", "group-already-member": "Oled juba selles grupis", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Sul ei ole lubatud antud sõnumit muuta", "cant-remove-last-user": "Sa ei saa viimast kasutajat eemaldada", "cant-delete-chat-message": "Sul ei ole lubatud antud sõnumit kustutada", + "already-voting-for-this-post": "Sa oled juba hääletanud sellel postitusel.", "reputation-system-disabled": "Reputatsiooni süsteem ei ole aktiveeritud", "downvoting-disabled": "Negatiivsete häälte andmine ei ole võimaldatud", "not-enough-reputation-to-downvote": "Sul ei ole piisavalt reputatsiooni, et anda negatiivset hinnangut sellele postitusele.", @@ -98,6 +122,10 @@ "invite-maximum-met": "Sa oled kutsunud maksimaalse lubatud inimeste arvu (%1 %2 'st).", "no-session-found": "Sisse logimis sessiooni ei leitud!", "not-in-room": "Kasutaja pole ruumis", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "no-users-in-room": "Ühtegi kasutajat ei leidu siit ruumist", + "cant-kick-self": "Sa ei saa ennast ära visata gruppist", + "no-users-selected": "Ühtki kasutajat pole valitud", + "invalid-home-page-route": "Vigane avalehe suunamine", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/et/global.json b/public/language/et/global.json index f43dd9007a..07a395cc83 100644 --- a/public/language/et/global.json +++ b/public/language/et/global.json @@ -7,8 +7,10 @@ "403.login": "Äkki peaksid <a href='%1/login'>sisse logima</a>?", "404.title": "Ei leitud", "404.message": "Tundub, et lehte mida otsid, ei eksisteeri. Mine tagasi <a href='%1/'>avalehele</a>.", - "500.title": "Süsteemi viga", + "500.title": "Süsteemne error.", "500.message": "Oih! Midagi läks valesti!", + "400.title": "Vigane päring.", + "400.message": "Tundub, et see link on vigane, palun kontrolli see üle ja proovi uuesti. Võid ka minna tagasi <a href='%1/'>avalehele</a>.", "register": "Registreeri", "login": "Logi sisse", "please_log_in": "Palun logi sisse", @@ -17,6 +19,7 @@ "welcome_back": "Tere tulemast tagasi!", "you_have_successfully_logged_in": "Edukalt sisse logitud", "save_changes": "Salvesta muudatused", + "save": "Save", "close": "Sulge", "pagination": "Lehekülgede numeratsioon", "pagination.out_of": "%1 kõigist %2-st", @@ -50,7 +53,9 @@ "topics": "Teemat", "posts": "Postitust", "best": "Parim", + "upvoters": "Poolt hääletajad", "upvoted": "Kiideti heaks", + "downvoters": "Vastu hääletajad", "downvoted": "Hääletas vastu", "views": "Vaatamist", "reputation": "Reputatsioon", @@ -87,8 +92,11 @@ "map": "Kaart", "sessions": "Logitud Sessioonid", "ip_address": "IP Aadress", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "enter_page_number": "Sisesta lehekülje number", + "upload_file": "Lae fail üles", + "upload": "Lae üles", + "allowed-file-types": "Lubatud faili formaadid on %1", + "unsaved-changes": "Sul on salvestamata muudatusi. Oled kindel, et soovid lahkuda?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/et/groups.json b/public/language/et/groups.json index db8dfce3d1..3ccbe588b8 100644 --- a/public/language/et/groups.json +++ b/public/language/et/groups.json @@ -41,7 +41,7 @@ "details.hidden": "Peidetud", "details.hidden_help": "Kui sisse lülitatud, siis seda gruppi ei kuvata gruppide nimekirjas ning liikmed tuleb lisada manuaalselt", "details.delete_group": "Kustuta grupp", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Privaatset gruppid on keelatud sellel süsteemi tasemel, see sätte ei tee midagi", "event.updated": "Grupi lisainformatsiooni on uuendatud", "event.deleted": "Grupp \"%1\" on kustutatud", "membership.accept-invitation": "Võta kutse vastu", @@ -50,5 +50,7 @@ "membership.leave-group": "Lahku grupist", "membership.reject": "Lükka tagasi", "new-group.group_name": "Grupi nimi:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Lae gruppi pilt üles", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/et/login.json b/public/language/et/login.json index 6c283a8b83..0b3eb840ff 100644 --- a/public/language/et/login.json +++ b/public/language/et/login.json @@ -5,7 +5,8 @@ "remember_me": "Mäleta mind?", "forgot_password": "Unustasid parooli?", "alternative_logins": "Alternatiivsed sisse logimise võimalused", - "failed_login_attempt": "Sisse logimine ebaõnnestus, palun proovi uuesti.", + "failed_login_attempt": "Sisselogimine ebaõnnestus", "login_successful": "Edukalt sisse logitud!", - "dont_have_account": "Pole veel kasutajat?" + "dont_have_account": "Pole veel kasutajat?", + "logged-out-due-to-inactivity": "Sind on Administraatori Juhtpaneelist ebaaktiivsuse tõttu välja logitud" } \ No newline at end of file diff --git a/public/language/et/modules.json b/public/language/et/modules.json index ec23804489..5824723051 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -6,7 +6,7 @@ "chat.user_typing": "%1 kirjutab sõnumit...", "chat.user_has_messaged_you": "%1 saatis sulle sõnumi.", "chat.see_all": "Vaata kõiki vestluseid", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Märgi kõik jutud loetuks", "chat.no-messages": "Vali sõnumisaaja, et vaadata sõnumite ajalugu.", "chat.no-users-in-room": "Ühtki kasutajat selles ruumis", "chat.recent-chats": "Hiljutised vestlused", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Kinnita ja Lukusta", "composer.toggle_dropdown": "Aktiveeri rippmenüü", "composer.uploading": "%1 Üleslaadimine", + "composer.formatting.bold": "Paksult", + "composer.formatting.italic": "Kaldkiri", + "composer.formatting.list": "Nimekiri", + "composer.formatting.strikethrough": "Läbitõmmatud", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Pilt", + "composer.upload-picture": "Lae pilt üles", + "composer.upload-file": "Lae fail üles", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "Olgu", "bootbox.cancel": "Katkesta", "bootbox.confirm": "Kinnita", diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json index 11e2823a75..c804914ee1 100644 --- a/public/language/et/notifications.json +++ b/public/language/et/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> ja %2 teist on kiitnud sinu postituse heaks: <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> liigutas sinu postituse <strong>%2 'sse</strong>", "moved_your_topic": "<strong>%1</strong> liigutas <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> raporteeris postitust <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> ja <strong>%2</strong> märgistasid postituse: <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> ja %2 teist märgistasid postituse: <strong>%3</strong>", @@ -30,7 +27,7 @@ "user_started_following_you_dual": "<strong>%1</strong> ja <strong>%2</strong> hakkasid sind jälgima.", "user_started_following_you_multiple": "<strong>%1</strong> ja %2 hakkasid sind jälgima.", "new_register": "<strong>%1</strong> saatis registreerimistaotluse.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "<strong>%1</strong> registreerimistaotlust ootavad ülevaadet.", "email-confirmed": "Emaili aadress kinnitatud", "email-confirmed-message": "Täname, et kinnitasite oma emaili aadressi. Teie kasutaja on nüüd täielikult aktiveeritud.", "email-confirm-error-message": "Emaili aadressi kinnitamisel tekkis viga. Võibolla kinnituskood oli vale või aegunud.", diff --git a/public/language/et/pages.json b/public/language/et/pages.json index f85b28ead0..d22b929152 100644 --- a/public/language/et/pages.json +++ b/public/language/et/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Kasutajad, kel on enim postitusi", "users/sort-reputation": "Suurima reputatsiooniga kasutajad", "users/banned": "Keelustatud Kasutajad", + "users/most-flags": "Enim raporteeritud kasutajad", "users/search": "Kasutajate otsing", "notifications": "Teated", "tags": "Märksõnad", "tag": "Teemad märksõnadega \"%1\"", "register": "Registreeri kasutaja", + "registration-complete": "Registration complete", "login": "Logi oma kasutajasse sisse", "reset": "Lähtesta oma kasutaja parool", "categories": "Kategooriad", @@ -28,18 +30,19 @@ "account/edit/password": "Redigeerid \"%1\" parooli", "account/edit/username": "Redigeerid \"%1\" kasutajanime", "account/edit/email": "Redigeerid \"%1\" emaili", + "account/info": "Kasutaja info", "account/following": "Kasutaja %1 jälgib", "account/followers": "Kasutajad, kes jälgivad %1", "account/posts": "Postitused, mis on tehtud kasutaja %1 poolt", "account/topics": "Teemad on kirjutanud %1", "account/groups": "Kasutaja %1 grupid", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Kasutaja sätted", "account/watched": "Teemasid jälgib %1 kasutajat", "account/upvoted": "Postitused %1 poolt heaks kiidetud", "account/downvoted": "Postitused %1 poolt vastu hääletatud", "account/best": "Parimad postitused %1 poolt", - "confirm": "Email Confirmed", + "confirm": "Emaili aadress kinnitatud", "maintenance.text": "%1 foorumil on käimas hooldustööd. Palun külastage meid mõne aja pärast uuesti.", "maintenance.messageIntro": "Administraator on jätnud ka omaltpoolt sõnumi:", "throttled.text": "%1 ei ole hetkel kättesaadav liigse koormuse tõttu. Palun tulge tagasi mõni teine kord." diff --git a/public/language/et/register.json b/public/language/et/register.json index cbd042fb68..11c8c1e2d0 100644 --- a/public/language/et/register.json +++ b/public/language/et/register.json @@ -1,5 +1,6 @@ { "register": "Registreeri", + "cancel_registration": "Katkesta registreerimine", "help.email": "Algsättena peidetakse sinu e-mail avalikuse eest.", "help.username_restrictions": "Unikaalne kasutajanimi, mis on %1 - %2 tähemärki pikk. Teised saavad sind postitustes mainida kasutades @<span id='yourUsername'>kasutajanime</span>.", "help.minimum_password_length": "Sinu parooli pikkus peab olema vähemalt %1 tähemärki pikk.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatiivne registreerimismeetod", "terms_of_use": "Foorumi reeglid", "agree_to_terms_of_use": "Nõustun foorumi reeglitega", - "registration-added-to-queue": "Teie registreerimine vaadatakse üle. Te saate e-kirja kui administraator on aktsepteerinud registreermimise." + "terms_of_use_error": "Sa pead nõustuma Tingimustega", + "registration-added-to-queue": "Teie registreerimine vaadatakse üle. Te saate e-kirja kui administraator on aktsepteerinud registreermimise.", + "interstitial.intro": "Enne kasutaja loomist on meil vaja lisainfot.", + "interstitial.errors-found": "Meil ei õnnestunud registreerimist lõpule viia:" } \ No newline at end of file diff --git a/public/language/et/topic.json b/public/language/et/topic.json index d6aba7fb7a..003ec3b941 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -26,12 +26,30 @@ "tools": "Tööriistad", "flag": "Märgista", "locked": "Lukustatud", - "bookmark_instructions": "Kliki siia, et naasta viimasele lugemata postitusele selles teemas.", + "pinned": "Märgistatud", + "moved": "Liigutatud", + "bookmark_instructions": "Vajuta siia, et tagasi minna viimati loetud postituse juurde siin teemas.", "flag_title": "Märgista see postitus modereerimiseks", "flag_success": "See postitus on nüüd märgistatud modereerimiseks.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "See teema on kustutatud. Ainult kasutajad kellel on piisavalt õigusi saavad seda näha.", "following_topic.message": "Sulle ei edastata enam teateid uutest postitustest kui keegi postitab siia teemasse.", - "not_following_topic.message": "Sulle ei edastata enam teateid uutest postitustest siin teemas.", + "not_following_topic.message": "Sa näed seda postitust lugemata postituste nimekirjas, kuid sa ei näe selle kohta teateid, kui keegi sinna postitab.", + "ignoring_topic.message": "Sa ei näe seda teemat enam lugemata teemade nimekirjas. Sind teavitatakse, kui Sind mainitakse või Sinu postitust kiidetakse heaks.", "login_to_subscribe": "Palun registreeru kasutajaks või logi sisse, et tellida teateid selle postituse kohta.", "markAsUnreadForAll.success": "Teema märgitud mitte-loetuks kõikidele.", "mark_unread": "Märgi lugematuks", @@ -41,8 +59,14 @@ "watch.title": "Saa teateid uutest postitustest siin teemas", "unwatch.title": "Ära järgi enam seda teemat", "share_this_post": "Jaga seda postitust", + "watching": "Vaatan", + "not-watching": "Ei vaata", + "ignoring": "Ignoreerin", + "watching.description": "Teavita mind uutest vastustest.<br/>Näita teemat lugemata teemade hulgas.", + "not-watching.description": "Ära teavita mind uutest vastustest.<br/>Näita teemat lugemata teemade hulgas, kui kategooria on ignoreeritud.", + "ignoring.description": "Ära teavita mind uutest vastustest.<br/>Ära näita teemat lugemata teemade hulgas.", "thread_tools.title": "Teema tööriistad", - "thread_tools.markAsUnreadForAll": "Märgi mitte-loetuks", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Tõsta esile teema", "thread_tools.unpin": "Märgista teema", "thread_tools.lock": "Lukusta teema", @@ -65,9 +89,9 @@ "disabled_categories_note": "Kinnised kategooriad on hallid", "confirm_move": "Liiguta", "confirm_fork": "Fork", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Järjehoidja", + "favourites": "Järjehoidjad", + "favourites.has_no_favourites": "Sa ei ole ühtegi postitust pannud järjehoidjatesse veel.", "loading_more_posts": "Laen postitusi", "move_topic": "Liiguta teemat", "move_topics": "Liiguta teemasi", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "See teema liigutatakse antud kategooriasse", "fork_topic_instruction": "Vajuta postitustele, mida soovid forkida", "fork_no_pids": "Sa ei ole postitusi valinud!", + "fork_pid_count": "%1 postitus(t) valitud", "fork_success": "Edukalt ''forkisid'' teema! Vajuta siia, et vaadata loodud teemat.", "delete_posts_instruction": "Klikka postitustel, mida tahad kustutada/puhastada", "composer.title_placeholder": "Sisesta teema pealkiri siia...", diff --git a/public/language/et/unread.json b/public/language/et/unread.json index 140ff1dd62..169806bdb0 100644 --- a/public/language/et/unread.json +++ b/public/language/et/unread.json @@ -6,5 +6,8 @@ "selected": "Valitud", "all": "Kõik", "all_categories": "Kõik kategooriad", - "topics_marked_as_read.success": "Teemad märgitud loetuks!" + "topics_marked_as_read.success": "Teemad märgitud loetuks!", + "all-topics": "Kõik teemad", + "new-topics": "Uued teemad", + "watched-topics": "Vaadatud teemad" } \ No newline at end of file diff --git a/public/language/et/uploads.json b/public/language/et/uploads.json index 1622cb5693..feee146ab8 100644 --- a/public/language/et/uploads.json +++ b/public/language/et/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Laen faili üles...", + "select-file-to-upload": "Vali fail mida üles laadida!", + "upload-success": "Fail üles laetud edukalt!", + "maximum-file-size": "Maksimaalselt %1 kb" } \ No newline at end of file diff --git a/public/language/et/user.json b/public/language/et/user.json index 463b4c5e8e..ab2d33c6e4 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -6,6 +6,7 @@ "postcount": "Postitusi", "email": "Email", "confirm_email": "Kinnita email", + "account_info": "Kasutaja info", "ban_account": "Bannige kasutaja", "ban_account_confirm": "Kas te tõesti soovite antud kasutajat bannida?", "unban_account": "Eemaldage kontolt ban", @@ -22,7 +23,7 @@ "profile": "Profiil", "profile_views": "Vaatamisi", "reputation": "Reputatsioon", - "favourites": "Bookmarks", + "favourites": "Järjehoidjad", "watched": "Vaadatud", "followers": "Jälgijad", "following": "Jälgimised", @@ -39,7 +40,7 @@ "change_username": "Vaheta kasutajanime", "change_email": "Vaheta emaili", "edit": "Muuda", - "edit-profile": "Edit Profile", + "edit-profile": "Redigeeri profiili", "default_picture": "Algne ikoon", "uploaded_picture": "Üleslaetud pilt", "upload_new_picture": "Laadi uus pilt", @@ -56,11 +57,11 @@ "password": "Parool", "username_taken_workaround": "Kasutajanimi mida soovisid, ei olnud saadaval, seeg muutsime seda natukene. Sinu uus kasutajanimi on nüüd: <strong>%1</strong>", "password_same_as_username": "Su parool kattub su kasutajanimega, palun vali mõni muu parool.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "Su parool kattub su e-mailiga, palun vali mõni muu parool.", "upload_picture": "Laadi pilt", "upload_a_picture": "Lae pilt üles", "remove_uploaded_picture": "Eemalda üleslaetud pilt", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Lae üles katte pilt", "settings": "Seaded", "show_email": "Näita minu emaili", "show_fullname": "Näita minu täisnime", @@ -88,14 +89,21 @@ "topics_per_page": "Teemasi ühe lehekülje kohta", "posts_per_page": "Postitusi ühe lehekülje kohta", "notification_sounds": "Mängi heli, kui teade saabub.", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Sirvimis sätted", "open_links_in_new_tab": "Ava väljaminevad lingid uues aknas", "enable_topic_searching": "Võimalda teemasisene otsing", "topic_search_help": "Kui see on sisse lükatud, siis teemasisene otsing võtab üle brauseri tavapärase otsingu ning võimaldab otsida ainult ekraanile mahtuva teema asemel terve teema ulatuses.", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Järgi teemasid, millele olete vastanud.", - "follow_topics_you_create": "Järgi teemasi, mis on teie loodud.", - "grouptitle": "Vali grupile tiitel mida kuvada soovid", + "delay_image_loading": "Viivita pildi laadimisega", + "image_load_delay_help": "Kui lubatud, pildid teemades ei lae kuni nad on nähtavuses", + "scroll_to_my_post": "Pärast vastuse postitamist, näita uut postitust", + "follow_topics_you_reply_to": "Jälgi teemasid, millele vastad", + "follow_topics_you_create": "Jälgi teemasid, mille lood", + "grouptitle": "Grupi tiitel", "no-group-title": "Grupi tiitel puudub", "select-skin": "Vali välimus", "select-homepage": "Vali avaleht", @@ -105,5 +113,17 @@ "custom_route_help": "Sisestage tee nimi siia, ilma eelnevate kaldkriipsudeta (nt. \"recent\", või \"popular\")", "sso.title": "Ühekordse sisselogimisega teenused", "sso.associated": "Seotud koos", - "sso.not-associated": "Kliki siia, et siduda koos" + "sso.not-associated": "Kliki siia, et siduda koos", + "info.latest-flags": "Viimased raporteerimised", + "info.no-flags": "Raporteeritud postitusi ei leitud", + "info.ban-history": "Hiljutiste keeldude ajalugu", + "info.no-ban-history": "Seda kasutajat pole kunagi keelustatud", + "info.banned-until": "Keelustatud kuni %1", + "info.banned-permanently": "Igavesti keelustatud", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/et/users.json b/public/language/et/users.json index bb377c7239..7c5da97116 100644 --- a/public/language/et/users.json +++ b/public/language/et/users.json @@ -2,6 +2,7 @@ "latest_users": "Hilised kasutajad", "top_posters": "Top postitajad", "most_reputation": "Kõige rohkem reputatsiooni", + "most_flags": "Enim raporteerimisi", "search": "Otsi", "enter_username": "Sisesta kasutajanimi, keda soovid otsida", "load_more": "Lae veel", diff --git a/public/language/fa_IR/category.json b/public/language/fa_IR/category.json index 50c14bee3b..174e85bb67 100644 --- a/public/language/fa_IR/category.json +++ b/public/language/fa_IR/category.json @@ -1,16 +1,20 @@ { - "category": "دسته بندی", - "subcategories": "زیر دسته ها", - "new_topic_button": "جستار تازه", - "guest-login-post": "برای ثبت نظر وارد شوید", - "no_topics": "<strong>هیچ پستی در این دسته نیست.</strong><br />چرا شما یکی نفرستید؟", + "category": "دستهبندی", + "subcategories": "زیر دستهبندی", + "new_topic_button": "موضوع تازه", + "guest-login-post": "برای ارسال پست وارد شوید", + "no_topics": "<strong>هیچ پستی در این دستهبندی نیست.</strong><br />چرا شما یکی نمیفرستید؟", "browsing": "بینندهها", "no_replies": "هیچ کسی پاسخ نداده است.", "no_new_posts": "هیچ پست جدیدی وجود ندارد.", "share_this_category": "به اشتراکگذاری این دسته بندی", "watch": "پیگیری", "ignore": "نادیده گرفتن", - "watch.message": "در حال حاضر شما به روز رسانی های این دسته بندی را دنبال می کنید", - "ignore.message": "در حال حاضر شما به روز رسانی های این دسته بندی را نادیده میگیرد", + "watching": "درحال پیگیری", + "ignoring": "در حال نادیده گرفتن", + "watching.description": "نمایش موضوعات خوانده نشده", + "ignoring.description": "هنوز موضوعات خوانده نشده را نشان نده", + "watch.message": "شما در حال حاضر در حال مشاهده بهروزرسانی این دستهبندی و همه زیر شاخههای آن هستید", + "ignore.message": "شما در حال حاضر نادیده گرفتهاید بهروزرسانی این دستهبندی و همه زیر شاخههای آن", "watched-categories": "دسته بندی های پیگیری شده" } \ No newline at end of file diff --git a/public/language/fa_IR/email.json b/public/language/fa_IR/email.json index 1ec13b4a26..121cfadc55 100644 --- a/public/language/fa_IR/email.json +++ b/public/language/fa_IR/email.json @@ -4,12 +4,12 @@ "invite": "دعوتنامه از %1", "greeting_no_name": "سلام", "greeting_with_name": "سلام %1", - "welcome.text1": "متشکر بابت ثبت نام در %1!", + "welcome.text1": "متشکریم بابت ثبت نام در %1!", "welcome.text2": "برای فعال کردن کامل اکانت شما، ما نیاز داریم تا اطمینان حاصل کنیم که شما مالک ایمیلی که با ان ثبت نام کردید هستید.", "welcome.text3": "ِک مدیر درخواست ثبت نام شما را قبول کرده. اکنون میتوانید با نام کاربری/رمز عبور خود وارد شوید", "welcome.cta": "برای تأیید آدرس ایمیل خود اینجا کلیک کنید", "invitation.text1": "%1 شما را برای پیوستن به %2 دعوت کرده", - "invitation.ctr": "برای ساخت حساب کاربری خود اینجا را کلیک کنید", + "invitation.ctr": "برای ساخت شناسه کاربری خود اینجا را کلیک کنید", "reset.text1": "ما یک درخواست برای بازنشانی رمزعبور شما دریافت کرده ایم، احتمالا به این دلیل که شما آن را فراموش کرده اید. اگر این مورد نیست و شما رمز خود را به یاد دارید، لطفا این ایمیل را نادیده بگیرید.", "reset.text2": "برای ادامه بازنشانی رمز، لطفابر روی این لینک کلیک کنید:", "reset.cta": "برای تنظیم مجدد کلمه عبوری خود اینجا کلیک کنید", @@ -24,6 +24,7 @@ "digest.day": "روز", "digest.week": "هفته", "digest.month": "ماه", + "digest.subject": "خلاصه برای %1", "notif.chat.subject": "پیام چتی جدیدی از %1 دریافت شد", "notif.chat.cta": "برای ادامهی چت اینجا کلیک کنید", "notif.chat.unsub.info": "این اطلاعیه ی چتیی که برای شما فرستاده شده به علت تنظیمات اشترک شماست.", diff --git a/public/language/fa_IR/error.json b/public/language/fa_IR/error.json index c6651bc9d8..d6c420b126 100644 --- a/public/language/fa_IR/error.json +++ b/public/language/fa_IR/error.json @@ -1,9 +1,9 @@ { "invalid-data": "داده(های) نامعتبر", - "not-logged-in": "وارد حساب کاربری نشده اید.", + "not-logged-in": "وارد حساب کاربری نشدهاید.", "account-locked": "حساب کاربری شما موقتاً مسدود شده است.", - "search-requires-login": "استفاده از جستجو نیازمند ورود با نام کاربری و رمز عبور است. لطفا ابتدا وارد شوید.", - "invalid-cid": "شناسه دسته نامعتبر است.", + "search-requires-login": "استفاده از جستجو نیازمند ورود با نامکاربری و رمزعبور است. لطفا ابتدا وارد شوید.", + "invalid-cid": "آیدی دستهبندی نامعتبر است.", "invalid-tid": "شناسه موضوع نامعتبر است.", "invalid-pid": "شناسه پست نامعتبر است.", "invalid-uid": "شناسه کاربر نامعتبر است.", @@ -14,20 +14,25 @@ "invalid-password": "کلمه عبور نامعتبر است.", "invalid-username-or-password": "لطفا هم نام کاربری و هم کلمه عبور را مشخص کنید", "invalid-search-term": "کلمه جستجو نامعتبر است", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "اجازه ورود شما تمام شده است، لطفا دوباره وارد شوید.", + "invalid-pagination-value": "ارزش گذاری صفحه نامعتبر است، کمترین مقدار 1% و بیشترین مقدار 2% باید باشد", "username-taken": "این نام کاربری گرفته شده است.", "email-taken": "این ایمیل گرفته شده است.", "email-not-confirmed": "ایمیل شما تاکنون تایید نشده است، برای تایید ایمیل خود را اینجا را کلیک کنید.", "email-not-confirmed-chat": "شما تا قبل از تایید ایمیل قادر به چت نیستید، لطفا برای تایید ایمیل خود اینجا کلیک کنید", + "email-not-confirmed-email-sent": "ایمیل شما هنوز تایید نشده است، لطفا صندوق پیام های خود را برای تایید ایمیل بررسی کنید.", "no-email-to-confirm": "ایمیل شما تایید نشده است ، لطفا برای وارد کردن ایمیل اینجا کلیک کنید", "email-confirm-failed": "سیستم موفق به تایید ایمیل شما نشد، لطفا بعدا دوباره سعی کنید", "confirm-email-already-sent": "ایمیل فعالسازی قبلا فرستاده شده، لطفا %1 دقیقه صبر کنید تا ایمیل دیگری بفرستید.", + "sendmail-not-found": "اجازه ارسال رایانامه پیدا نشد، لطفا مطمئن شوید این قابلیت نصب شده و توسط کاربر مد نظر در نودبیبی قابل اجرا است.", "username-too-short": "نام کاربری خیلی کوتاه است.", "username-too-long": "نام کاربری بسیار طولانیست", "password-too-long": "کلمه عبور بسیار طولانیست", - "user-banned": "کاربر محروم شد.", + "user-banned": "کاربر اخراج شد", + "user-banned-reason": "کاربر اخراج شده (دلیل: %1)", "user-too-new": "با عرض پوزش، شما باید %1 ثانیه پیش از فرستادن پست نخست خود صبر کنید", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "با عرض پوزش فراوان، نشانی آی پی شما در این انجمن مسدود شده است، اگر فکر میکنید اشتباهی رخ داده با مدیریت انجمن تماس بگیرید.", + "ban-expiry-missing": "لطفا تاریخ پایان برای این مسدود کردن ارائه دهید", "no-category": "دسته بندی وجود ندارد", "no-topic": "موضوع وجود ندارد.", "no-post": "پست وجود ندارد", @@ -38,10 +43,26 @@ "category-disabled": "دسته غیرفعال شد.", "topic-locked": "موضوع بسته شد.", "post-edit-duration-expired": "شما تنها می توانید %1 ثانیه پس از فرستادن پست آنرا ویرایش کنید", + "post-edit-duration-expired-minutes": "شما تنها می توانید %1 دقیقه(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-edit-duration-expired-minutes-seconds": "شما تنها می توانید %1 دقیقه(ها) %2 ثانیه(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-edit-duration-expired-hours": "شما تنها می توانید %1 ساعت(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-edit-duration-expired-hours-minutes": "شما تنها می توانید %1 ساعت(ها) %2 دقیقه(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-edit-duration-expired-days": "شما تنها می توانید %1 روز(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-edit-duration-expired-days-hours": "شما تنها می توانید %1 روز(ها) %2 ساعت(ها) پس از فرستادن پست آن را ویرایش کنید", + "post-delete-duration-expired": "شما تنها می توانید %1 ثانیه(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-minutes": "شما تنها می توانید %1 دقیقه(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-minutes-seconds": "شما تنها می توانید %1 دقیقه(ها) %2 ثانیه(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-hours": "شما تنها می توانید %1 ساعت(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-hours-minutes": "شما تنها می توانید %1 ساعت(ها) %2 دقیقه(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-days": "شما تنها می توانید %1 روز(ها) پس از فرستادن پست آن را پاک کنید", + "post-delete-duration-expired-days-hours": "شما تنها می توانید %1 روز(ها) %2 ساعت(ها) پس از فرستادن پست آن را پاک کنید", + "cant-delete-topic-has-reply": "اگر کسی به موضوع شما پاسخ داده باشد، نمیتوانید آنرا حذف نمائید", + "cant-delete-topic-has-replies": "اگر 1% به موضوع جواب داده شده باشد ، نمیتوانید آنرا حذف نمائید", "content-too-short": "خواهشمندیم پست بلندتری بنویسید. پستها دستکم باید %1 کاراکتر داشته باشند.", "content-too-long": "لطفا طول مطلب را کوتاه تر کنید. طول پست نمیتواند بیشتر از %1 کاراکتر باشد.", "title-too-short": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل %1 کاراکتر داشته باشد.", "title-too-long": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل %1 کاراکتر داشته باشد.", + "category-not-selected": "هیچ دستهبندی انتخاب نشده.", "too-many-posts": "شما می توانید هر %1 ثانیه یک پست ایجاد کنید - لطفا قبل از ارسال پست جدید صبر کنید", "too-many-posts-newbie": "به عنوان یک کاربر جدید ، تا زمانی که شما 2% اعتبار کسب کنید می توانید هر %1 ثانیه یک پست ایجاد کنید - لطفا قبل از ایجاد پست جدید صبر کنید .", "tag-too-short": "لطفا برچسب بلندتری وارد کنید. برچسبها باید حداقل %1 کاراکتر داشته باشند.", @@ -50,15 +71,17 @@ "too-many-tags": "تعداد برچسب ها بیشتر از حد مجاز است. موضوع ها نمی توانند بیشتر از %1 برچسب داشته باشند", "still-uploading": "خواهشمندیم تا پایان بارگذاریها شکیبا باشید.", "file-too-big": "حداکثر مجاز حجم فایل %1 کیلوبایت می باشد - لطفا فایلی با حجم کمتر بارگذاری کنید", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "guest-upload-disabled": "بارگذاری برای مهمانان غیر فعال شده است", + "already-favourited": "شما در حال حاضر این پست را به علاقمندی های خود اضافه کرده ایید", + "already-unfavourited": "شما در حال حاضر این پست را از لیست علاقمندی خود خارج کردید", "cant-ban-other-admins": "شما نمیتوانید دیگر مدیران را محروم کنید!", "cant-remove-last-admin": "شما تنها مدیر می باشید . شما باید قبل از عزل خود از مدیریت یک کاربر دیگر را مدیر کنید", + "cant-delete-admin": "قبل از حذف این کاربر دسترسی های مدیریت را از وی بگیرید.", "invalid-image-type": "نوع تصویر نامعتبر است. نوعهای قابل قبول اینها هستند: %1", "invalid-image-extension": "پسوند عکس نامعتبر است", "invalid-file-type": "نوع پرونده نامعتبر است. نوعهای قابل قبول اینها هستند: %1", "group-name-too-short": "نام گروه خیلی کوتاه است.", + "group-name-too-long": "نام گروه بسیار طولانی است.", "group-already-exists": "این گروه از پیش وجود دارد.", "group-name-change-not-allowed": "تغیر نام گروه نیاز به دسترسی دارد.", "group-already-member": "شما عضوی از این گروه می باشید", @@ -78,13 +101,14 @@ "about-me-too-long": "با عرض پوزش محتوای 'درباره ی من' نمی تواند طولانی تر از %1 کاراکتر باشد", "cant-chat-with-yourself": "شما نمیتوانید با خودتان چت کنید!", "chat-restricted": "این کاربر پیام های چتی خود را محدود کرده است . آنها بایدشما را دنبال کنند تا اینکه شما بتوانید به آنها پیامی بفرستید", - "chat-disabled": "Chat system disabled", + "chat-disabled": "سیستم گفتمان غیرفعال شده است", "too-many-messages": "شما پیامهای خیلی زیادی فرستاده اید، لطفا مدتی صبر نمایید", "invalid-chat-message": "پیام نامعتبر", "chat-message-too-long": "پیام طولانی تر از حد مجاز است", "cant-edit-chat-message": "شما اجازه ی ویرایش این پیام را ندارید", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "cant-remove-last-user": "شما نمی توانید آخرین کاربر را حذف کنید", + "cant-delete-chat-message": "شما اجازه حذف این پیام را ندارید.", + "already-voting-for-this-post": "شما قبلا به این پست رای داده اید.", "reputation-system-disabled": "سیستم اعتبار غیر فعال شده است", "downvoting-disabled": "رأی منفی غیر فعال شده است", "not-enough-reputation-to-downvote": "شما اعتبار کافی برای دادن رأی منفی به این پست را ندارید.", @@ -95,9 +119,13 @@ "parse-error": "هنگام تجزیه پاسخ سرور اشتباهی پیش امد", "wrong-login-type-email": "لطفا از ایمیل خود برای ورود استفاده کنید", "wrong-login-type-username": "لطفا از نام کاربری خود برای ورود استفاده کنید", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", + "invite-maximum-met": "ظرفیت دعوت شما تکمیل شده است (1% از 2%)", + "no-session-found": "هیچ سشن ورودی یافت نشد!", + "not-in-room": "هیچ کاربری در این گفتگو نیست", "no-users-in-room": "هیچ کاربری در این گفتگو نیست", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "شما نمی توانید خودتان را از گروه کیک کنید", + "no-users-selected": "هیچ کاربر(های) انتخاب نشده", + "invalid-home-page-route": "مسیر صفحه اصلی نامعتبر است", + "invalid-session": "عدم تطابق جلسه", + "invalid-session-text": "به نظر میرسد این جلسه برای ورود دیگر فعال نیست و یا با سرور هماهنگ نیست. لطفا این صفحه را رفرش کنید." } \ No newline at end of file diff --git a/public/language/fa_IR/global.json b/public/language/fa_IR/global.json index 948145c589..d9b70f3224 100644 --- a/public/language/fa_IR/global.json +++ b/public/language/fa_IR/global.json @@ -7,8 +7,10 @@ "403.login": "شاید باید <a href='%1/login'>وارد شوید</a>؟", "404.title": "یافت نشد", "404.message": "به نظر میاید شما به صفحه ای برخورد کرده اید که وجود ندارد. بازگشت به <a href='%1/'>صفحه ی خانه</a>", - "500.title": "خطای درونی.", + "500.title": "خطای داخلی.", "500.message": "اوه! گویا اشتباهی رخ داده!", + "400.title": "درخواست بد.", + "400.message": "به نظر میرسد که این پیوند مشکل دارد، لطفا دوباره بررسی کنید که این پیوند صحیح است و دوباره تلاش کنید، در غیر اینصورت به <a href='%1/'>صفحه اصلی</a> بازگردید.", "register": "نامنویسی", "login": "درون آمدن", "please_log_in": "لطفا به درون بیایید", @@ -17,6 +19,7 @@ "welcome_back": "خوش آمدید", "you_have_successfully_logged_in": "با موفقیت درون آمدهاید", "save_changes": "اندوختن تغییرها", + "save": "ذخیره", "close": "بستن", "pagination": "صفحهبندی", "pagination.out_of": "%1 از %2", @@ -49,9 +52,11 @@ "users": "کاربران", "topics": "موضوع ها", "posts": "دیدگاهها", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", + "best": "بهترین", + "upvoters": "رای مثبتها", + "upvoted": "رای مثبت", + "downvoters": "رای منفیها", + "downvoted": "رای منفی", "views": "بازدیدها", "reputation": "اعتبار", "read_more": "بیشتر بخوانید", @@ -65,11 +70,11 @@ "posted_in_ago_by": "ارسال شده در %1 %2 توسط %3", "user_posted_ago": "%1 در %2 ارسال کرده است", "guest_posted_ago": "مهمان در %1 ارسال کرده است", - "last_edited_by": "last edited by %1", + "last_edited_by": "آخرین ویرایش توسط %1 انجام شده", "norecentposts": "هیچ دیدگاه تازهای نیست", "norecenttopics": "هیچ جستار تازهای نیست", "recentposts": "دیدگاههای تازه", - "recentips": "آخرین IPها", + "recentips": "آخرین آیپی وارد شده", "away": "دور از دسترس", "dnd": "مزاحم نشوید", "invisible": "مخفی", @@ -85,10 +90,13 @@ "unfollow": "دنبال نکن", "delete_all": "حذف همه", "map": "نقشه", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "sessions": "Session های ورود", + "ip_address": "آدرس آی پی", + "enter_page_number": "شماره صفحه را وارد کنید", + "upload_file": "بارگذاری فایل", + "upload": "بارگذاری", + "allowed-file-types": "فایل قابل قبول اینها هستند %1", + "unsaved-changes": "تغییرات شما ذخیره نشده. شما مطمئن هستید که میخواهید از اینجا دور شوید؟", + "reconnecting-message": "اتصال شما به %1 به نظر میرسد از دست رفته. لطفا صبر کنید ما سعی میکنیم که دوباره شما را متصل کنیم.", + "play": "پخش" } \ No newline at end of file diff --git a/public/language/fa_IR/groups.json b/public/language/fa_IR/groups.json index 2c1db5b30d..1c0331ba78 100644 --- a/public/language/fa_IR/groups.json +++ b/public/language/fa_IR/groups.json @@ -8,7 +8,7 @@ "pending.reject": "رد", "pending.accept_all": "پذیرش همه", "pending.reject_all": "رد همه", - "pending.none": "در حال حاضر هیچ عضوی در انتظار نیست", + "pending.none": "در حال حاضر هیچ عضوی در انتظار تایید نیست", "invited.none": "در حال حاضر هیچ کسی دعوت نشده است", "invited.uninvite": "لغو دعوت", "invited.search": "جستجو به دنبال کاربرانی به جهت دعوت به این گروه", @@ -18,7 +18,7 @@ "cover-save": "ذخیره", "cover-saving": "در حال ذخیره کردن", "details.title": "جزئیات گروه", - "details.members": "لیست اعضا", + "details.members": "فهرست اعضا", "details.pending": "اعضای در انتظار", "details.invited": "اعضای دعوت شده", "details.has_no_posts": "اعضای این گروه هیچ پستی ایجاد نکرده اند", @@ -41,7 +41,7 @@ "details.hidden": "پنهان", "details.hidden_help": "اگر فعال باشد، این گروه در فهرست گروهها پیدا نمیشود و کاربران باید دستی فراخوانده شوند", "details.delete_group": "حذف گروه", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "گروه های خصوصی در این سطح سیستم غیر فعال هستند، این گزینه هیچ کاری انجام نمی دهد", "event.updated": "جزییات گروه با موفقیت به روز شد", "event.deleted": "گروه \"%1\" حدف شد", "membership.accept-invitation": "دعوت را قبول میکنم", @@ -49,6 +49,8 @@ "membership.join-group": "ورود به گروه", "membership.leave-group": "خروج از گروه", "membership.reject": "رد", - "new-group.group_name": "نام گروه", - "upload-group-cover": "آپلود کاور گروه" + "new-group.group_name": "نام گروه:", + "upload-group-cover": "آپلود کاور گروه", + "bulk-invite-instructions": "برای دعوت به این گروه فهرستی از نامکاربری که با کاما جدا کنید را وارد کنید", + "bulk-invite": "میزان دعوت" } \ No newline at end of file diff --git a/public/language/fa_IR/login.json b/public/language/fa_IR/login.json index 578071109d..3e408a5133 100644 --- a/public/language/fa_IR/login.json +++ b/public/language/fa_IR/login.json @@ -5,7 +5,8 @@ "remember_me": "مرا به یاد بسپار؟", "forgot_password": "گذرواژه را فراموش کردهاید؟", "alternative_logins": "روشهای درون آمدن جایگزین", - "failed_login_attempt": "شکست در درون آمدن، لطفا دوباره تلاش کنید.", + "failed_login_attempt": "ورود ناموفق", "login_successful": "شما با موفقیت به درون آمدهاید!", - "dont_have_account": "حساب کاربری ندارید؟" + "dont_have_account": "حساب کاربری ندارید؟", + "logged-out-due-to-inactivity": "شما به علت عدم فعالیت از کنترل پنل مدیر کل خارج شده اید " } \ No newline at end of file diff --git a/public/language/fa_IR/modules.json b/public/language/fa_IR/modules.json index cae06f83b0..dac3c93b2f 100644 --- a/public/language/fa_IR/modules.json +++ b/public/language/fa_IR/modules.json @@ -1,25 +1,25 @@ { "chat.chatting_with": "گفتگو با <span id=\"chat-with-name\"></span>", "chat.placeholder": "پیام گفتگو را اینجا بنویسید، دکمه Enter را بزنید تا فرستاده شود.", - "chat.send": "فرستادن", + "chat.send": "ارسال", "chat.no_active": "شما هیچ گفتگوی فعالی ندارید.", "chat.user_typing": "%1 در حال نوشتن است...", "chat.user_has_messaged_you": "%1 به شما پیام داده است.", - "chat.see_all": "دیدن همه ی چت ها", - "chat.mark_all_read": "Mark all chats read", - "chat.no-messages": "مشخص کنید تاریخچه چتهایتان با چه کاربری را میخواهید ببینید", + "chat.see_all": "دیدن همهی گفتگوها", + "chat.mark_all_read": "علامت گذاری شده به عنوان تمام گفتگو ها را خوانده", + "chat.no-messages": "مشخص کنید تاریخچه گفتگوها با چه کاربری را میخواهید ببینید", "chat.no-users-in-room": "هیچ کاربری در این گفتگو نیست", - "chat.recent-chats": "چتهای اخیر", + "chat.recent-chats": "گفتگو های اخیر", "chat.contacts": "تماسها", "chat.message-history": "تاریخچه پیامها", - "chat.pop-out": "پاپ آپ چت", + "chat.pop-out": "پاپ آپ گفتگو", "chat.maximize": "تمام صفحه", "chat.seven_days": "7 روز", "chat.thirty_days": "30 روز", "chat.three_months": "3 ماه", "chat.delete_message_confirm": "آیا مطمئن هستید که می خواهید این پیام را حذف کنید؟", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", + "chat.roomname": "اتاق گفتگو %1", + "chat.add-users-to-room": "اضافه کردن کاربر به این گفتگو", "composer.compose": "ارسال", "composer.show_preview": "نمایش پیشنمایش", "composer.hide_preview": "مخفی کردن پیشنمایش", @@ -29,8 +29,18 @@ "composer.submit_and_lock": "ارسال و قفل", "composer.toggle_dropdown": "باز و بسته کردن کرکره", "composer.uploading": "در حال بارگذاری %1", + "composer.formatting.bold": "توپر", + "composer.formatting.italic": "کج", + "composer.formatting.list": "فهرست", + "composer.formatting.strikethrough": "خط خورده", + "composer.formatting.link": "پیوند", + "composer.formatting.picture": "عکس", + "composer.upload-picture": "بارگذاری عکس", + "composer.upload-file": "بارگذاری فایل", + "composer.zen_mode": "حالت ذن", + "composer.select_category": "یک دستهبندی انتخاب کنید", "bootbox.ok": "باشه", - "bootbox.cancel": "لغو", + "bootbox.cancel": "انصراف", "bootbox.confirm": "تایید", "cover.dragging_title": "تنظیم مکان عکس کاور", "cover.dragging_message": "عکس کاور با کلیک موس گرفته و در مکان دلخواه رها کنید و بر روی \"ذخیره\" کلیک کنید", diff --git a/public/language/fa_IR/notifications.json b/public/language/fa_IR/notifications.json index ee4641d006..6d49770f07 100644 --- a/public/language/fa_IR/notifications.json +++ b/public/language/fa_IR/notifications.json @@ -1,36 +1,33 @@ { "title": "آگاهسازیها", "no_notifs": "هیچ آگاهسازی تازهای ندارید", - "see_all": "دیدن همه ی اطلاعیه ها", - "mark_all_read": "همه اطلاعیه ها را خوانده شده علامت بزن", + "see_all": "دیدن همهی آگاهسازیها", + "mark_all_read": "همه آگاهسازیها را خوانده شده علامت بزن", "back_to_home": "بازگشت به %1", "outgoing_link": "پیوند برونرو", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "شما در حال ترک %1 هستید", "continue_to": "ادامه به %1", "return_to": "بازگشت به %1", - "new_notification": "آکاهسازی تازه", - "you_have_unread_notifications": "شما اطلاعیه های نخوانده دارید.", + "new_notification": "آگاهسازی تازه", + "you_have_unread_notifications": "شما آگاهسازیهای نخوانده دارید.", "new_message_from": "پیام تازه از <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> امتیاز مثبت به پست شما در <strong>%2</strong> داده", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", + "upvoted_your_post_in_dual": "<strong>%1</strong> و <strong>%2</strong> رای مثبت به پست شما در\n <strong>%3</strong>.", + "upvoted_your_post_in_multiple": "<strong>%1</strong>و %2 دیگران به پست شما رای مثبت دادن در <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> پست شما را به <strong>%2</strong> انتقال داده است", "moved_your_topic": "<strong>%2</strong> <strong>%1</strong> را منتقل کرده است", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> پست شما را در <strong>%2</strong> علامتدار کرده", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", + "user_flagged_post_in_dual": "<strong>%1</strong> و <strong>%2</strong> نشانهگذاری کرده اند پست را در <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> و %2 نفر دیگر این پست را نشانهگذاری کرده در <strong>%3</strong>", "user_posted_to": "پاسخ دادن به <strong>%2</strong> از سوی <strong>%1</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", + "user_posted_to_dual": "<strong>%1</strong> و <strong>%2</strong> پاسخ به پست دادند در: <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> و %2 نفر دیگر به پست شما پاسخ ارسال کردهاند در: <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> یک موضوع جدید ارسال کرده: <strong>%2</strong>", "user_started_following_you": "<strong>%1</strong> شروع به دنبال کردن شما کرده", "user_started_following_you_dual": "<strong>%1</strong> و <strong>%2</strong> شروع به دنبال کردن شما کرده.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", + "user_started_following_you_multiple": "<strong>%1</strong> و %2 نفر دیگر شروع به دنبال کردن شما کرده.", "new_register": "<strong>%1</strong> یک درخواست ثبت نام ارسال کرده است", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "تعداد <strong>%1</strong> درخواست عضویت برای بررسی وجود دارد.", "email-confirmed": "ایمیل تایید شد", "email-confirmed-message": "بابت تایید ایمیلتان سپاسگزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.", "email-confirm-error-message": "خطایی در تایید آدرس ایمیل شما پیش آمده است. ممکن است کد نامعتبر و یا منقضی شده باشد.", diff --git a/public/language/fa_IR/pages.json b/public/language/fa_IR/pages.json index 06d6c1ec35..fd04a9f0b3 100644 --- a/public/language/fa_IR/pages.json +++ b/public/language/fa_IR/pages.json @@ -5,21 +5,23 @@ "popular-week": "موضوعات پربازدید این هفته", "popular-month": "موضوعات پربازدید این ماه", "popular-alltime": "پربازدیدترین موضوعات", - "recent": "جستارهای تازه", - "flagged-posts": "Flagged Posts", + "recent": "موضوعهای تازه", + "flagged-posts": "پست نشانه گذاری شده", "users/online": "کاربران آنلاین", "users/latest": "آخرین کاربران", "users/sort-posts": "کاربران با بیشترین پست", "users/sort-reputation": "کاربران دارای بیشترین اعتبار", - "users/banned": "Banned Users", + "users/banned": "کاربران مسدود شده", + "users/most-flags": "بیشترین کاربران پرچم شده", "users/search": "جستجوی کاربر", "notifications": "آگاهسازیها", "tags": "برچسبها", "tag": "موضوع های داری کلیدواژه \"%1\"", "register": "ثبت نام یک حساب", + "registration-complete": "ثبت نام تکمیل شد", "login": "به حساب خوب وارد شوید", "reset": "رمز عبور حساب خود را بازنشانی کنید", - "categories": "طبقه بندی ها", + "categories": "دستهبندیها", "groups": "گروهها", "group": "%1 گروه", "chats": "چتها", @@ -28,18 +30,19 @@ "account/edit/password": "ویرایش کلمه ی عبورِ \"%1\"", "account/edit/username": "ویرایش نام کاربریِ \"%1\"", "account/edit/email": "ویرایش ایمیلِ \"%1\"", + "account/info": "اطلاعات شناسه کاربری", "account/following": "کاربرانی که %1 دنبال میکند", "account/followers": "کاربرانی که %1 را دنبال میکنند", "account/posts": "پستهای %1", "account/topics": "موضوع های %1", "account/groups": "گروههای %1", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1 پست نشانک گذاری شده است", "account/settings": "تنظیمات کاربر", "account/watched": "موضوع های دیده شده توسط \"%1\"", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "account/upvoted": "رای مثبت داده شده به پست ها توسط %1", + "account/downvoted": "رای منفی داده شده به پست ها توسط %1", + "account/best": "بهترین پست های ارسال شده توسط %1", + "confirm": "ایمیل تایید شد", "maintenance.text": "%1 در حال حاضر تحت تعمیر و نگهدارییست. لطفا زمان دیگری مراجعه کنید.", "maintenance.messageIntro": "علاوه بر این، مدیر این پیام را گذاشته است:", "throttled.text": "%1 به دلیل بارگذاری بیش از حد ، قابل دسترس نمی باشد. لطفا در زمان دیگری دوباره امتحان کنید" diff --git a/public/language/fa_IR/register.json b/public/language/fa_IR/register.json index 1478014a36..15af9f66cb 100644 --- a/public/language/fa_IR/register.json +++ b/public/language/fa_IR/register.json @@ -1,7 +1,8 @@ { "register": "نامنویسی", + "cancel_registration": "انصراف ثبت نام", "help.email": "به طور پیشفرض، ایمیلی شما از دید همگان پنهان میشود.", - "help.username_restrictions": "یک نام کاربری یکتا بین %1 و %2 کاراکتر. دیگران میتوانند با @<span id='yourUsername'>نامکاربری</span> به شما اشاره کنند.", + "help.username_restrictions": "یک نام کاربری یکتا بین %1 و %2 نویسه. دیگران میتوانند با @<span id='yourUsername'>نامکاربری</span> به شما اشاره کنند.", "help.minimum_password_length": "کلمه عبور شما باید دستکم %1 کاراکتر داشته باشد.", "email_address": "نشانی رایانامه", "email_address_placeholder": "نوشتن نشانی رایانامه", @@ -15,5 +16,8 @@ "alternative_registration": "روش ثبت نام جایگزین", "terms_of_use": "شرایط استفاده", "agree_to_terms_of_use": "با شرایط استفاده موافقم", - "registration-added-to-queue": "ثبت نام شما به صف تایید اضافه شد. وقتی توسط یک مدیر تایید شد شما ایمیلی دریافت خواهید کرد." + "terms_of_use_error": "شما باید با شرایط انجمن موافقت کنید", + "registration-added-to-queue": "ثبت نام شما به صف تایید اضافه شد. وقتی توسط یک مدیر تایید شد شما ایمیلی دریافت خواهید کرد.", + "interstitial.intro": "ما نیاز به برخی اطلاعات اضافی شما قبل از ایجاد شناسه کاربری داریم.", + "interstitial.errors-found": "شما نمیتوانید ثبت نام خود را تکمیل کنید:" } \ No newline at end of file diff --git a/public/language/fa_IR/search.json b/public/language/fa_IR/search.json index 19c8f28c6e..8adfa77049 100644 --- a/public/language/fa_IR/search.json +++ b/public/language/fa_IR/search.json @@ -25,7 +25,7 @@ "sort-by": "مرتبسازی بر اساس", "last-reply-time": "زمان آخرین پاسخ", "topic-title": "عنوان موضوع", - "number-of-replies": "تعداد پاسخها", + "number-of-replies": "تعداد پاسخها", "number-of-views": "تعداد مشاهده ها", "topic-start-date": "زمان شروع موضوع", "username": "نام کاربری", diff --git a/public/language/fa_IR/tags.json b/public/language/fa_IR/tags.json index ba8c46a569..45470ec855 100644 --- a/public/language/fa_IR/tags.json +++ b/public/language/fa_IR/tags.json @@ -1,7 +1,7 @@ { "no_tag_topics": "جُستاری با این برچسب وجود ندارد.", "tags": "برچسبها", - "enter_tags_here": "برچسبها را اینجا وارد کنید. هر کدام بین %1 و %2 کاراکتور", - "enter_tags_here_short": "برچسب ها را وارد کنید ...", + "enter_tags_here": "برچسبها را اینجا وارد کنید، هر کدام بین %1 و %2 کاراکتر.", + "enter_tags_here_short": "برچسبها را وارد کنید...", "no_tags": "هنوز برچسبی وجود ندارد." } \ No newline at end of file diff --git a/public/language/fa_IR/topic.json b/public/language/fa_IR/topic.json index f3265b1ca9..391b15731d 100644 --- a/public/language/fa_IR/topic.json +++ b/public/language/fa_IR/topic.json @@ -13,7 +13,7 @@ "notify_me": "از پاسخهای تازه در موضوع آگاه شوید", "quote": "نقل قول", "reply": "پاسخ", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "پاسخ به موضوع", "guest-login-reply": "وارد شوید تا پست بفرستید", "edit": "ویرایش", "delete": "حذف", @@ -26,23 +26,47 @@ "tools": "ابزارها", "flag": "پرچم", "locked": "قفل شده است", - "bookmark_instructions": "برای بازگشت به آخرین پست خوانده نشده در این دسته اینجا کلیک کنید", + "pinned": "سنجاق شده", + "moved": "منتقل شده", + "bookmark_instructions": "برای بازگشت به آخرین پست در این موضوع اینجا را کلیک کنید.", "flag_title": "پرچمگذاری این موضوع برای بررسی ناظران", "flag_success": "این موضوع برای بررسی ناظران پرچم گذاشته شد.", + "flag_manage_title": "پست نشانهگذاری شده در %1", + "flag_manage_history": "تاریخچه عملیات", + "flag_manage_no_history": "هیچ تاریخ رویدادی برای گزارش نیست", + "flag_manage_assignee": "نماینده", + "flag_manage_state": "حالت", + "flag_manage_state_open": "جدید/بازکردن", + "flag_manage_state_wip": "کار در حال انجام", + "flag_manage_state_resolved": "دوباره حل شده", + "flag_manage_state_rejected": "رد شده", + "flag_manage_notes": "یادداشتها به اشتراک گذاشته شده", + "flag_manage_update": "بهروزرسانی وضعیت نشانه", + "flag_manage_history_assignee": "اختصاص داده شده به %1", + "flag_manage_history_state": "وضعیت بهروزرسانی شد به %1", + "flag_manage_history_notes": "یادداشتهای نشانه بهروزرسانی شد", + "flag_manage_saved": "جزئیات نشانه بهروزرسانی شد", "deleted_message": "این موضوع پاک شده است. تنها کاربرانِ با حق مدیریت موضوع میتوانند آن را ببینند.", "following_topic.message": "از این پس اگر کسی در این موضوع پست بگذارد، شما آگاه خواهید شد.", - "not_following_topic.message": "شما دیگر اطلاعیه های این موضوع را دریافت نخواهید کرد.", + "not_following_topic.message": "شما این موضوع را تو فهرست موضوعات خوانده نشده میبینید، اما وقتی پست جدیدی ارسال میشود آگاهسازی دریافت نمیکنید.", + "ignoring_topic.message": "شما دیگر نمی خواهید این تاپیک را در فهرست عنوان های خوانده نشده ببینید. به شما اطلاع داده خواهد شد زمانی که به پست شما کسی رای بدهد.", "login_to_subscribe": "برای دنبال کردن این موضوع، لطفا ثبت نام کنید و یا با نام کاربری خود وارد شوید", "markAsUnreadForAll.success": "موضوع برای همگان نخوانده در نظر گرفته شد.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "علامت بزن به عنوان خوانده نشده", + "mark_unread.success": "تاپیک را علامت بزن به عنوان خوانده نشده", "watch": "پیگیری", "unwatch": "عدم پیگیری", "watch.title": "از پاسخهای تازه به این موضوع آگاه شوید.", "unwatch.title": "توقف پیگیری این موضوع", "share_this_post": "به اشتراکگذاری این موضوع", + "watching": "درحال پیگیری", + "not-watching": "درحال پیگیری نیستید", + "ignoring": "نادیده گرفتن", + "watching.description": "به من اطلاع بده برای پاسخ های جدید.<br/>نشان بده تاپیک های خوانده نشده را.", + "not-watching.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.<br/>تاپیک به صورت خوانده نشده قرار بگیرد ولی نادیده گرفته نشود.", + "ignoring.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.<br/>دیگر تاپیک را به صورت خوانده نشده نشان نده.", "thread_tools.title": "ابزارهای موضوع", - "thread_tools.markAsUnreadForAll": "نخوانده بگیر", + "thread_tools.markAsUnreadForAll": "علامت بزن به عنوان خوانده نشده برای همه", "thread_tools.pin": "سنجاق زدن موضوع", "thread_tools.unpin": "برداشتن سنجاق موضوع", "thread_tools.lock": "قفل کردن موضوع", @@ -51,7 +75,7 @@ "thread_tools.move_all": "جابجایی همه", "thread_tools.fork": "شاخه ساختن از موضوع", "thread_tools.delete": "پاک کردن موضوع", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "حذف پست ها", "thread_tools.delete_confirm": "آیا مطمئنید می خواهید این موضوع را حذف کنید؟", "thread_tools.restore": "برگرداندن موضوع", "thread_tools.restore_confirm": "آیا مطمئنید که می خواهید این موضوع را بازگردانی کنید؟", @@ -65,9 +89,9 @@ "disabled_categories_note": "دستههای از کار افتاده به رنگ خاکستری در میآیند", "confirm_move": "جابهجا کردن", "confirm_fork": "شاخه ساختن", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "علاقمندی", + "favourites": "علاقمندی ها", + "favourites.has_no_favourites": "شما هیچ پستی را به صورت علاقمندی ندارید.", "loading_more_posts": "بارگذاری پستهای بیشتر", "move_topic": "جابهجایی موضوع", "move_topics": "انتقال موضوع", @@ -77,8 +101,9 @@ "topic_will_be_moved_to": "این موضوع جابهجا خواهد شد به دستهٔ", "fork_topic_instruction": "پستهایی را که میخواهید به موضوع تازه ببرید، انتخاب کنید", "fork_no_pids": "هیچ پستی انتخاب نشده!", + "fork_pid_count": "%1 پست (ها) انتخاب شده اند", "fork_success": "موضوع با موفقیت منشعب شد! برای رفتن به موضوع انشعابی اینجا را کلیک کنید.", - "delete_posts_instruction": "Click the posts you want to delete/purge", + "delete_posts_instruction": "با کلیک بر روی پست شما می خواهید به حذف/پاکسازی", "composer.title_placeholder": "عنوان موضوعتان را اینجا بنویسید...", "composer.handle_placeholder": "نام", "composer.discard": "دور بیانداز", @@ -105,7 +130,7 @@ "stale.warning": "موضوعی که شما در حال پاسخگویی به آن هستید قدیمی می باشد. آیا میلید به جای آن یک موضوع جدید ایجاد کنید و در آن به این موضوع ارجاع دهید؟", "stale.create": "ایجاد یک موضوع جدید", "stale.reply_anyway": "در هر صورت می خواهم به این موضوع پاسخ دهم", - "link_back": "Re: [%1](%2)", + "link_back": "پاسخ: [%1](%2)", "spam": "اسپم", "offensive": "توهین آمیز", "custom-flag-reason": "وارد کردن دلیل پرچمگذاری" diff --git a/public/language/fa_IR/unread.json b/public/language/fa_IR/unread.json index c5a17f9649..b99597020c 100644 --- a/public/language/fa_IR/unread.json +++ b/public/language/fa_IR/unread.json @@ -6,5 +6,8 @@ "selected": "برگزیده", "all": "همه", "all_categories": "تمام دسته ها", - "topics_marked_as_read.success": "همه موضوع ها خوانده شدند" + "topics_marked_as_read.success": "همه موضوع ها خوانده شدند", + "all-topics": "همه موضوع ها", + "new-topics": "موضوع های جدید", + "watched-topics": "موضوع های پیگیری شده" } \ No newline at end of file diff --git a/public/language/fa_IR/uploads.json b/public/language/fa_IR/uploads.json index 1622cb5693..8cf6622f5b 100644 --- a/public/language/fa_IR/uploads.json +++ b/public/language/fa_IR/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "در حال بارگذاری فایل...", + "select-file-to-upload": "فایل مورد نظر را برای بارگذاری انتخاب کنید!", + "upload-success": "فایل با موفقیت بارگذاری شد!", + "maximum-file-size": "حداکثر %1 کیلوبایت" } \ No newline at end of file diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index 57d8e534b7..520868319b 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -3,17 +3,18 @@ "offline": "آفلاین", "username": "نام کاربری", "joindate": "زمان عضویت", - "postcount": "تعداد پست ها", + "postcount": "تعداد پستها", "email": "رایانامه", "confirm_email": "تأیید ایمیل", - "ban_account": "مسدود کردن حساب کاربری", + "account_info": "اطلاعات شناسه کاربری", + "ban_account": "مسدود کردن", "ban_account_confirm": "از مسدود کردن این کاربر اطمینان دارید؟", "unban_account": "آزاد کردن حساب کاربری", "delete_account": "حذف حساب کاربری", "delete_account_confirm": "آیا مطمئنید که میخواهید حساب کاربری خود را حذف کنید؟ <br /><strong>این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات خود را بازیابی کنید./strong><br /><br /> برای تایید حذف این حساب کاربری، نام کاربری خود را وارد کنید", "delete_this_account_confirm": "آیا مطمئنید که میخواهید این حساب کاربری را حذف کنید؟<br/><strong>این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات را بازیابی کنید.</strong><br/><br/>", "account-deleted": "حساب کاربری پاک شد", - "fullname": "نام و نام خانوادگی", + "fullname": "نام و نامخانوادگی", "website": "تارنما", "location": "محل سکونت", "age": "سن", @@ -22,7 +23,7 @@ "profile": "پروفایل", "profile_views": "بازدیدهای نمایه", "reputation": "اعتبار", - "favourites": "Bookmarks", + "favourites": "علاقمندی ها", "watched": "پیگیری شده", "followers": "دنبالکنندهها", "following": "دنبالشوندهها", @@ -39,7 +40,7 @@ "change_username": "تغییر نام کاربری", "change_email": "تغییر ایمیل", "edit": "ویرایش", - "edit-profile": "Edit Profile", + "edit-profile": "ویرایش پروفایل", "default_picture": "آیکون پیش فرض", "uploaded_picture": "تصویر بارشده", "upload_new_picture": "بارگذاری تصویر تازه", @@ -56,17 +57,17 @@ "password": "گذرواژه", "username_taken_workaround": "نام کاربری درخواستی شما در حال حاضر گرفته شده است، بنابراین ما آن را کمی تغییر دادهایم. شما هماکنون با نام <strong>%1</strong شناخته میشوید.", "password_same_as_username": "کلمه ی عبور شما با نام کاربری شما یکسان می باشند ، لطفا کلمه ی عبور دیگری را انتخاب کنید", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "کلمه ی عبور شما با ایمیل شما یکسان است، لطفا کلمه عبور دیگری را انتخاب کنید.", "upload_picture": "بارگذاری تصویر", "upload_a_picture": "یک تصویر بارگذاری کنید", "remove_uploaded_picture": "پاک کردن عکس بارگذاری شده", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "بارگذاری عکس کاور", "settings": "تنظیمات", - "show_email": "نمایش ایمیلام", + "show_email": "نمایش ایمیلهای من", "show_fullname": "نام کامل من را نشان بده", - "restrict_chats": "قبول پیام فقط ازکاربرانی که من را دنبال میکنند", + "restrict_chats": "قبول پیام فقط ازکاربرانی که آنها را دنبال میکنم", "digest_label": "مشترک شدن در چکیده", - "digest_description": "مشترک شدن برای دریافت تازههی این انجمن (موضوع ها و آکاهسازیهای تازه) با ایمیل روی یک برنامه زمانبندی", + "digest_description": "مشترک شدن برای دریافت جدیدترینهای این انجمن (موضوع ها و آکاهسازیهای تازه) با ایمیل روی یک برنامه زمانبندی", "digest_off": "خاموش", "digest_daily": "روزانه", "digest_weekly": "هفتگی", @@ -79,31 +80,50 @@ "has_no_posts": "این کاربر تا به حال هیچ چیزی ارسال نکرده است.", "has_no_topics": "این کاربر تا به حال هیچ موضوعی ارسال نکرده است", "has_no_watched_topics": "این کاربر تا به حال هیچ موضوعی را پیگیری نکرده است", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "این کاربر به هیچ پستی امتیاز نداده است.", + "has_no_downvoted_posts": "این کاربر به هیچ پستی رای منفی نداده است.", + "has_no_voted_posts": "این کاربر به پست رای نداده است", "email_hidden": "ایمیل پنهان شده", "hidden": "پنهان", "paginate_description": "صفحه بندی و نمایش موضوع ها و پستها به جای نمایش بر اساس اسکرول موس", "topics_per_page": "شمار موضوع ها در هر برگه", "posts_per_page": "شمار پستها در هر برگه", "notification_sounds": "پخش صدا زمانی که یک اطلاعیه دریافت میکنید", + "notifications_and_sounds": "آگاهسازیها و صداها", + "incoming-message-sound": "صدای پیام دریافتی", + "outgoing-message-sound": "صدای پیام ارسال شده", + "notification-sound": "آگاهسازی از طریق صدا", + "no-sound": "بدون صدا", "browsing": "تنظیمات مرور", "open_links_in_new_tab": "پیوندهای به بیرون را در برگ جدید باز کن", "enable_topic_searching": "فعال کردن جستجوی داخل-موضوع", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "تاپیک هایی که پاسخ داده ای را دنبال کن", - "follow_topics_you_create": "موضوع هایی که ایجاد کرده ای را دنبال کن", - "grouptitle": "عنوان گروهی که میخواهید نشان داده شود را انتخاب کنید.", + "delay_image_loading": "تاخیر در حال بارگذاری عکس", + "image_load_delay_help": "اگر فعال باشد، تصاویر در تاپیک ها بارگذاری نمی شود تا زمانی که از طریق اسکرول روی آن بروید قابل مشاهده است", + "scroll_to_my_post": "پس از ارسال پست، اولین پست جدید نشان بده", + "follow_topics_you_reply_to": "موضوعهای که جواب دادید را پیگیری نمائید", + "follow_topics_you_create": "موضوعهای که ایجاد کردید را پیگیری نمائید", + "grouptitle": "عنوان گروه", "no-group-title": "عنوان گروهی نیست", - "select-skin": "Select a Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", + "select-skin": "انتخاب یک پوسته", + "select-homepage": "انتخاب صفحه اصلی", + "homepage": "صفحه اصلی", "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", "custom_route": "Custom Homepage Route", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "آخرین نشانه گذاریها", + "info.no-flags": "هیچ پست نشانهگذاری شده یافت نشد", + "info.ban-history": "تاریخچه مسدودیت اخیر", + "info.no-ban-history": "این کاربر هرگز مسدود نشده است", + "info.banned-until": "مسدود شده تا %1", + "info.banned-permanently": "مسدود شده به طور دائم", + "info.banned-reason-label": "دلیل", + "info.banned-no-reason": "هیچ دلیلی ارایه نشد.", + "info.username-history": "تاریخچه نام کاربری", + "info.email-history": "تاریخچه رایانامه", + "info.moderation-note": "یادداشت میانهروی", + "info.moderation-note.success": "یادداشت میانهروی ذخیره شد" } \ No newline at end of file diff --git a/public/language/fa_IR/users.json b/public/language/fa_IR/users.json index 3cd0087dcf..0e470d9fef 100644 --- a/public/language/fa_IR/users.json +++ b/public/language/fa_IR/users.json @@ -1,20 +1,21 @@ { "latest_users": "آخرین کاربران", - "top_posters": "بهترین فرستندهها", - "most_reputation": "بیشترین اعتبار", + "top_posters": "برترین فرستندهها", + "most_reputation": "بیشترین اعتبار", + "most_flags": "بیشترین پرچمها", "search": "جستجو", "enter_username": "یک نام کاربری برای جستجو وارد کنید", "load_more": "بارگذاری بیشتر", - "users-found-search-took": "%1 کاربر(ها) یافت شد! جستجو %2 ثانیه طولید", + "users-found-search-took": "%1 کاربر(ها) یافت شد! جستجو %2 ثانیه زمان گرفت.", "filter-by": "فیلتر با", "online-only": "فقط آنلاین", "invite": "دعوت", - "invitation-email-sent": "ایمیل ی دعوتنامه به %1 ارسال شد", + "invitation-email-sent": "رایانامه دعوتنامه برای %1 ارسال شد", "user_list": "فهرست کاربران", - "recent_topics": "موضوع های اخیر", - "popular_topics": "موضوع های پربازدید", - "unread_topics": "موضوع های خوانده نشده", - "categories": "دسته ها", + "recent_topics": "موضوعهای اخیر", + "popular_topics": "موضوعهای پربازدید", + "unread_topics": "موضوعهای خوانده نشده", + "categories": "دستهبندیها", "tags": "برچسبها", - "no-users-found": "No users found!" + "no-users-found": "کاربری پیدا نشد!" } \ No newline at end of file diff --git a/public/language/fi/category.json b/public/language/fi/category.json index 0083f12e8b..a328811300 100644 --- a/public/language/fi/category.json +++ b/public/language/fi/category.json @@ -10,7 +10,11 @@ "share_this_category": "Jaa tämä kategoria", "watch": "Watch", "ignore": "Sivuuta", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/fi/email.json b/public/language/fi/email.json index 92002dae56..105dc065a1 100644 --- a/public/language/fi/email.json +++ b/public/language/fi/email.json @@ -24,6 +24,7 @@ "digest.day": "päivä", "digest.week": "viikko", "digest.month": "kuukausi", + "digest.subject": "Digest for %1", "notif.chat.subject": "Uusi chatviesti henkilöltä %1", "notif.chat.cta": "Click here to continue the conversation", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index dbb4fd10c2..90ade01717 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -14,20 +14,25 @@ "invalid-password": "Virheellinen salasana", "invalid-username-or-password": "Ole hyvä ja anna sekä käyttäjänimi että salasana", "invalid-search-term": "Virheellinen hakutermi", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Käyttäjänimi varattu", "email-taken": "Sähköpostiosoite varattu", "email-not-confirmed": "Sähköpostiasi ei ole vielä vahvistettu, ole hyvä ja napsauta tätä vahvistaaksesi sen.", "email-not-confirmed-chat": "Et voi keskustella ennen kuin sähköpostiosoitteesi on vahvistettu, ole hyvä ja paina tästä vahvistaaksesi sen.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Käyttäjänimi on liian lyhyt", "username-too-long": "Käyttäjänimi on liian pitkä", "password-too-long": "Password too long", "user-banned": "Käyttäjä on estetty", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Anteeksi, mutta sinun täytyy odottaa %1 sekunti(a) ennen sinun ensimmäisen viestin lähettämistä", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategoriaa ei ole olemassa", "no-topic": "Aihetta ei ole olemassa", "no-post": "Viestiä ei ole olemassa", @@ -38,10 +43,26 @@ "category-disabled": "Kategoria ei ole käytössä", "topic-locked": "Aihe lukittu", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Ole hyvä ja syötä pidempi viesti. Sen pitäisi sisältää ainakin %1 merkki(ä).", "content-too-long": "Ole hyvä ja syötä lyhyempi viesti. Sen voi sisältää vain %1 merkki(ä).", "title-too-short": "Ole hyä ja syötä pidempi otsikko. Sen pitäisi sisältää anakin %1 merkki(ä).", "title-too-long": "Ole hyvä ja syötä lyhyempi otsikko. Se voi sisältää vain %1 merkki(ä).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Et voi estää muita ylläpitäjiä!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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": "Ryhmän nimi on liian lyhyt", + "group-name-too-long": "Group name too long", "group-already-exists": "Ryhmä on jo olemassa", "group-name-change-not-allowed": "Et voi vaihtaa ryhmän nimeä", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "Käyttäjä ei ole huoneessa", "no-users-in-room": "Ei käyttäjiä tässä huoneessa", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/fi/global.json b/public/language/fi/global.json index 94d68b78fb..adea62f9c0 100644 --- a/public/language/fi/global.json +++ b/public/language/fi/global.json @@ -7,8 +7,10 @@ "403.login": "Sinun pitäisi kai <a href='%1/login'>kirjautua sisään</a>?", "404.title": "Ei löydy", "404.message": "Olet päätynyt sivulle, jota ei ole olemassa. Palaa <a href='%1/'>etusivulle</a>.", - "500.title": "Sisäinen virhe.", + "500.title": "Internal Error.", "500.message": "Oho! Jotain meni pieleen!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Rekisteröidy", "login": "Kirjaudu", "please_log_in": "Kirjaudu, ole hyvä", @@ -17,6 +19,7 @@ "welcome_back": "Tervetuloa takaisin", "you_have_successfully_logged_in": "Olet onnistuneesti kirjautunut sisään", "save_changes": "Tallenna muutokset", + "save": "Save", "close": "Sulje", "pagination": "Sivutus", "pagination.out_of": "%1/%2", @@ -50,7 +53,9 @@ "topics": "Aiheet", "posts": "Viestit", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Katsottu", "reputation": "Maine", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/fi/groups.json b/public/language/fi/groups.json index 55e7b41f2e..b9625fb64a 100644 --- a/public/language/fi/groups.json +++ b/public/language/fi/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/fi/login.json b/public/language/fi/login.json index 3dc14792ee..7631447d06 100644 --- a/public/language/fi/login.json +++ b/public/language/fi/login.json @@ -5,7 +5,8 @@ "remember_me": "Muista minut?", "forgot_password": "Unohditko salasanasi?", "alternative_logins": "Vaihtoehtoiset kirjautumistavat", - "failed_login_attempt": "Kirjautumisyritys epäonnistui, ole hyvä ja yritä uudestaan.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Olet onnistuneesti kirjautunut sisään!", - "dont_have_account": "Ei käyttäjätunnusta?" + "dont_have_account": "Ei käyttäjätunnusta?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json index 978d306170..2038bdfd96 100644 --- a/public/language/fi/modules.json +++ b/public/language/fi/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json index 427e04311e..b10e43729c 100644 --- a/public/language/fi/notifications.json +++ b/public/language/fi/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/fi/pages.json b/public/language/fi/pages.json index d2eba81a6c..5e8336f28d 100644 --- a/public/language/fi/pages.json +++ b/public/language/fi/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Ilmoitukset", "tags": "Tunnisteet", "tag": "Topics tagged under \"%1\"", "register": "Luo käyttäjät", + "registration-complete": "Registration complete", "login": "Kirjaudu käyttäjällesi", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "Ihmiset, jota %1 seuraa", "account/followers": "Ihmiset, jotka seuraavat %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/fi/register.json b/public/language/fi/register.json index f7d7ab7e08..f0b78c94a1 100644 --- a/public/language/fi/register.json +++ b/public/language/fi/register.json @@ -1,5 +1,6 @@ { "register": "Rekisteröidy", + "cancel_registration": "Cancel Registration", "help.email": "Oletuksena sähköpostiosoitettasi ei näytetä muille.", "help.username_restrictions": "Yksilöllisen käyttäjätunnuksen pitää olla %1-%2 merkkiä pitkä. Toiset voivat mainita sinut @<span id='yourUsername'>username</span>.", "help.minimum_password_length": "Salasanasi pitää olla vähintään %1 merkin mittainen.", @@ -15,5 +16,8 @@ "alternative_registration": "Vaihtoehtoiset rekisteröitymistavat", "terms_of_use": "Käyttöehdot", "agree_to_terms_of_use": "Hyväksyn käyttöehdot", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index 37b9b98e80..18950d0fda 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -26,12 +26,30 @@ "tools": "Työkalut", "flag": "Ilmianna", "locked": "Lukittu", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Ilmianna tämä viesti moderaattoreille", "flag_success": "Tämä viesti ilmiannettiin moderaattoreille.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Tämä aihe on poistettu. Vain käyttäjät, joilla on aiheen hallintaoikeudet, voivat nähdä sen.", "following_topic.message": "Saat nyt ilmoituksen, kun joku kirjoittaa tähän aiheeseen.", - "not_following_topic.message": "Et saa enää ilmoituksia tästä aiheesta.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Ole hyvä ja rekisteröidy tai kirjaudu sisään tilataksesi tämän aiheen.", "markAsUnreadForAll.success": "Aihe merkitty lukemattomaksi kaikille.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Ilmoita, kun tähän keskusteluun tulee uusia viestejä", "unwatch.title": "Lopeta tämän aiheen seuraaminen", "share_this_post": "Jaa tämä viesti", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Aiheen työkalut", - "thread_tools.markAsUnreadForAll": "Merkitse lukemattomaksi", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Kiinnitä aihe", "thread_tools.unpin": "Poista aiheen kiinnitys", "thread_tools.lock": "Lukitse aihe", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Tämä keskustelu siirretään aihealueelle", "fork_topic_instruction": "Napsauta viestejä, jotka haluat haaroittaa", "fork_no_pids": "Ei valittuja viestejä!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Syötä aiheesi otsikko tähän...", diff --git a/public/language/fi/unread.json b/public/language/fi/unread.json index 5588fce2c3..e7296b4cf8 100644 --- a/public/language/fi/unread.json +++ b/public/language/fi/unread.json @@ -6,5 +6,8 @@ "selected": "Valitut", "all": "Kaikki", "all_categories": "All categories", - "topics_marked_as_read.success": "Aihe merkitty luetuksi!" + "topics_marked_as_read.success": "Aihe merkitty luetuksi!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/fi/user.json b/public/language/fi/user.json index f7218528a3..f1344be78f 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -6,6 +6,7 @@ "postcount": "Viestien määrä", "email": "Sähköposti", "confirm_email": "Vahvista sähköpostiosoite", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Aihetta per sivu", "posts_per_page": "Viestiä per sivu", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Selataan asetuksia", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Salli aiheen sisäiset haut", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/fi/users.json b/public/language/fi/users.json index 9ebaf393e1..2a01aed651 100644 --- a/public/language/fi/users.json +++ b/public/language/fi/users.json @@ -2,6 +2,7 @@ "latest_users": "Viimeisimmät käyttäjät", "top_posters": "Aktiivisimmat viestittelijät", "most_reputation": "Eniten mainetta", + "most_flags": "Most Flags", "search": "Hae", "enter_username": "Syötä käyttäjätunnus hakeaksesi", "load_more": "Lataa lisää", diff --git a/public/language/fr/category.json b/public/language/fr/category.json index 785b6af048..5c70da5593 100644 --- a/public/language/fr/category.json +++ b/public/language/fr/category.json @@ -8,9 +8,13 @@ "no_replies": "Personne n'a répondu", "no_new_posts": "Pas de nouveau message", "share_this_category": "Partager cette catégorie", - "watch": "Surveiller", + "watch": "S'abonner", "ignore": "Ne plus surveiller", - "watch.message": "Vous surveillez désormais les mises à jour de cette catégorie.", - "ignore.message": "Vous ne surveillez plus les mises jour de cette catégorie.", - "watched-categories": "Catégories Suivies" + "watching": "Abonné", + "ignoring": "Ignoré", + "watching.description": "Affiche les sujets dans la section Non lus", + "ignoring.description": "N'affiche pas les sujets dans la section Non lus", + "watch.message": "Vous êtes désormais abonné aux mises à jour de cette catégorie et de ses sous-catégories.", + "ignore.message": "Vous n'êtes plus abonné aux mises à jour de cette catégorie et de ses sous-catégories.", + "watched-categories": "Abonnements" } \ No newline at end of file diff --git a/public/language/fr/email.json b/public/language/fr/email.json index 5de03234d8..b990e55c58 100644 --- a/public/language/fr/email.json +++ b/public/language/fr/email.json @@ -24,6 +24,7 @@ "digest.day": "jour", "digest.week": "semaine", "digest.month": "mois", + "digest.subject": "Résumé de l'actualité de %1", "notif.chat.subject": "Nouveau message de chat reçu de %1", "notif.chat.cta": "Cliquez ici pour continuer la conversation", "notif.chat.unsub.info": "Cette notification de chat a été envoyé en raison de vos paramètres d'abonnement.", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index d80c43277e..a71e59534c 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -14,20 +14,25 @@ "invalid-password": "Mot de passe invalide", "invalid-username-or-password": "Veuillez entrer un nom d'utilisateur et un mot de passe", "invalid-search-term": "Données de recherche invalides", + "csrf-invalid": "Nous ne pouvons pas vous connectez, possiblement car votre session a expiré. Merci de réessayer.", "invalid-pagination-value": "Valeur de pagination invalide. Celle-ci doit être comprise entre %1 et %2.", "username-taken": "Nom d’utilisateur déjà utilisé", "email-taken": "Email déjà utilisé", "email-not-confirmed": "Votre adresse email n'est pas confirmée, cliquez ici pour la valider.", "email-not-confirmed-chat": "Il ne vous est pas possible d'utiliser le chat tant que votre adresse email n'a pas été vérifiée. Veuillez cliquer ici pour confirmer votre adresse email.", + "email-not-confirmed-email-sent": "Votre adresse email n'a pas encore été confirmée. Merci de vérifier l'email de confirmation dans votre boîte de reception.", "no-email-to-confirm": "Ce forum requiert une vérification de votre adresse email. Veuillez cliquer ici pour entrer une adresse.", "email-confirm-failed": "Votre adresse email n'a pas pu être vérifiée. Veuillez ré-essayer plus tard.", "confirm-email-already-sent": "L'email de confirmation a déjà été envoyé. Veuillez attendre %1 minute(s) avant de redemander un nouvel envoi.", + "sendmail-not-found": "L'application d'envoi de mail est introuvable, assurez-vous qu'elle est installée et que l'utilisateur servant à démarrer NodeBB ait des droits suffisants.", "username-too-short": "Nom d'utilisateur trop court", "username-too-long": "Nom d'utilisateur trop long", "password-too-long": "Mot de passe trop long", "user-banned": "Utilisateur banni", + "user-banned-reason": "Utilisateur banni (Raison : %1)", "user-too-new": "Désolé, vous devez attendre encore %1 seconde(s) avant d'envoyer votre premier message", "blacklisted-ip": "Désolé, votre adresse IP a été bannie de cette communauté. Si vous pensez que c'est une erreur, veuillez contacter un administrateur.", + "ban-expiry-missing": "Veuillez entrer une date de fin de banissement.", "no-category": "Cette catégorie n'existe pas", "no-topic": "Ce sujet n'existe pas", "no-post": "Ce message n'existe pas", @@ -37,11 +42,27 @@ "no-privileges": "Vous n'avez pas les privilèges nécessaires pour effectuer cette action.", "category-disabled": "Catégorie désactivée", "topic-locked": "Sujet verrouillé", - "post-edit-duration-expired": "Vous ne pouvez modifier un message que %1 seconde(s) après l'avoir posté.", + "post-edit-duration-expired": "Vous ne pouvez modifier un message que pendant %1 seconde(s) après l'avoir posté.", + "post-edit-duration-expired-minutes": "Vous ne pouvez éditer un message que pendant %1 minute(s) après l'avoir posté.", + "post-edit-duration-expired-minutes-seconds": "Vous ne pouvez éditer un message que pendant %1 minute(s) et %2 seconde(s) après l'avoir posté.", + "post-edit-duration-expired-hours": "Vous ne pouvez éditer un message que pendant %1 heure(s) après l'avoir posté.", + "post-edit-duration-expired-hours-minutes": "Vous ne pouvez éditer un message que pendant %1 heure(s) et %2 minute(s) après l'avoir posté.", + "post-edit-duration-expired-days": "Vous ne pouvez éditer un message que pendant %1 jours(s) après l'avoir posté.", + "post-edit-duration-expired-days-hours": "Vous ne pouvez éditer un message que pendant %1 jours(s) et %2 heures(s) après l'avoir posté.", + "post-delete-duration-expired": "Vous ne pouvez supprimer un message que pendant %1 seconde(s) après l'avoir posté.", + "post-delete-duration-expired-minutes": "Vous ne pouvez supprimer un message que pendant %1 minute(s) après l'avoir posté.", + "post-delete-duration-expired-minutes-seconds": "Vous ne pouvez supprimer un message que pendant %1 minute(s) et %2 seconde(s) après l'avoir posté.", + "post-delete-duration-expired-hours": "Vous ne pouvez supprimer un message que pendant %1 heure(s) après l'avoir posté.", + "post-delete-duration-expired-hours-minutes": "Vous ne pouvez supprimer un message que pendant %1 heure(s) et %2 minute(s) après l'avoir posté.", + "post-delete-duration-expired-days": "Vous ne pouvez supprimer un message que pendant %1 jour(s) après l'avoir posté.", + "post-delete-duration-expired-days-hours": "Vous ne pouvez supprimer un message que pendant %1 jour(s) et %2 heure(s) après l'avoir posté.", + "cant-delete-topic-has-reply": "Vous ne pouvez pas supprimer votre sujet s'il a au moins une réponse.", + "cant-delete-topic-has-replies": "Vous ne pouvez pas supprimer votre sujet s'il a au moins %1 réponses.", "content-too-short": "Veuillez entrer un message plus long. %1 caractère(s) minimum.", "content-too-long": "Veuillez poster un message plus cours. Les messages ne peuvent être plus long que %1 caractère(s).", "title-too-short": "Veuillez entrer un titre plus long. %1 caractère(s) minimum.", "title-too-long": "Veuillez entrer un titre plus court. Les titres ne peuvent excéder %1 caractère(s).", + "category-not-selected": "Aucune catégorie sélectionnée", "too-many-posts": "Vous ne pouvez poster que toutes les %1 seconde(s).", "too-many-posts-newbie": "En tant que nouvel utilisateur, vous ne pouvez poster que toutes les %1 seconde(s) jusqu'à ce que vous obteniez une réputation de %2 - patientez avant de publier de nouveau.", "tag-too-short": "Veuillez entrer un mot-clé plus long. Les mots-clés doivent contenir au moins %1 caractère(s).", @@ -55,10 +76,12 @@ "already-unfavourited": "Vous avez déjà retiré un marque-page", "cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !", "cant-remove-last-admin": "Vous seul êtes administrateur. Ajouter un autre utilisateur en tant qu'administrateur avant de vous en retirer.", + "cant-delete-admin": "Veuillez retirer les droits d'administration de ce compte avant de tenter de le supprimer.", "invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1", "invalid-image-extension": "Extension d'image invalide", "invalid-file-type": "Type de fichier non valide. Les types autorisés sont : %1", "group-name-too-short": "Nom de groupe trop court", + "group-name-too-long": "Nom du groupe trop long", "group-already-exists": "Ce groupe existe déjà", "group-name-change-not-allowed": "Modification du nom de groupe non permise", "group-already-member": "Déjà membre du groupe", @@ -77,7 +100,7 @@ "signature-too-long": "La signature ne peut dépasser %1 caractère(s).", "about-me-too-long": "Votre texte \"à propos de moi\" ne peut dépasser %1 caractère(s).", "cant-chat-with-yourself": "Vous ne pouvez chatter avec vous même !", - "chat-restricted": "Cet utilisateur a restreint les ses messages de chat. Il doit d'abord vous suivre avant de pouvoir discuter avec lui.", + "chat-restricted": "Cet utilisateur a restreint les ses messages de chat. Il doit d'abord s'abonner à votre compte avant que vous puissiez discuter avec lui.", "chat-disabled": "Système de chat désactivé", "too-many-messages": "Vous avez envoyé trop de messages, veuillez patienter un instant.", "invalid-chat-message": "Message de Chat invalide", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Vous n'avez pas l'autorisation de modifier ce message", "cant-remove-last-user": "Vous ne pouvez pas supprimer le dernier utilisateur", "cant-delete-chat-message": "Vous n'avez pas l'autorisation de supprimer ce message", + "already-voting-for-this-post": "Vous avez déjà voté pour ce message.", "reputation-system-disabled": "Le système de réputation est désactivé", "downvoting-disabled": "Les votes négatifs ne sont pas autorisés", "not-enough-reputation-to-downvote": "Vous n'avez pas une réputation assez élevée pour noter négativement ce message", @@ -99,5 +123,9 @@ "no-session-found": "Pas de session de connexion trouvé!", "not-in-room": "L'utilisateur n'est pas dans cette salle", "no-users-in-room": "Aucun utilisateur dans cette salle", - "cant-kick-self": "Vous ne pouvez pas vous exclure vous-même du groupe" + "cant-kick-self": "Vous ne pouvez pas vous exclure vous-même du groupe", + "no-users-selected": "Aucun utilisateur sélectionné", + "invalid-home-page-route": "Route de page d'accueil invalide", + "invalid-session": "Session Interrompue", + "invalid-session-text": "Il semble que votre session ne soit plus active, ou que le serveur ne la reconnaisse plus. Merci de rafraichir cette page." } \ No newline at end of file diff --git a/public/language/fr/global.json b/public/language/fr/global.json index 7096b63f7c..05dfc319fc 100644 --- a/public/language/fr/global.json +++ b/public/language/fr/global.json @@ -7,8 +7,10 @@ "403.login": "Peut-être deviez vous <a href='%1/login'>essayer de vous connecter</a>?", "404.title": "Introuvable", "404.message": "Il semble que vous ayez atteint une page qui n'existe pas. Retourner à la <a href='%1/'>page d'accueil</a>.", - "500.title": "Erreur interne.", + "500.title": "Erreur Interne.", "500.message": "Oops ! Il semblerait que quelque chose se soit mal passé !", + "400.title": "Requête erronée.", + "400.message": "Il semble que ce lien ne soit pas correct, merci de le vérifier. Sinon, retournez à la <a href='%1/'>page d'accueil</a>.", "register": "S'inscrire", "login": "Se connecter", "please_log_in": "Veuillez vous connecter", @@ -17,6 +19,7 @@ "welcome_back": "Bienvenue", "you_have_successfully_logged_in": "Vous vous êtes bien connecté", "save_changes": "Enregistrer les changements", + "save": "Enregistrer", "close": "Fermer", "pagination": "Pagination", "pagination.out_of": "%1 sur %2", @@ -24,7 +27,7 @@ "header.admin": "Admin", "header.categories": "Catégories", "header.recent": "Récent", - "header.unread": "Non lu", + "header.unread": "Non lus", "header.tags": "Mots-clés", "header.popular": "Populaire", "header.users": "Utilisateurs", @@ -41,16 +44,18 @@ "nextpage": "Page suivante", "alert.success": "Succès", "alert.error": "Erreur", - "alert.banned": "Banni", + "alert.banned": "Bannis", "alert.banned.message": "Vous venez d'être banni, vous allez maintenant être déconnecté.", - "alert.unfollow": "Vous ne suivez désormais plus %1 !", - "alert.follow": "Vous suivez désormais %1 !", + "alert.unfollow": "Vous n'êtes plus abonné à %1 !", + "alert.follow": "Vous êtes désormais abonné à %1 !", "online": "En ligne", "users": "Utilisateurs", "topics": "Sujets", "posts": "Messages", "best": "Meilleurs", + "upvoters": "Votes pour", "upvoted": "Votes pour", + "downvoters": "Votes contre", "downvoted": "Votes contre", "views": "Vues", "reputation": "Réputation", @@ -81,8 +86,8 @@ "updated.title": "Forum mis à jour", "updated.message": "Ce forum a été mis à jour à la dernière version. Cliquez ici pour recharger la page.", "privacy": "Vie privée", - "follow": "Suivre", - "unfollow": "Ne plus suivre", + "follow": "S'abonner", + "unfollow": "Se désabonner", "delete_all": "Tout supprimer", "map": "Carte", "sessions": "Sessions de connexion", @@ -90,5 +95,8 @@ "enter_page_number": "Entrer un numéro de page", "upload_file": "Envoyer un fichier", "upload": "Envoyer", - "allowed-file-types": "Les types de fichiers autorisés sont : %1" + "allowed-file-types": "Les types de fichiers autorisés sont : %1", + "unsaved-changes": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir naviguer tout de même ?", + "reconnecting-message": "Il semble que votre connexion ait été perdue, veuillez patienter pendant que nous vous re-connectons.", + "play": "Lire" } \ No newline at end of file diff --git a/public/language/fr/groups.json b/public/language/fr/groups.json index 65a5274979..48f1ad2b2a 100644 --- a/public/language/fr/groups.json +++ b/public/language/fr/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Quitter le groupe", "membership.reject": "Refuser", "new-group.group_name": "Nom du groupe :", - "upload-group-cover": "Envoyer une image de groupe" + "upload-group-cover": "Envoyer une image de groupe", + "bulk-invite-instructions": "Entrez une liste de nom d'utilisateurs séparés par des virgules pour les inviter à rejoindre ce groupe.", + "bulk-invite": "Invitation multiple" } \ No newline at end of file diff --git a/public/language/fr/login.json b/public/language/fr/login.json index 6470717691..72fa0ef401 100644 --- a/public/language/fr/login.json +++ b/public/language/fr/login.json @@ -5,7 +5,8 @@ "remember_me": "Se souvenir de moi ?", "forgot_password": "Mot de passe oublié ?", "alternative_logins": "Autres méthodes de connexion", - "failed_login_attempt": "Echèc d'authentification, veuillez réessayer.", + "failed_login_attempt": "Identification échouée", "login_successful": "Vous êtes maintenant connecté !", - "dont_have_account": "Vous n'avez pas de compte ?" + "dont_have_account": "Vous n'avez pas de compte ?", + "logged-out-due-to-inactivity": "Vous avez été déconnecté du Panneau de Contrôle d'Administration en raison de votre inactivité" } \ No newline at end of file diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 33fd5fe494..7cbdc0ebc6 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Envoyer et verrouiller", "composer.toggle_dropdown": "Afficher/masquer le menu", "composer.uploading": "Envoi en cours %1", + "composer.formatting.bold": "Gras", + "composer.formatting.italic": "Italique", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Barré", + "composer.formatting.link": "Lien", + "composer.formatting.picture": "Image", + "composer.upload-picture": "Envoyer une image", + "composer.upload-file": "Envoyer un fichier", + "composer.zen_mode": "Mode Zen", + "composer.select_category": "Sélectionnez une catégorie", "bootbox.ok": "OK", "bootbox.cancel": "Annuler", "bootbox.confirm": "Confirmer", diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json index fadee3aa62..e13aba7ee4 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> et %2 autres on voté pour votre message dans <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> a déplacé votre message vers <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> a déplacé <strong>%2</strong>.", - "favourited_your_post_in": "<strong>%1</strong> a enregistré votre message dans <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> et <strong>%2</strong> autres ont enregistré votre message dans <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> et %2 autres ont enregistré votre message dans <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> a signalé un message dans <strong>%2</strong>.", "user_flagged_post_in_dual": "<strong>%1</strong> et <strong>%2</strong> ont signalé un message dans <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> et %2 autres on signalé un message dans <strong>%3</strong>", @@ -27,8 +24,8 @@ "user_posted_to_multiple": "<strong>%1</strong> et %2 autres ont posté une réponse à : <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> a posté un nouveau sujet: <strong>%2</strong>.", "user_started_following_you": "<strong>%1</strong> vous suit.", - "user_started_following_you_dual": "<strong>%1</strong> et <strong>%2</strong> vous suivent.", - "user_started_following_you_multiple": "<strong>%1</strong> et %2 autres vous suivent.", + "user_started_following_you_dual": "<strong>%1</strong> et <strong>%2</strong> se sont abonnés à votre compte.", + "user_started_following_you_multiple": "<strong>%1</strong> et %2 autres se sont abonnés à votre compte.", "new_register": "<strong>%1</strong> a envoyé une demande d'incription.", "new_register_multiple": "<strong>%1</strong> inscription(s) est en attente de validation.", "email-confirmed": "Email vérifié", diff --git a/public/language/fr/pages.json b/public/language/fr/pages.json index 3c0cb29b90..0191aed5ea 100644 --- a/public/language/fr/pages.json +++ b/public/language/fr/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Utilisateurs avec le plus de messages", "users/sort-reputation": "Utilisateurs avec la plus grande réputation", "users/banned": "Utilisateurs bannis", + "users/most-flags": "Utilisateurs les plus souvent signalés", "users/search": "Rechercher des utilisateurs", "notifications": "Notifications", "tags": "Mots-clés", "tag": "Sujets ayant le mot-clé \"%1\"", "register": "Créer un compte", + "registration-complete": "Inscription terminée", "login": "Connectez-vous à votre compte", "reset": "Remettez à zéro votre mot de passe", "categories": "Catégories", @@ -24,23 +26,24 @@ "group": "%1 groupe", "chats": "Discussions", "chat": "Conversation avec %1", - "account/edit": "Edition de \"%1\"", + "account/edit": "Édition de \"%1\"", "account/edit/password": "Édition du mot de passe de \"%1\"", "account/edit/username": "Édition du nom d'utilisateur de \"%1\"", "account/edit/email": "Édition de l'e-mail de \"%1\"", - "account/following": "Les personnes que %1 suit", - "account/followers": "Les personnes qui suivent %1", + "account/info": "Informations du compte", + "account/following": "Les personnes auxquelles %1 est abonné", + "account/followers": "Les personnes abonnées à %1", "account/posts": "Messages postés par %1", "account/topics": "Sujets créés par %1", "account/groups": "Groupes auxquels appartient %1", - "account/favourites": "Marques-pages de %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Paramètres d'utilisateur", - "account/watched": "Sujets surveillés par %1", + "account/watched": "Sujets auxquels %1 est abonné", "account/upvoted": "Messages pour lesquels %1 a voté", "account/downvoted": "Messages contre lesquels %1 a voté", "account/best": "Meilleurs messages postés par %1", "confirm": "Email vérifié", "maintenance.text": "%1 est en maintenance. Veuillez revenir un peu plus tard.", - "maintenance.messageIntro": "De plus, l'administrateur a laissé ce message:", + "maintenance.messageIntro": "De plus, l'administrateur a laissé ce message :", "throttled.text": "%1 est actuellement indisponible en raison d'une charge excessive. Merci de réessayer plus tard." } \ No newline at end of file diff --git a/public/language/fr/register.json b/public/language/fr/register.json index 82a8eb27f5..e95bab6ad3 100644 --- a/public/language/fr/register.json +++ b/public/language/fr/register.json @@ -1,5 +1,6 @@ { "register": "S'inscrire", + "cancel_registration": "Annuler l'inscription", "help.email": "Par défaut, votre email est cachée du public.", "help.username_restrictions": "Un nom d'utilisateur est unique et a entre %1 et %2 charactères. Les autres utilisateurs peuvent vous mentionner avec @<span id='yourUsername'>nom_d'utilisateur</span>.", "help.minimum_password_length": "Votre mot de passe doit avoir au moins %1 charactères.", @@ -15,5 +16,8 @@ "alternative_registration": "Autres méthodes d'inscription", "terms_of_use": "Conditions d'utilisation", "agree_to_terms_of_use": "J'accepte les Conditions d'utilisation", - "registration-added-to-queue": "Votre inscription a été ajoutée à la liste d'approbation. Vous recevrez un email quand celle-ci sera acceptée par un administrateur." + "terms_of_use_error": "Vous devez accepter les conditions d'utilisation", + "registration-added-to-queue": "Votre inscription a été ajoutée à la liste d'approbation. Vous recevrez un email quand celle-ci sera acceptée par un administrateur.", + "interstitial.intro": "Nous avons besoin de quelques informations supplémentaires avant de pouvoir créer votre compte.", + "interstitial.errors-found": "Nous n'avons pas pu compléter votre inscription :" } \ No newline at end of file diff --git a/public/language/fr/search.json b/public/language/fr/search.json index cfbabc6dd0..b371aa3097 100644 --- a/public/language/fr/search.json +++ b/public/language/fr/search.json @@ -7,7 +7,7 @@ "titles-posts": "Titres et Messages", "posted-by": "Posté par", "in-categories": "Dans les catégories", - "search-child-categories": "Chercher les sous catégories", + "search-child-categories": "Rechercher également dans les sous catégories", "reply-count": "Nombre de réponses", "at-least": "Au moins", "at-most": "Au plus", diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index 8a71ff511a..06c08bd13e 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -26,23 +26,47 @@ "tools": "Outils", "flag": "Signaler", "locked": "Verrouillé", - "bookmark_instructions": "Cliquez ici pour retourner au dernier message non lu dans ce fil de discussion.", + "pinned": "Épinglé", + "moved": "Déplacé", + "bookmark_instructions": "Cliquez ici pour retourner au dernier message lu de ce fil.", "flag_title": "Signaler ce message à la modération", "flag_success": "Ce message a bien été signalé aux modérateurs.", + "flag_manage_title": "Post signalé dans %1", + "flag_manage_history": "Historique des actions", + "flag_manage_no_history": "Aucun historique d'évènement à reporter", + "flag_manage_assignee": "Assigné à", + "flag_manage_state": "État", + "flag_manage_state_open": "Nouveau/Ouvert", + "flag_manage_state_wip": "En cours", + "flag_manage_state_resolved": "Résolu", + "flag_manage_state_rejected": "Rejeté", + "flag_manage_notes": "Notes partagées", + "flag_manage_update": "Mettre à jour l'état du signalement", + "flag_manage_history_assignee": "Assigné à %1", + "flag_manage_history_state": "État mis à jour à %1", + "flag_manage_history_notes": "Notes du signalement à jour", + "flag_manage_saved": "Détails du signalement à jour", "deleted_message": "Ce sujet a été supprimé. Seuls les utilisateurs avec les droits d'administration peuvent le voir.", "following_topic.message": "Vous recevrez désormais des notifications lorsque quelqu'un postera dans ce sujet.", - "not_following_topic.message": "Vous ne recevrez plus de notifications pour ce sujet.", + "not_following_topic.message": "Vous verrez ce sujet dans la liste des sujets non-lus, mais vous ne recevrez pas de notification lorsque quelqu'un postera dans ce sujet.", + "ignoring_topic.message": "Vous ne verrez plus ce sujet dans la liste des sujets non lus. Vous serez notifié lorsque vous serez mentionné ou que quelqu'un votera pour votre message.", "login_to_subscribe": "Veuillez vous enregistrer ou vous connecter afin de vous abonner à ce sujet.", "markAsUnreadForAll.success": "Sujet marqué comme non lu pour tout le monde.", "mark_unread": "Marquer comme non-lu", "mark_unread.success": "Sujet marqué comme non lu.", - "watch": "Surveiller", - "unwatch": "Ne plus surveiller", + "watch": "S'abonner", + "unwatch": "Se désabonner", "watch.title": "Être notifié des nouvelles réponses dans ce sujet", - "unwatch.title": "Arrêter de surveiller ce sujet", + "unwatch.title": "Se désabonner de ce sujet", "share_this_post": "Partager ce message", + "watching": "Abonné", + "not-watching": "Non abonné", + "ignoring": "Ignoré", + "watching.description": "Me notifier les nouvelles réponses.<br/>Afficher le sujet dans l'onglet \"Non lu\".", + "not-watching.description": "Ne pas me notifier les nouvelles réponses.<br/>Afficher le sujet dans l'onglet \"Non lu\" si la catégorie n'est pas ignorée.", + "ignoring.description": "Ne pas me notifier les nouvelle réponses.<br/>Ne pas afficher ce sujet dans l'onglet \"Non lu\".", "thread_tools.title": "Outils pour sujets", - "thread_tools.markAsUnreadForAll": "Marquer comme non lu", + "thread_tools.markAsUnreadForAll": "Marquer tous comme non-lus", "thread_tools.pin": "Épingler le sujet", "thread_tools.unpin": "Désépingler le sujet", "thread_tools.lock": "Verrouiller le sujet", @@ -77,6 +101,7 @@ "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_pid_count": "%1 message(s) sélectionné(s)", "fork_success": "Sujet copié avec succès ! Cliquez ici pour aller au sujet copié.", "delete_posts_instruction": "Sélectionnez les messages que vous souhaitez supprimer/vider", "composer.title_placeholder": "Entrer le titre du sujet ici…", diff --git a/public/language/fr/unread.json b/public/language/fr/unread.json index 9432bf9d36..dae04c99b9 100644 --- a/public/language/fr/unread.json +++ b/public/language/fr/unread.json @@ -6,5 +6,8 @@ "selected": "Sélectionnés", "all": "Tous", "all_categories": "Toutes Catégories", - "topics_marked_as_read.success": "Sujets marqués comme lus !" + "topics_marked_as_read.success": "Sujets marqués comme lus !", + "all-topics": "Tous les sujets", + "new-topics": "Nouveau sujet", + "watched-topics": "Abonnements" } \ No newline at end of file diff --git a/public/language/fr/user.json b/public/language/fr/user.json index 0a6450fdd3..6379677d76 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -6,6 +6,7 @@ "postcount": "Nombre de messages", "email": "Email", "confirm_email": "Confirmer l'adresse email", + "account_info": "Informations du compte", "ban_account": "Bannir", "ban_account_confirm": "Êtes-vous sûr de bien vouloir bannir cet utilisateur ?", "unban_account": "Restaurer le Compte", @@ -23,7 +24,7 @@ "profile_views": "Vues", "reputation": "Réputation", "favourites": "Marque-pages", - "watched": "Suivis", + "watched": "Abonnements", "followers": "Abonnés", "following": "Abonnements", "aboutme": "À propos de moi", @@ -64,8 +65,8 @@ "settings": "Paramètres", "show_email": "Afficher mon email", "show_fullname": "Afficher mon nom complet", - "restrict_chats": "Autoriser la réception de messages provenant uniquement des personnes que je suis", - "digest_label": "S’inscrire aux comptes-rendus", + "restrict_chats": "Autoriser la réception de messages ne provenant que des personnes auxquelles je suis abonné", + "digest_label": "S’inscrire aux résumés de l'actualité du forum", "digest_description": "S'abonner par email aux mises à jours de ce forum (nouvelles notifications et nouveaux sujets) selon le planning sélectionné.", "digest_off": "Désactivé", "digest_daily": "Quotidien", @@ -74,11 +75,11 @@ "send_chat_notifications": "Envoyer un e-mail si un nouveau message de chat arrive lorsque je ne suis pas en ligne", "send_post_notifications": "Envoyer un email lors de réponses envoyées aux sujets auxquels je suis abonné", "settings-require-reload": "Certains réglages nécessitent un rechargement. Cliquez ici pour recharger la page.", - "has_no_follower": "Cet utilisateur n'est suivi par personne :(", - "follows_no_one": "Cet utilisateur ne suit personne :(", + "has_no_follower": "Personne n'est abonné à cet utilisateur :(", + "follows_no_one": "Cet utilisateur n'est abonné à personne :(", "has_no_posts": "Cet utilisateur n'a encore rien posté.", "has_no_topics": "Cet utilisateur n'a encore créé aucun sujet.", - "has_no_watched_topics": "Cet utilisateur n'a encore consulté aucun sujet.", + "has_no_watched_topics": "Cet utilisateur ne s'est encore abonné à aucun sujet.", "has_no_upvoted_posts": "Cet utilisateur n'a voté pour aucun message", "has_no_downvoted_posts": "Cet utilisateur n'a voté contre aucun message", "has_no_voted_posts": "Personne n'a voté pour des messages de cet utilisateur", @@ -88,14 +89,21 @@ "topics_per_page": "Sujets par page", "posts_per_page": "Messages par page", "notification_sounds": "Émettre un son lors de la réception de notifications", + "notifications_and_sounds": "Notifications & Sons", + "incoming-message-sound": "Son pour les messages entrants", + "outgoing-message-sound": "Son pour les messages sortants", + "notification-sound": "Son de notification", + "no-sound": "Pas de son", "browsing": "Paramètres de navigation", "open_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet", "enable_topic_searching": "Activer la recherche dans les sujets", "topic_search_help": "Une fois activé, la recherche dans les sujets va remplacer la recherche de page du navigateur et vous permettra de rechercher dans l'intégralité d'un sujet au lieu des seuls posts affichés à l'écran.", + "delay_image_loading": "Délayer le chargement des images", + "image_load_delay_help": "Lorsque cette option est activée, les images des sujets ne seront pas chargées avant qu'elles ne soient visibles dans la fenêtre.", "scroll_to_my_post": "Après avoir répondu, montrer le nouveau message", - "follow_topics_you_reply_to": "Suivre les sujets auxquels vous répondez", - "follow_topics_you_create": "Suivre les sujets que vous créez", - "grouptitle": "Sélectionnez le titre de groupe que vous souhaitez afficher", + "follow_topics_you_reply_to": "S'abonner aux sujets auxquels vous répondez", + "follow_topics_you_create": "S'abonner aux sujets que vous créez", + "grouptitle": "Nom du groupe", "no-group-title": "Aucun titre de groupe", "select-skin": "Sélectionner un thème", "select-homepage": "Sélectionner une page d'accueil", @@ -105,5 +113,17 @@ "custom_route_help": "Entrez le nom d'une route ici, sans slash au début (par exemple \"recent\" ou \"popular\")", "sso.title": "Services d'authentification unique", "sso.associated": "Associé avec", - "sso.not-associated": "Cliquez ici pour associer" + "sso.not-associated": "Cliquez ici pour associer", + "info.latest-flags": "Derniers signalements", + "info.no-flags": "Aucun signalement trouvé", + "info.ban-history": "Historique de bannissement récent", + "info.no-ban-history": "Cet utilisateur n'a jamais été banni", + "info.banned-until": "Banni jusqu'au %1", + "info.banned-permanently": "Banni de façon permanente", + "info.banned-reason-label": "Raison", + "info.banned-no-reason": "Aucune raison donnée", + "info.username-history": "Historique des noms d'utilisateur", + "info.email-history": "Historique des adresses email", + "info.moderation-note": "Note de modération", + "info.moderation-note.success": "Note de modération enregistrée" } \ No newline at end of file diff --git a/public/language/fr/users.json b/public/language/fr/users.json index fad9536811..35b6a06787 100644 --- a/public/language/fr/users.json +++ b/public/language/fr/users.json @@ -2,6 +2,7 @@ "latest_users": "Derniers inscrits", "top_posters": "Actifs", "most_reputation": "Réputés", + "most_flags": "Les plus souvent signalés", "search": "Rechercher", "enter_username": "Entrez le nom d'un utilisateur", "load_more": "Charger la suite", diff --git a/public/language/gl/category.json b/public/language/gl/category.json index 920cb96b4a..45bee7454e 100644 --- a/public/language/gl/category.json +++ b/public/language/gl/category.json @@ -10,7 +10,11 @@ "share_this_category": "Comparte esta categoría", "watch": "Vixiar", "ignore": "Ignorar", - "watch.message": "Agora vixías as novidades desta categoría", - "ignore.message": "Agora ignoras as novidades nesta categoría", + "watching": "Seguindo", + "ignoring": "Ignorando", + "watching.description": "Amosa-los temas en \"non lidos\"", + "ignoring.description": "Non amosa-los temas en \"non lidos\"", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Categorías vixiadas" } \ No newline at end of file diff --git a/public/language/gl/email.json b/public/language/gl/email.json index 340dd326e1..9d13746d08 100644 --- a/public/language/gl/email.json +++ b/public/language/gl/email.json @@ -24,6 +24,7 @@ "digest.day": "día", "digest.week": "semana", "digest.month": "mes", + "digest.subject": "Resumo de 1%", "notif.chat.subject": "Nova charla recibida de %1", "notif.chat.cta": "Pica aquí para continuar a conversación", "notif.chat.unsub.info": "Esta notificación de charla foiche enviada polas túas opcións de subscrición.", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 5a5619eaf0..80efe2e431 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -2,32 +2,37 @@ "invalid-data": "Datos non válidos", "not-logged-in": "Parece que estás desconectado.", "account-locked": "A túa conta foi bloqueada temporalmente.", - "search-requires-login": "As buscas requiren unha conta - por favor inicia sesión ou rexístrate.", + "search-requires-login": "As buscas requiren unha conta. Por favor inicia sesión ou rexístrate.", "invalid-cid": "Identificador de Categoría Inválido ", "invalid-tid": "Identificador de Tema Inválido", "invalid-pid": "Identificador de Publicación Inválido", "invalid-uid": "Identificador de Usuario Inválido", "invalid-username": "Nome de Usuario Inválido", - "invalid-email": "Correo electrónico inválido", + "invalid-email": "Enderezo electrónico inválido", "invalid-title": "Título inválido!", "invalid-user-data": "Datos de Usuario Inválidos", "invalid-password": "Contrasinal Inválido", "invalid-username-or-password": "Especifica ámbolos dous por favor, nome de usuario e contrasinal", "invalid-search-term": "Termo de búsqueda inválido", + "csrf-invalid": "Non fomos capaces de entrar, probablemente debido a que a sesión expirou. Por favor, téntao de novo", "invalid-pagination-value": "Valor de paxinación incorreto, ten que estar entre %1 e %2", "username-taken": "Nome de usuario en uso", - "email-taken": "Correo en uso", + "email-taken": "Enderezo electrónico en uso", "email-not-confirmed": "O teu correo aínda non está confirmado, por favor pica aquí para confirmalo.", "email-not-confirmed-chat": "Non podes charlar ata que confirmes o teu correo, por favor pica aquí para confirmalo.", - "no-email-to-confirm": "Este foro require confirmación de correo, por favor pica aquí para introducir un correo.", - "email-confirm-failed": "Non podemos confirmar o teu correo, por favor téntao de novo máis tarde.", + "email-not-confirmed-email-sent": "O teu enderezo electrónico está sen confirmar. Por favor, busca o correo de confirmación na túa bandexa de entrada.", + "no-email-to-confirm": "O foro require confirmación de enderezo electrónico, por favor pica aquí para engadir un.", + "email-confirm-failed": "Non podemos confirmar o teu enderezo, por favor téntao de novo máis tarde.", "confirm-email-already-sent": "O correo de confirmación foi enviado, agarda %1 minute(s) para enviar outro.", + "sendmail-not-found": "Non se atopa o executable \"sendmail\", por favor, asegúrate de que está instalado no teu sistema e que é accesible polo usuario que executa NodeBB. ", "username-too-short": "Nome de usuario demasiado curto", "username-too-long": "Nome de usuario demasiado longo.", "password-too-long": "Contrasinal moi longa", "user-banned": "Usuario expulsado", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Desculpa, agarda %1 second(s) antes de facer a túa primeira publicación.", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Sentímolo, o teu enderezo IP foi baneado desta comunidade. Se crees que se debe a un erro, por favor, contacte cun administrador.", + "ban-expiry-missing": "Por favor, engade unha data de fin do ban", "no-category": "A categoría non existe", "no-topic": "O tema non existe", "no-post": "A publicación non existe", @@ -38,10 +43,26 @@ "category-disabled": "Categoría deshabilitada", "topic-locked": "Tema Pechado", "post-edit-duration-expired": "Só podes editar as publicacións %1 segundo(s) despois de envialas. ", + "post-edit-duration-expired-minutes": "Só podes editar as publicacións %1 segundo(s) despois de envialas. ", + "post-edit-duration-expired-minutes-seconds": "Só podes editar as publicacións %1 minuto(s) %2 segundo(s) despois de envialas. ", + "post-edit-duration-expired-hours": "Só podes editar as publicacións %1 hora(s) despois de envialas. ", + "post-edit-duration-expired-hours-minutes": "Só podes editar as publicacións %1 hora(s) %2 segundo(s) despois de envialas. ", + "post-edit-duration-expired-days": "Só podes editar as publicacións %1 día(s) despois de envialas. ", + "post-edit-duration-expired-days-hours": "Só podes editar as publicacións %1 día(s) %2 hora(s) despois de envialas. ", + "post-delete-duration-expired": "Só podes borrar mensaxes %1 segundo(s) despois de escribilos.", + "post-delete-duration-expired-minutes": "Só podes borrar mensaxes %1 minuto(s) despois de escribilos.", + "post-delete-duration-expired-minutes-seconds": "Só podes borrar mensaxes %1 minuto(s) e 2% segundo(s) despois de escribilos.", + "post-delete-duration-expired-hours": "Só podes borrar mensaxes %1 hora(s) despois de escribilos.", + "post-delete-duration-expired-hours-minutes": "Só podes borrar mensaxes %1 hora(s) e %2 minuto(s) despois de escribilos.", + "post-delete-duration-expired-days": "Só podes borrar mensaxes %1 día(s) despois de escribilos.", + "post-delete-duration-expired-days-hours": "Só podes borrar mensaxes %1 día(s) e %2 hora(s) despois de escribilos.", + "cant-delete-topic-has-reply": "Non podes borrar o teu tema cando xa ten respostas", + "cant-delete-topic-has-replies": "Non podes borrar o teu tema cando xa ten %1 respostas", "content-too-short": "Por favor, introduce unha publicación máis longa. Debe conter %1 carácter(es) como mínimo.", "content-too-long": "Por favor, introduce unha publicación máis curta. As publicacións non poden conter máis de %1 carácter(es).", "title-too-short": "Por favor, introduce un título máis longo. Os títulos deben conter %1 carácter(es) como mínimo.", "title-too-long": "Por favor, introduce un título máis curto. Os títulos non poden conter máis de %1 carácter(es).", + "category-not-selected": "Category not selected.", "too-many-posts": "Só podes postear unha vez cada %1 segundo(s) - por favor agarda antes de publicar de novo.", "too-many-posts-newbie": "Como novo usuario, só podes publicar unha vez cada %1 segundo(s) ata que acades %2 de reputación -por favor, agarda para publicar de novo.", "tag-too-short": "Por favor, introduce unha etiqueta máis longa. As etiquetas deben conter %1 carácter(es) como mínimo.", @@ -51,14 +72,16 @@ "still-uploading": "Por favor, agarda a que remate a subida.", "file-too-big": "O tamaño máximo permitido é %1 kB - por favor, sube un arquivo máis pequeno", "guest-upload-disabled": "As subidas están deshabilitadas para os convidados", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "already-favourited": "Xa gardaras esta publicación.", + "already-unfavourited": "Xa desgardaras esta publicación.", "cant-ban-other-admins": "Non podes botar outros administradores!", "cant-remove-last-admin": "Eres o único administrador. Engade outros administradores antes de quitarte a ti mesmo como administrador.", + "cant-delete-admin": "Retirar privilexios de administrador desta conta antes de intentar borrala", "invalid-image-type": "Tipo de imaxe inválida. Tipos admitidos: %1", "invalid-image-extension": "Extensión de imaxe inválida", "invalid-file-type": "Tipo de arquivo inválido. Tipos admitidos: %1", "group-name-too-short": "Nome de grupo moi curto", + "group-name-too-long": "Nome de grupo demasiado longo", "group-already-exists": "O grupo xa existe", "group-name-change-not-allowed": "Cambio de nome do grupo non permitido", "group-already-member": "Xa eres parte deste grupo", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Non tes permitido editar esta mensaxe.", "cant-remove-last-user": "Non podes quitar o último usuario", "cant-delete-chat-message": "Non tes permitido borrar esta mensaxe.", + "already-voting-for-this-post": "Xa votache esta mensaxe.", "reputation-system-disabled": "O sistema de reputación está deshabilitado", "downvoting-disabled": "Os votos negativos están deshabilitados", "not-enough-reputation-to-downvote": "Non tes reputación dabonda para esta acción", @@ -96,8 +120,12 @@ "wrong-login-type-email": "Por favor, emprega o teu correo para contectarte", "wrong-login-type-username": "Por favor, usa o teu nome de usuario para conectarte", "invite-maximum-met": "Convidaches á cantidade máxima de persoas (%1 de %2).", - "no-session-found": "Non se atopa ningún inicio de sesión!", - "not-in-room": "O usuario non se encontra nesta sala", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "no-session-found": "Non se atopou ningún inicio de sesión!", + "not-in-room": "O usuario non se atopa nesta sala", + "no-users-in-room": "Non hai usuarios nesta sala", + "cant-kick-self": "Non te podes expulsar a ti mesmo do grupo", + "no-users-selected": "Ningún usuario seleccionado", + "invalid-home-page-route": "Ruta de páxina de inicio inválida", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/gl/global.json b/public/language/gl/global.json index 6ff44536a7..e4e8889b60 100644 --- a/public/language/gl/global.json +++ b/public/language/gl/global.json @@ -7,8 +7,10 @@ "403.login": "Quizais deberías <a href='%1/login'>tentar iniciar sesión</a>?", "404.title": "Non Atopado", "404.message": "Ao parecer, esta páxina non existe. Volver ao <a href='%1/'>Inicio</a>.", - "500.title": "Erro interno.", + "500.title": "Internal Error.", "500.message": "Ups! Parece que algo saíu mal!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Rexistrarse", "login": "Conectarse", "please_log_in": "Por favor, conéctate", @@ -17,6 +19,7 @@ "welcome_back": "Benvido de novo", "you_have_successfully_logged_in": "Fuches rexistrado con éxito.", "save_changes": "Gardar Cambios", + "save": "Save", "close": "Pechar ", "pagination": "Paxinación", "pagination.out_of": "%1 de %2", @@ -50,7 +53,9 @@ "topics": "Temas", "posts": "Publicacións", "best": "Mellor", + "upvoters": "Positivos", "upvoted": "Votado positivamente", + "downvoters": "Negativos", "downvoted": "Votado negativamente", "views": "Vistas", "reputation": "Reputación", @@ -87,8 +92,11 @@ "map": "Mapa", "sessions": "Inicios de sesión", "ip_address": "Enderezo IP", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "enter_page_number": "Escribe o número da páxina", + "upload_file": "Subir arquivo ", + "upload": "Subir", + "allowed-file-types": "Os tipos de arquivos permitidos son: %1", + "unsaved-changes": "Non gardaches tódolos cambios. Queres continuar e saír da páxina?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/gl/groups.json b/public/language/gl/groups.json index d9b33b659d..ef5b61ed89 100644 --- a/public/language/gl/groups.json +++ b/public/language/gl/groups.json @@ -41,7 +41,7 @@ "details.hidden": "Oculto", "details.hidden_help": "Se está habilitado, este grupo non se poderá atopar na listaxe de grupos e os usuarios deberán ser convidados manualmente.", "details.delete_group": "Eliminar Grupo", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Os grupos privados están desactivados ao nivel do sistema, esta opción non trocará nada.", "event.updated": "Os detalles do grupo foron actualizados", "event.deleted": "O grupo \"%1\" foi borrado.", "membership.accept-invitation": "Aceptar ", @@ -50,5 +50,7 @@ "membership.leave-group": "Marchar do grupo", "membership.reject": "Rexeitar", "new-group.group_name": "Nome do grupo:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Cargar foto para o grupo", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/gl/login.json b/public/language/gl/login.json index 34e3a89ecb..8b885cbb78 100644 --- a/public/language/gl/login.json +++ b/public/language/gl/login.json @@ -5,7 +5,8 @@ "remember_me": "Lembrarme?", "forgot_password": "Esqueciches o teu contrasinal?", "alternative_logins": "Métodos alternativos", - "failed_login_attempt": "Erro ao iniciar sesión, téntao de novo.", + "failed_login_attempt": "Erro ao iniciar sesión", "login_successful": "Sesión iniciada con éxito!", - "dont_have_account": "Aínda non tes conta?" + "dont_have_account": "Aínda non tes conta?", + "logged-out-due-to-inactivity": "Debido a inactividade fuches desconectado do Panel de Control de Administradores" } \ No newline at end of file diff --git a/public/language/gl/modules.json b/public/language/gl/modules.json index a50f0a5a03..f2f4605276 100644 --- a/public/language/gl/modules.json +++ b/public/language/gl/modules.json @@ -6,7 +6,7 @@ "chat.user_typing": "%1 está a escribir...", "chat.user_has_messaged_you": "%1 enviouche unha mensaxe.", "chat.see_all": "Ver tódalas chamadas", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Marcar tódolos chats como lidos ", "chat.no-messages": "Por favor, seleccione un destinatario para ver o historial das mensaxes ", "chat.no-users-in-room": "Non hai usuarios nesta sala", "chat.recent-chats": "Charlas Recentes", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Enviar e bloquear", "composer.toggle_dropdown": "Alternar despregable", "composer.uploading": "Subindo %1", + "composer.formatting.bold": "Negriña", + "composer.formatting.italic": "Itálica", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Tachado", + "composer.formatting.link": "Ligazón", + "composer.formatting.picture": "Foto", + "composer.upload-picture": "Subir foto", + "composer.upload-file": "Subir arquivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Select a category", "bootbox.ok": "De acordo", "bootbox.cancel": "Cancelar", "bootbox.confirm": "Confirmar", diff --git a/public/language/gl/notifications.json b/public/language/gl/notifications.json index 2796d782a0..326680a2fd 100644 --- a/public/language/gl/notifications.json +++ b/public/language/gl/notifications.json @@ -15,10 +15,7 @@ "upvoted_your_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> votaron positivamente a túa mensaxe en <strong>%3</strong>.", "upvoted_your_post_in_multiple": "<strong>%1</strong> e %2 máis votaron positivamente a túa mensaxe en <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> moveu a túa publicación a<strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> foi movido <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", + "moved_your_topic": "<strong>%1</strong> moveu <strong>%2</strong>", "user_flagged_post_in": "<strong>%1</strong> reportou unha mensaxe en <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> reportaron a túa mensaxe en <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> e outras %2 persoas reportaron unha mensaxe en <strong>%3</strong>", @@ -30,7 +27,7 @@ "user_started_following_you_dual": "<strong>%1</strong> e <strong>%2</strong> comezaron a seguirte.", "user_started_following_you_multiple": "<strong>%1</strong> e %2 máis comezaron a seguirte.", "new_register": "<strong>%1</strong> enviou unha petición de rexistro.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "Hai <strong>%1</strong> peticións de rexistros pendentes de revisión", "email-confirmed": "Correo confirmado", "email-confirmed-message": "Grazas por validar o teu correo. A túa conta agora está activada.", "email-confirm-error-message": "Houbo un problema validando o teu correo. Poida que o código fose inválido ou expirase. ", diff --git a/public/language/gl/pages.json b/public/language/gl/pages.json index b0a97d55cd..42001c3f70 100644 --- a/public/language/gl/pages.json +++ b/public/language/gl/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Usuarios con máis temas", "users/sort-reputation": "Usuarios máis reputados", "users/banned": "Usuarios Expulsados", + "users/most-flags": "Usuarios máis reportados", "users/search": "Búsqueda de usuarios", "notifications": "Notificacións", "tags": "Etiquetas", "tag": "Temas etiquetados con \"%1\"", "register": "Rexistrar conta", + "registration-complete": "Registration complete", "login": "Ingresa coa túa conta", "reset": "Reinicia o teu contrasinal", "categories": "Categorías", @@ -28,18 +30,19 @@ "account/edit/password": "Editando contrasinal \"%1\"", "account/edit/username": "Editando nome de usuario \"%1\"", "account/edit/email": "Editando o correo \"%1\"", + "account/info": "Información da conta", "account/following": "Xente %1 seguindo", "account/followers": "Xente a quen segues %1", "account/posts": "Publicación de %1", "account/topics": "Temas de %1", "account/groups": "%1's Grupos", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Opcións de Usuario", "account/watched": "Temas vistos por %1", "account/upvoted": "Mensaxes votadas positivamente por %1", "account/downvoted": "Mensaxes votadas negativamente por %1", "account/best": "Mellores mensaxes escritas por %1", - "confirm": "Email Confirmed", + "confirm": "Enderezo electrónico confirmado", "maintenance.text": "%1 está baixo mantemento. Por favor, volve máis tarde.", "maintenance.messageIntro": "A máis, o administrador deixou esta mensaxe: ", "throttled.text": "&1 non está dispoñible debido a unha carga excesiva. Por favor, volva noutro momento" diff --git a/public/language/gl/register.json b/public/language/gl/register.json index 305528f442..62129c9cee 100644 --- a/public/language/gl/register.json +++ b/public/language/gl/register.json @@ -1,5 +1,6 @@ { "register": "Rexistrarse", + "cancel_registration": "Cancelar rexistro", "help.email": "Por defecto, o teu correo electrónico está oculto ao público.", "help.username_restrictions": "O nome de usuario debe ter entre %1 e %2 caracteres. Os outros usuarios poden mencionarte escribindo @<span id='yourUsername'>usuario</span>.", "help.minimum_password_length": "O teu contrasinal debe ter polo menos %1 caracteres.", @@ -15,5 +16,8 @@ "alternative_registration": "Métodos de rexistro alternativos", "terms_of_use": "Termos e Condicións de Uso", "agree_to_terms_of_use": "Acepto os Termos e Condicións de Uso", - "registration-added-to-queue": "O teu rexistro foi engadido á cola de aprobación. Recibirás un correo electrónico cando sexa aceptado por un administrador." + "terms_of_use_error": "Debes acepta-los termos de uso", + "registration-added-to-queue": "O teu rexistro foi engadido á cola de aprobación. Recibirás un correo electrónico cando sexa aceptado por un administrador.", + "interstitial.intro": "Requírese información adicional antes de crea-la túa conta", + "interstitial.errors-found": "Non puidemos completa-lo teu rexistro:" } \ No newline at end of file diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index e892d34146..a66c0907ef 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -26,12 +26,30 @@ "tools": "Ferramentas", "flag": "Reportar", "locked": "Pechado", - "bookmark_instructions": "Clic aquí para ir á última publicación sen ler neste tema. ", + "pinned": "Fixo", + "moved": "Movido", + "bookmark_instructions": "Pica aquí para volver á última mensaxe lida neste tema ", "flag_title": "Reportar esta mensaxe", "flag_success": "Esta mensaxe foi reportada para moderación.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Este tema foi borrado. Só os usuarios con privilexios administrativos poden velo.", "following_topic.message": "Agora recibirás notificacións cando alguén publique neste tema.", - "not_following_topic.message": "Non recibirás notificacións deste tema.", + "not_following_topic.message": "Poderás ver este tema na lista de No Lidos, pero non recibirás notificacións cando alguén escriba nel.", + "ignoring_topic.message": "Xa non verás este fío na lista de fíos non lidos. Serás notificado cando sexas mencionado ou a túa publicación sexa votada.", "login_to_subscribe": "Por favor, identifícate para subscribirte a este tema.", "markAsUnreadForAll.success": "Publicación marcada como non lida para todos.", "mark_unread": "Marcar coma non lido", @@ -39,10 +57,16 @@ "watch": "Vixiar", "unwatch": "Deixar de vixiar", "watch.title": "Serás notificado canto haxa novas respostas neste tema", - "unwatch.title": "Deixar de vixiar este tema", + "unwatch.title": "Deixar de seguir este tema", "share_this_post": "Compartir esta publicación", + "watching": "Seguindo", + "not-watching": "Non seguindo", + "ignoring": "Ignorar", + "watching.description": "Notificádeme das novas repostas. <br/>Amosa-lo fío nos non lidos.", + "not-watching.description": "Non me notifiquedes as novas respostas. <br/>Amosa-lo fío en non lidos se a categoría non está ignorada.", + "ignoring.description": "Non me notifiquedes as novas respostas. <br/>Non amosa-lo fío en non lidos.", "thread_tools.title": "Ferramentas do tema", - "thread_tools.markAsUnreadForAll": "Marcar como non lido", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Fixar Tema", "thread_tools.unpin": "Despegar Tema", "thread_tools.lock": "Pechar Tema", @@ -65,9 +89,9 @@ "disabled_categories_note": "As categorías deshabilitadas están en gris", "confirm_move": "Mover", "confirm_fork": "Dividir", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Marcador", + "favourites": "Marcadores", + "favourites.has_no_favourites": "Aínda non marcache ningún tema. ", "loading_more_posts": "Cargando máis publicacións", "move_topic": "Mover Tema", "move_topics": "Mover Temas", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Este tema será movido á categoría", "fork_topic_instruction": "Fai clic nas publicacións que queiras dividir", "fork_no_pids": "Non seleccionaches ninguna publicación!", + "fork_pid_count": "%1 mensaxe(s) seleccionada(s)", "fork_success": "Creouse un novo tema a partir do orixinal! Fai clic aquí para ir ó novo tema.", "delete_posts_instruction": "Fai clic nas mensaxes que queres eliminar/limpar", "composer.title_placeholder": "Introduce o título do teu tema", diff --git a/public/language/gl/unread.json b/public/language/gl/unread.json index 11f79dc4ca..bb0975ec26 100644 --- a/public/language/gl/unread.json +++ b/public/language/gl/unread.json @@ -6,5 +6,8 @@ "selected": "Seleccionado", "all": "Todos", "all_categories": "Tódalas categorías", - "topics_marked_as_read.success": "Temas marcados como lidos" + "topics_marked_as_read.success": "Temas marcados como lidos", + "all-topics": "Tódolos Temas", + "new-topics": "Temas Novos", + "watched-topics": "Temas Suscritos" } \ No newline at end of file diff --git a/public/language/gl/uploads.json b/public/language/gl/uploads.json index 1622cb5693..223d022e87 100644 --- a/public/language/gl/uploads.json +++ b/public/language/gl/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Subindo o arquivo...", + "select-file-to-upload": "Selecciona un arquivo para subir!", + "upload-success": "Arquivo subido correctamente!", + "maximum-file-size": "Máximo %1 kb" } \ No newline at end of file diff --git a/public/language/gl/user.json b/public/language/gl/user.json index 014b7aead3..350604ac8b 100644 --- a/public/language/gl/user.json +++ b/public/language/gl/user.json @@ -6,6 +6,7 @@ "postcount": "Reconto de mensaxes", "email": "Correo Electrónico", "confirm_email": "Confirma o teu correo electrónico", + "account_info": "Información da conta", "ban_account": "Suspender conta", "ban_account_confirm": "Estás seguro de que desexas expulsar a este usuario?", "unban_account": "Readmitir conta", @@ -22,7 +23,7 @@ "profile": "Perfil", "profile_views": "Visitas ao perfil:", "reputation": "Reputación", - "favourites": "Bookmarks", + "favourites": "Marcadores", "watched": "Visto", "followers": "Seguidores", "following": "Seguindo", @@ -39,7 +40,7 @@ "change_username": "Cambia-lo nome de usuario", "change_email": "Cambia-lo correo", "edit": "Editar", - "edit-profile": "Edit Profile", + "edit-profile": "Editar Perfil", "default_picture": "Icona por defecto.", "uploaded_picture": "Foto subida", "upload_new_picture": "Subir unha nova foto", @@ -56,11 +57,11 @@ "password": "Contrasinal", "username_taken_workaround": "Ese nome de usuario xa estaba collido, así que o modificamos lixeiramente. Agora o teu nome é <strong>%1</strong> ", "password_same_as_username": "O teu contrasinal e o teu nome de usuario son os mesmos, por favor, escolle outro contrasinal.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "O teu contrasinal é igual que o teu enderezo electrónico, por favor, escolle outro contrasinal.", "upload_picture": "Subir foto", "upload_a_picture": "Subir unha foto", "remove_uploaded_picture": "Borrar unha foto subida", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Subir imaxen de portada", "settings": "Opcións", "show_email": "Amosa-lo meu Email", "show_fullname": "Amosa-lo meu Nome Completo", @@ -88,14 +89,21 @@ "topics_per_page": "Temas por páxina", "posts_per_page": "Mensaxes por páxina", "notification_sounds": "Reproducir un son cando recibes unha notificación.", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Opcións de busca", "open_links_in_new_tab": "Abrir ligazóns externos nunca nova pestaña", "enable_topic_searching": "Permitir buscar dentro dun tema", "topic_search_help": "Se se activa, o buscador do NodeBB superporase ao propio do navegador dentro de cada tema, permitindo buscar en todo o tema e non só naquilo que se presenta na pantalla.", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Segui-los temas nos que respostas", - "follow_topics_you_create": "Segui-los temas que ti fas", - "grouptitle": "Escolle o título de grupo que che gustaría amosar", + "delay_image_loading": "Retrasar a carga de imaxes", + "image_load_delay_help": "Se se habilita, as imaxes non cargarán hasta que sexan visibles na pantalla", + "scroll_to_my_post": "Logo de enviar unha resposta, mostrar a nova mensaxe", + "follow_topics_you_reply_to": "Segui-los temas aos que respondes", + "follow_topics_you_create": "Segui-los temas que creaches ti", + "grouptitle": "Título do Grupo", "no-group-title": "Sen titulo de grupo", "select-skin": "Seleccionar apariencia", "select-homepage": "Escolla unha páxina de inicio", @@ -105,5 +113,17 @@ "custom_route_help": "Introduza o nome de ruta aquí, sen ningunha barra (por exemplo, \"recente\", ou \"popular\")", "sso.title": "Servizos de Inicio de Sesión Único", "sso.associated": "Asociado con", - "sso.not-associated": "Pica aquí para asociarte con" + "sso.not-associated": "Pica aquí para asociarte con", + "info.latest-flags": "Últimos reportes", + "info.no-flags": "Non se atopou ninguna mensaxe reportada", + "info.ban-history": "Histórico recente de bans", + "info.no-ban-history": "Este usuario nunca foi baneado", + "info.banned-until": "Baneado hasta 1%", + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/gl/users.json b/public/language/gl/users.json index 625c85f741..4abbb74a27 100644 --- a/public/language/gl/users.json +++ b/public/language/gl/users.json @@ -2,6 +2,7 @@ "latest_users": "Últimos usuarios", "top_posters": "Maiores Publicadores", "most_reputation": "Máis Reputados", + "most_flags": "Máis reportados", "search": "Busca", "enter_username": "Introduce o nome de usuario a procurar", "load_more": "Cargar máis", diff --git a/public/language/he/category.json b/public/language/he/category.json index ed3cef9f98..fbf21e942d 100644 --- a/public/language/he/category.json +++ b/public/language/he/category.json @@ -10,7 +10,11 @@ "share_this_category": "שתף קטגוריה זו", "watch": "עקוב", "ignore": "התעלם", - "watch.message": "אתה עכשיו עוקב אחרי עדכונים מקטגוריה זו", - "ignore.message": "אתה עכשיו מתעלם מעדכונים מקטגוריה זו", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "צפה בנושאים שלא נקראו", + "ignoring.description": "אל תציג נושאים שאינם נקראו", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "קטגוריות נעקבות" } \ No newline at end of file diff --git a/public/language/he/email.json b/public/language/he/email.json index 9ca8de27a9..48ff8f4a50 100644 --- a/public/language/he/email.json +++ b/public/language/he/email.json @@ -24,6 +24,7 @@ "digest.day": "יום", "digest.week": "שבוע", "digest.month": "חודש", + "digest.subject": "Digest for %1", "notif.chat.subject": "הודעת צ'אט חדשה התקבלה מ%1", "notif.chat.cta": "לחץ כאן כדי להמשיך את השיחה", "notif.chat.unsub.info": "התראה הצ'אט הזו נשלחה אליך על-פי הגדרות החשבון שלך.", diff --git a/public/language/he/error.json b/public/language/he/error.json index 33972d8246..f84a042f25 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -14,20 +14,25 @@ "invalid-password": "סיסמא שגויה", "invalid-username-or-password": "אנא הגדר שם משתמש וסיסמה", "invalid-search-term": "מילת חיפוש לא תקינה", + "csrf-invalid": "אין באפשרותנו לחבר אותך למערכת, מכיוון שעבר זמן רב מידי. אנא נסה שנית.", "invalid-pagination-value": "ערך דף לא חוקי, חייב להיות לפחות %1 ולא מעל %2", "username-taken": "שם משתמש תפוס", "email-taken": "כתובת אימייל תפוסה", "email-not-confirmed": "כתובת המייל שלך עוד לא אושרה, לחץ כאן על-מנת לאשר את המייל שלך.", "email-not-confirmed-chat": "אין באפשרותך לשוחח עד שהדוא\"ל שלך יאושר, אנא לחץ כאן כדי לאשר את הדוא\"ל שלך.", + "email-not-confirmed-email-sent": "האימייל עדין לא אושר. אנא בדוק בתיבת הדואר בנוגע לאישור האימייל שנשלח לך על ידנו.", "no-email-to-confirm": "פורום זה דורש אישור בדוא\"ל, אנא לחץ כאן כדי להכניס לדואר אלקטרוני", "email-confirm-failed": "לא הצלחנו לאשר את הדוא\"ל שלך, תנסה שוב אחר כך", "confirm-email-already-sent": "מייל האישור כבר נשלח, אנא המתן %1 דקות כדי לשלוח מייל נוסף.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "שם משתמש קצר מדי", "username-too-long": "שם משתמש ארוך מדי", "password-too-long": "הסיסמה ארוכה מדי", "user-banned": "המשתמש מושעה", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "אנא המתן %1 שניות לפני פרסום ההודעה", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "מצטערים, אך הורחקת מקהילה זו. אם הנך סבור שמדובר בטעות, אנא צור קשר עם מנהלי הקהילה.", + "ban-expiry-missing": "אנא ספק תאריך סיום להרחקה זו.", "no-category": "קטגוריה אינה קיימת", "no-topic": "נושא אינו קיים", "no-post": "פוסט אינו קיים", @@ -38,10 +43,26 @@ "category-disabled": "קטגוריה לא פעילה", "topic-locked": "נושא נעול", "post-edit-duration-expired": "אתה רשאי לערוך פוסטים רק ל-%1 שניות לאחר הפרסום", + "post-edit-duration-expired-minutes": "הנך רשאי לערוך תגובה עד %1 דקות מרגע פרסום התגובה.", + "post-edit-duration-expired-minutes-seconds": "הנך רשאי לערוך תגובה עד %1 דקות %2 ושניות מרגע פרסום התגובה.", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "אינך יכול למחוק נושא אחרי שכבר הגיבו בו.", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "אנא הכנס פוסט ארוך יותר. פוסטים חייבים להכיל לפחות %1 תווים.", "content-too-long": "אנא הכנס פוסט קצר יותר. פוסטים חייבים להיות קצרים יותר מ-%1 תווים.", "title-too-short": "אנא הכנס כותרת ארוכה יותר. כותרות חייבות להכיל לפחות %1 תווים.", "title-too-long": "אנא הכנס כותרת קצרה יותר. כותרות אינן יכולות להיות ארוכות מ-%1 תווים.", + "category-not-selected": "Category not selected.", "too-many-posts": "אתה יכול לפרסם פוסט רק פעם ב-%1 שניות - אנא המתן לפני פרסום שוב", "too-many-posts-newbie": "כמשתמש חדש, אתה יכול לפרסם פוסט רק פעם ב-%1 שניות עד שיהיו לך %2 נקודות מוניטין - אנא המתן לפני פרסום שוב", "tag-too-short": "אנא הכנס תגית ארוכה יותר. תגיות חייבות להכיל לפחות %1 תווים", @@ -55,10 +76,12 @@ "already-unfavourited": "כבר הסרת את הסימון מפוסט זה", "cant-ban-other-admins": "אינך יכול לחסום מנהלים אחרים!", "cant-remove-last-admin": "אתה המנהל היחיד. הוסף משתמש אחר לניהול לפני שאתה מוריד את עצמך מניהול", + "cant-delete-admin": "משתמש זה מוגדר כמנהל. על מנת למחוק את המשתמש, עליך להסיר קודם את גישותיו.", "invalid-image-type": "פורמט תמונה לא תקין. הפורמטים המורשים הם: %1", "invalid-image-extension": "פורמט תמונה לא תקין", "invalid-file-type": "פורמט הקובץ לא תקין. הפורמטים המורשים הם: %1", "group-name-too-short": "שם הקבוצה קצר מדי", + "group-name-too-long": "שם הקבוצה ארוך מידי", "group-already-exists": "הקבוצה כבר קיימת", "group-name-change-not-allowed": "לא ניתן לשנות את שם הקבוצה", "group-already-member": "כבר חבר בקבוצה זו", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "אתה לא רשאי לערוך הודעה זו", "cant-remove-last-user": "אינך יכול למחוק את המשתמש האחרון", "cant-delete-chat-message": "אתה לא רשאי למחוק הודעה זו", + "already-voting-for-this-post": "הצבעת כבר בנושא זה", "reputation-system-disabled": "מערכת המוניטין לא פעילה.", "downvoting-disabled": "היכולת להצביע נגד לא פעילה", "not-enough-reputation-to-downvote": "אין לך מספיק מוניטין כדי להוריד את הדירוג של פוסט זה", @@ -99,5 +123,9 @@ "no-session-found": "לא נמצאו סשני התחברות!", "not-in-room": "משתמש זה לא בצ'אט", "no-users-in-room": "אין משתמש בחדר הזה", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "אינך יכול להסיר את עצמך מהקבוצה", + "no-users-selected": "לא נבחרו משתמשים", + "invalid-home-page-route": "כתובת דף הבית הינה שגויה", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/he/global.json b/public/language/he/global.json index f4b966429e..8ada0b3245 100644 --- a/public/language/he/global.json +++ b/public/language/he/global.json @@ -9,6 +9,8 @@ "404.message": "נראה שהגעת לעמוד שלא קיים. חזור <a href=\"%1/\">לעמוד הבית</a>", "500.title": "שגיאה פנימית.", "500.message": "אופס! נראה שמשהו השתבש!", + "400.title": "בקשה שגויה", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "הרשמה", "login": "התחברות", "please_log_in": "אנא התחבר", @@ -17,6 +19,7 @@ "welcome_back": "ברוכים השבים", "you_have_successfully_logged_in": "התחברת בהצלחה", "save_changes": "שמור שינויים", + "save": "Save", "close": "סגור", "pagination": "עימוד", "pagination.out_of": "%1 מתוך %2", @@ -50,7 +53,9 @@ "topics": "נושאים", "posts": "פוסטים", "best": "הגבוה ביותר", + "upvoters": "Upvoters", "upvoted": "הוצבע בעד", + "downvoters": "Downvoters", "downvoted": "הוצבע נגד", "views": "צפיות", "reputation": "מוניטין", @@ -90,5 +95,8 @@ "enter_page_number": "הכנס מספר עמוד", "upload_file": "העלה קובץ", "upload": "העלה", - "allowed-file-types": "פורמטי הקבצים המורשים הם %1" + "allowed-file-types": "פורמטי הקבצים המורשים הם %1", + "unsaved-changes": "יש לך שינויים שאינם נשמרו. האם הנך בטוח שברצונך להמשיך?", + "reconnecting-message": "נראה שההתחברות שלך אל %1 אבדה, אנא המתן בזמן שהמערכת מנסה לחבר אותך מחדש", + "play": "Play" } \ No newline at end of file diff --git a/public/language/he/groups.json b/public/language/he/groups.json index fe0bb93380..4e505a826e 100644 --- a/public/language/he/groups.json +++ b/public/language/he/groups.json @@ -41,7 +41,7 @@ "details.hidden": "מוסתר", "details.hidden_help": "אם מאופשר, הקבוצה לא תופיע ברשימת הקבוצות, ומשתמשים יצטרכו להיות מוזמנים ידנית", "details.delete_group": "מחק קבוצה", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "קבוצות פרטיות מבוטלות על ידי המערכת. אופציה זו אינה עושה דבר.", "event.updated": "פרטי הקבוצה עודכנו", "event.deleted": "קבוצת \"%1\" נמחקה", "membership.accept-invitation": "קבל הזמנה", @@ -50,5 +50,7 @@ "membership.leave-group": "עזוב קבוצה", "membership.reject": "דחה", "new-group.group_name": "שם קבוצה", - "upload-group-cover": "העלה תמונת נושא לקבוצה" + "upload-group-cover": "העלה תמונת נושא לקבוצה", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/he/language.json b/public/language/he/language.json index 203ee90e04..2c12710abf 100644 --- a/public/language/he/language.json +++ b/public/language/he/language.json @@ -1,5 +1,5 @@ { - "name": "אנגלית (הממלכה המאוחדת/קנדה)", + "name": "עברית (ישראל)", "code": "he", "dir": "rtl" } \ No newline at end of file diff --git a/public/language/he/login.json b/public/language/he/login.json index 6142a8a045..cbc60f999c 100644 --- a/public/language/he/login.json +++ b/public/language/he/login.json @@ -5,7 +5,8 @@ "remember_me": "זכור אותי?", "forgot_password": "שכחת סיסמתך?", "alternative_logins": "התחבר באמצעות...", - "failed_login_attempt": "נסיון התחברות נכשל, נסה שוב.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "התחברת בהצלחה!", - "dont_have_account": "אין לך חשבון עדיין?" + "dont_have_account": "אין לך חשבון עדיין?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/he/modules.json b/public/language/he/modules.json index 6b6f8c40f3..fda2dd982c 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "אשר ונעל", "composer.toggle_dropdown": "הדלק/כבה את התפריט הנפתח", "composer.uploading": "העלאה %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "בסדר", "bootbox.cancel": "בטל", "bootbox.confirm": "אשר", diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json index 30255302fc..134b173de0 100644 --- a/public/language/he/notifications.json +++ b/public/language/he/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> ו%2 אחרים הצביעו לפוסט שלך ב<strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> העביר את הפוסט שלך ל<strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> הוזז ל<strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> סימן את הפוסט שלך ב<strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> ו<strong>%2</strong> סימנו את הפוסט שלך ב<strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> ו-%2 אחרים סימנו את הפוסט שלך ב<strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> דיווח על פוסט ב <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> ו<strong>%2</strong> סימנו פוסט ב<strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> ו%2 נוספים סימנו פוסט ב<strong>%3</strong>", diff --git a/public/language/he/pages.json b/public/language/he/pages.json index 0a36793f6a..6affa90b24 100644 --- a/public/language/he/pages.json +++ b/public/language/he/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "משתמשים עם המונה הגבוה ביותר", "users/sort-reputation": "משתמשים עם המוניטין הגבוה ביותר", "users/banned": "משתמשים מורחקים", + "users/most-flags": "Most flagged users", "users/search": "חיפוש משתמשים", "notifications": "התראות", "tags": "תגיות", "tag": "נושאים שתוייגו תחת \"%1\"", "register": "יצירת חשבון", + "registration-complete": "Registration complete", "login": "התחבר לחשבונך", "reset": "איפוס סיסמה למשתמש", "categories": "קטגוריות", @@ -28,12 +30,13 @@ "account/edit/password": "עורך את הסיסמה של \"%1\"", "account/edit/username": "עורך את שם המשתמש של \"%1\"", "account/edit/email": "עורך את כתובת המייל של \"%1\"", + "account/info": "Account Info", "account/following": "אנשים ש%1 עוקב אחריהם", "account/followers": "אנשים שעוקבים אחרי %1", "account/posts": "הודעות שפורסמו על ידי %1", "account/topics": "נושאים שנוצרו על ידי %1", "account/groups": "הקבוצות של %1", - "account/favourites": "הפוסטים שסומנו על ידי %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "הגדרות משתמש", "account/watched": "נושאים שנצפו על ידי %1", "account/upvoted": "פוסטים שהוצבעו לטובה על ידי %1", diff --git a/public/language/he/register.json b/public/language/he/register.json index 752a5f85e7..777e83302c 100644 --- a/public/language/he/register.json +++ b/public/language/he/register.json @@ -1,5 +1,6 @@ { "register": "הרשם", + "cancel_registration": "Cancel Registration", "help.email": "כברירת מחדל, כתובת האימייל שלך אינה חשופה למשתמשים אחרים", "help.username_restrictions": "שם משתמש ייחודי בין %1 ל %2 תווים. משתמשים אחרים יכולים לציין את שמך באמצעות @<span id='yourUsername'>שם המשתמש שלך</span>.", "help.minimum_password_length": "סיסמתך חייבת להיות לפחות באורך של %1 תווים.", @@ -15,5 +16,8 @@ "alternative_registration": "הרשם באמצעות...", "terms_of_use": "תנאי שימוש", "agree_to_terms_of_use": "אני מסכים לתנאי השימוש", - "registration-added-to-queue": "הבקשה שלך להרשמה נשלחה. תקבל בקרוב מייל אישור לכתובת האימייל שהכנסת כשמנהל יאשר את הבקשה." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "הבקשה שלך להרשמה נשלחה. תקבל בקרוב מייל אישור לכתובת האימייל שהכנסת כשמנהל יאשר את הבקשה.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json index 49ca0e5bba..aef745d1d3 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -26,12 +26,30 @@ "tools": "כלים", "flag": "דווח", "locked": "נעול", - "bookmark_instructions": "לחץ כאן כדי לחזור לפוסט האחרון שלא קראת באשכול זה", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "דווח על פוסט זה למנהל", "flag_success": "התקבל דיווח על פוסט זה.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "נושא זה נמחק. רק משתמשים עם ההרשאות המתאימות יכולים לצפות בו.", "following_topic.message": "מעתה, תקבל הודעות כאשר מישהו יעלה פוסט לנושא זה.", - "not_following_topic.message": "לא תקבל הודעות נוספות בנושא זה.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "אנא הרשם או התחבר על-מנת לעקוב אחר נושא זה.", "markAsUnreadForAll.success": "נושא זה סומן כלא נקרא לכולם.", "mark_unread": "סמן כלא נקרא", @@ -41,8 +59,14 @@ "watch.title": "קבל התראה כאשר יש תגובות חדשות בנושא זה", "unwatch.title": "הפסק לעקוב אחר נושא זה", "share_this_post": "שתף פוסט זה", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "כלי נושא", - "thread_tools.markAsUnreadForAll": "סמן כלא נקרא", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "נעץ נושא", "thread_tools.unpin": "הסר נעץ", "thread_tools.lock": "נעל נושא", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "נושא זה יועבר לקטגוריה", "fork_topic_instruction": "לחץ על הפוסטים שברצונך לשכפל", "fork_no_pids": "לא בחרת אף פוסט!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "הפוסט שוכפל בהצלחה! לחץ כאן על מנת לעבור לפוסט המשוכפל.", "delete_posts_instruction": "לחץ על הפוסטים שברצונך למחוק", "composer.title_placeholder": "הכנס את כותרת הנושא כאן...", diff --git a/public/language/he/unread.json b/public/language/he/unread.json index 587071777b..51923d7147 100644 --- a/public/language/he/unread.json +++ b/public/language/he/unread.json @@ -6,5 +6,8 @@ "selected": "נבחר", "all": "הכל", "all_categories": "כל הקטגוריות", - "topics_marked_as_read.success": "נושאים שמסומנים כנקרא!" + "topics_marked_as_read.success": "נושאים שמסומנים כנקרא!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/he/user.json b/public/language/he/user.json index a442d5fd10..f09484a711 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -6,6 +6,7 @@ "postcount": "כמות פוסטים", "email": "כתובת אימייל", "confirm_email": "אשר מייל", + "account_info": "Account Info", "ban_account": "הרחק חשבון", "ban_account_confirm": "אתה בטוח שברצונך להרחיק את המשתמש הזה?", "unban_account": "בטל את הרחקת החשבון", @@ -88,14 +89,21 @@ "topics_per_page": "כמות נושאים בעמוד", "posts_per_page": "כמות פוסטים בעמוד", "notification_sounds": "נגן סאונד כשמתקבלת התראה", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "הגדרות צפייה", "open_links_in_new_tab": "פתח קישורים חיצוניים בכרטיסייה חדשה", "enable_topic_searching": "הפעל חיפוש בתוך נושא", "topic_search_help": "אם מאופשר, החיפוש בתוך הנושא יעקוף את שיטת החיפוש של הדפדפן, ויאפשר לך לחפש בכל הנושא - ולא רק במה שמוצג על המסך", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "עקוב אחרת נושאים שהגבת בהם", - "follow_topics_you_create": "עקוב אחר נושאים שיצרת", - "grouptitle": "בחר את כותרת הקבוצה שברצונך להציג", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "ללא כותרת לקבוצה", "select-skin": "בחר מראה", "select-homepage": "בחר דף בית", @@ -105,5 +113,17 @@ "custom_route_help": "הכנס שם נתיב כאן, ללא לוכסן לפני (לדוגמא \"אחרונים\", או \"פופולארי\")", "sso.title": "שירות יחיד להתחברות", "sso.associated": "משוייך עם", - "sso.not-associated": "לחץ כאן כדי לשייך" + "sso.not-associated": "לחץ כאן כדי לשייך", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/he/users.json b/public/language/he/users.json index 7bbdc26e0b..b4c068bd33 100644 --- a/public/language/he/users.json +++ b/public/language/he/users.json @@ -2,6 +2,7 @@ "latest_users": "משתמשים אחרונים", "top_posters": "מפרסמים הכי הרבה", "most_reputation": "המוניטין הגבוה ביותר", + "most_flags": "Most Flags", "search": "חיפוש", "enter_username": "הכנס שם משתמש לחיפוש", "load_more": "טען עוד", diff --git a/public/language/hu/category.json b/public/language/hu/category.json index 6bbcf00519..4e703da034 100644 --- a/public/language/hu/category.json +++ b/public/language/hu/category.json @@ -10,7 +10,11 @@ "share_this_category": "Kategória megosztása", "watch": "Watch", "ignore": "Ignorálás", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/hu/email.json b/public/language/hu/email.json index 729de1a530..15a689ac13 100644 --- a/public/language/hu/email.json +++ b/public/language/hu/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Új chat üzenet érkezett a következőtől: %1", "notif.chat.cta": "Kattints ide a beszélgetés folytatásához", "notif.chat.unsub.info": "Ez a chat-értesítés a feliratkozási beállításaid miatt lett kiküldve.", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index e0744b39ca..7f77bc1fff 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -14,20 +14,25 @@ "invalid-password": "Érvénytelen jelszó", "invalid-username-or-password": "Kérlek adj meg egy felhasználónevet és egy jelszót", "invalid-search-term": "Érvénytelen keresési feltétel", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Foglalt felhasználónév", "email-taken": "Foglalt e-mail", "email-not-confirmed": "Az e-mail címed még nem lett ellenőrizve, kérlek kattints ide az e-mail címed ellenőrzéséhez!", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Ez a fórum e-mail megerősítést kíván, kérlek kattints ide egy cím beírásához", "email-confirm-failed": "Nem tudtuk ellenőrizni az e-mail címedet, kérlek próbálkozz később.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Túl rövid felhasználónév", "username-too-long": "Túl hosszú felhasználónév", "password-too-long": "Password too long", "user-banned": "Kitiltott felhasználó", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Nem létező kategória", "no-topic": "Nem létező téma", "no-post": "Nem létező hozzászólás", @@ -38,10 +43,26 @@ "category-disabled": "Kategória kikapcsolva", "topic-locked": "Téma lezárva", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Nem tilthatsz ki másik adminisztrátort!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Érvénytelen a kép típusa. Engedett kiterjesztések: %1", "invalid-image-extension": "Érvénytelen a kép kiterjesztése", "invalid-file-type": "Érvénytelen a fájl típusa. Engedélyezett kiterjesztések: %1", "group-name-too-short": "A csoport név túl rövid", + "group-name-too-long": "Group name too long", "group-already-exists": "A csoport nem létezik", "group-name-change-not-allowed": "A csoport névváltoztatás nem engedélyezett", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Hírnév funkció kikapcsolva.", "downvoting-disabled": "Leszavazás funkció kikapcsolva", "not-enough-reputation-to-downvote": "Nem rendelkezel elég Hírnév ponttal, hogy leszavazhasd ezt a hozzászólást", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/hu/global.json b/public/language/hu/global.json index 63ad09e507..17b9be5766 100644 --- a/public/language/hu/global.json +++ b/public/language/hu/global.json @@ -7,8 +7,10 @@ "403.login": "Talán meg kellene próbálnod <a href='%1/login'>belépni</a>?", "404.title": "Nincs találat", "404.message": "Úgy tűnik, hogy rábukkantál egy olyan oldalra ami nem létezik. Visszatérés a <a href='%1/'>kezdőlapra</a>", - "500.title": "Belső hiba.", + "500.title": "Internal Error.", "500.message": "Hoppá! Úgy tűnik valami hiba történt!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Regisztráció", "login": "Belépés", "please_log_in": "Jelentkezzünk be", @@ -17,6 +19,7 @@ "welcome_back": "Üdvözlet", "you_have_successfully_logged_in": "Sikeres bejelentkezés", "save_changes": "Változások mentése", + "save": "Save", "close": "Bezárás", "pagination": "Lapozás", "pagination.out_of": "%1 - %2", @@ -50,7 +53,9 @@ "topics": "Témák", "posts": "Hozzászólások", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Megtekintések", "reputation": "Hírnév", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/hu/groups.json b/public/language/hu/groups.json index 0430f698e5..26ef5b4d2e 100644 --- a/public/language/hu/groups.json +++ b/public/language/hu/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/hu/login.json b/public/language/hu/login.json index 14be774171..ad206f1845 100644 --- a/public/language/hu/login.json +++ b/public/language/hu/login.json @@ -5,7 +5,8 @@ "remember_me": "Megjegyzés", "forgot_password": "Elfelejtett Jelszó?", "alternative_logins": "Alternatív belépés", - "failed_login_attempt": "Sikertelen belépési kísérlet, kérlek próbálkozzunk újra.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Sikeres belépés", - "dont_have_account": "Nincs még fiók?" + "dont_have_account": "Nincs még fiók?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json index 65142c04d3..6b050a4f2f 100644 --- a/public/language/hu/modules.json +++ b/public/language/hu/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json index b1b09a5dd7..81f6ed12ed 100644 --- a/public/language/hu/notifications.json +++ b/public/language/hu/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/hu/pages.json b/public/language/hu/pages.json index c67ccb5355..59dd5244b3 100644 --- a/public/language/hu/pages.json +++ b/public/language/hu/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Értesítések", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/hu/register.json b/public/language/hu/register.json index a14ce476ca..041b12c0c4 100644 --- a/public/language/hu/register.json +++ b/public/language/hu/register.json @@ -1,5 +1,6 @@ { "register": "Regisztráció", + "cancel_registration": "Cancel Registration", "help.email": "Az e-mail cím nem lesz közzétéve.", "help.username_restrictions": "Egyedi felhasználói név %1 és %2 karakterek között. A többiek megemlíthetnek az alábbi módon @<span id='yourUsername'>nicknév</span>.", "help.minimum_password_length": "A jelszónak legalább %1 karakter hosszúnak kell lennie.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatív Regisztráció", "terms_of_use": "Használtai feltételek", "agree_to_terms_of_use": "Egyetértek a Használtai Feltételekkel", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index 189f065bde..8185a77748 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -26,12 +26,30 @@ "tools": "Eszközök", "flag": "Jelentés", "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "A hozzászólás jelentése a moderátoroknál", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", "markAsUnreadForAll.success": "Topic marked as unread for all.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Hozzászólás megosztása", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Olvasatlannak jelölés", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Kiemel", "thread_tools.unpin": "Kiemelés megszűntetése", "thread_tools.lock": "Topik lezárása", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Ez a téma ebbe a kategóriába lesz mozgatva", "fork_topic_instruction": "Klikkelj azokra a hozzászólásokra, amiket szét akarsz szedni", "fork_no_pids": "Nincs hozzászólás kiválasztva!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Írd be a témanevet...", diff --git a/public/language/hu/unread.json b/public/language/hu/unread.json index 8bf28e0be6..dd411974f5 100644 --- a/public/language/hu/unread.json +++ b/public/language/hu/unread.json @@ -6,5 +6,8 @@ "selected": "Kiválasztva", "all": "Összes", "all_categories": "All categories", - "topics_marked_as_read.success": "Témakör olvasottnak jelölve!" + "topics_marked_as_read.success": "Témakör olvasottnak jelölve!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/hu/user.json b/public/language/hu/user.json index fd2a183e03..45bfad8825 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -6,6 +6,7 @@ "postcount": "Bejegyzés megtekintés", "email": "E-mail", "confirm_email": "E-mail megerősítése", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Téma oldalanként", "posts_per_page": "Hozzászólás oldalanként", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Témán belüli keresés bekapcsolása", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/hu/users.json b/public/language/hu/users.json index faedac9f35..8deea8b8c2 100644 --- a/public/language/hu/users.json +++ b/public/language/hu/users.json @@ -2,6 +2,7 @@ "latest_users": "Legújabb felhasználók", "top_posters": "Legaktívabbak", "most_reputation": "Legnépszerűbbek", + "most_flags": "Most Flags", "search": "Keresés", "enter_username": "Írj be egy nicknevet a kereséshez", "load_more": "Több betöltése", diff --git a/public/language/id/category.json b/public/language/id/category.json index ac3493acb0..aedde51460 100644 --- a/public/language/id/category.json +++ b/public/language/id/category.json @@ -10,7 +10,11 @@ "share_this_category": "Bagikan kategori ini", "watch": "Watch", "ignore": "Abaikan", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/id/email.json b/public/language/id/email.json index 4b3a591c23..4f778999a4 100644 --- a/public/language/id/email.json +++ b/public/language/id/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Pesan yang baru diterima dari %1", "notif.chat.cta": "Klik di sini untuk melanjutkan percakapan", "notif.chat.unsub.info": "Sesuai pengaturan langganan anda, notifikasi obrolan ini dikirmkan kepada anda", diff --git a/public/language/id/error.json b/public/language/id/error.json index de932ffac2..0994f23233 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -14,20 +14,25 @@ "invalid-password": "Password Salah", "invalid-username-or-password": "Mohon spesifikasikan username dan password", "invalid-search-term": "Kata pencarian salah", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Username sudah terdaftar", "email-taken": "Email sudah terdaftar", "email-not-confirmed": "Email kamu belum dikonfirmasi, klik disini untuk mengkonfirmasi email.", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username terlalu pendek", "username-too-long": "Username terlalu panjang", "password-too-long": "Password too long", "user-banned": "Pengguna dibanned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategori tidak ditemukan", "no-topic": "Topik tidak ditemukan", "no-post": "Post tidak ditemukan", @@ -38,10 +43,26 @@ "category-disabled": "Kategori ditiadakan", "topic-locked": "Topik dikunci", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Kamu tidak dapat ban admin lainnya!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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": "Nama grup terlalu pendek", + "group-name-too-long": "Group name too long", "group-already-exists": "Grup sudah ada", "group-name-change-not-allowed": "Perubahan nama grup tidak dibolehkan", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistem reputasi ditiadakan.", "downvoting-disabled": "Downvoting ditiadakan", "not-enough-reputation-to-downvote": "Tidak cukup reputation untuk downvote post ini", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/id/global.json b/public/language/id/global.json index c488a606a9..a6285fcf1a 100644 --- a/public/language/id/global.json +++ b/public/language/id/global.json @@ -7,8 +7,10 @@ "403.login": "Mungkin kamu harus <a href='%1/login'>mencoba untuk login</a>?", "404.title": "Tidak ditemukan", "404.message": "Kamu kelihatan mengakses halaman yang tidak ada. Kembali ke <a href='%1/'>beranda</a>.", - "500.title": "Kesalahan internal", + "500.title": "Internal Error.", "500.message": "Oops! Terjadi kesalahan", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Daftar", "login": "Login", "please_log_in": "Silakan Log In", @@ -17,6 +19,7 @@ "welcome_back": "Selamat Datang Kembali", "you_have_successfully_logged_in": "Kamu sudah login", "save_changes": "Menyimpan perubahan", + "save": "Save", "close": "Tutup", "pagination": "Halaman", "pagination.out_of": "%1 dari %2", @@ -50,7 +53,9 @@ "topics": "Topik", "posts": "Post", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Views", "reputation": "Reputasi", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/id/groups.json b/public/language/id/groups.json index 32f99e3fdc..255c6caef0 100644 --- a/public/language/id/groups.json +++ b/public/language/id/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/id/login.json b/public/language/id/login.json index 7a1c52fe71..bc847cc9f8 100644 --- a/public/language/id/login.json +++ b/public/language/id/login.json @@ -5,7 +5,8 @@ "remember_me": "Ingin Diingat?", "forgot_password": "Lupa Password?", "alternative_logins": "Login Alternatif", - "failed_login_attempt": "Percobaan login gagal, cobalah kembali.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Kamu telah berhasil login!", - "dont_have_account": "Belum memiliki akun?" + "dont_have_account": "Belum memiliki akun?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/id/modules.json b/public/language/id/modules.json index 7ddec19b96..cd256ea920 100644 --- a/public/language/id/modules.json +++ b/public/language/id/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/id/notifications.json b/public/language/id/notifications.json index e4290a117d..f0d1954715 100644 --- a/public/language/id/notifications.json +++ b/public/language/id/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> menandai sebuah posting di <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/id/pages.json b/public/language/id/pages.json index 0ec372ded8..961f7f898a 100644 --- a/public/language/id/pages.json +++ b/public/language/id/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Pemberitahuan", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/id/register.json b/public/language/id/register.json index 305cb2c19f..62a5a15ee2 100644 --- a/public/language/id/register.json +++ b/public/language/id/register.json @@ -1,5 +1,6 @@ { "register": "Daftar", + "cancel_registration": "Cancel Registration", "help.email": "Secara default, emailmu akan disembunyikan dari publik.", "help.username_restrictions": "Nama Pengguna yang unik antara %1 dan %2 karakter. Pengguna lain dapat menyebutmu dengan menggunakan @<span id='Nama Pengguna mu'>nama pengguna</span>.", "help.minimum_password_length": "Panjang password harus setidaknya %1 karakter.", @@ -15,5 +16,8 @@ "alternative_registration": "Pendaftaran Alternatif", "terms_of_use": "Aturan Penggunaan", "agree_to_terms_of_use": "Saya menyetujui Aturan Penggunaan", - "registration-added-to-queue": "Pendaftaranmu telah ditambahkan dalam daftar persetujuan. Kamu akan menerima email ketika pendaftaranmu disetujui oleh administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Pendaftaranmu telah ditambahkan dalam daftar persetujuan. Kamu akan menerima email ketika pendaftaranmu disetujui oleh administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/id/topic.json b/public/language/id/topic.json index 6ba52cacce..fee041af64 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -26,12 +26,30 @@ "tools": "Perangkat", "flag": "Tandai", "locked": "Terkunci", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Tandai posting ini untuk moderasi", "flag_success": "Posting ini telah ditandai untuk moderasi", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Topik ini telah dihapus. Hanya pengguna dengan hak manajemen topik yang dapat melihatnya.", "following_topic.message": "Saat ini kamu akan menerima pemberitahuan saat seseorang membuat posting di dalam topik ini.", - "not_following_topic.message": "Kamu tidak lagi menerima notiifikasi dari topik ini.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Daftar atau login untuk berlangganan topik ini.", "markAsUnreadForAll.success": "Topik ditandai Belum Dibaca seluruhnya", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Beritahukan balasan baru untuk topik ini", "unwatch.title": "Berhenti memantau topik ini", "share_this_post": "Bagikan Posting ini", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Perangkat Topik", - "thread_tools.markAsUnreadForAll": "Tandai Belum Dibaca", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Tempel Topik", "thread_tools.unpin": "Copot Topik", "thread_tools.lock": "Kunci Topik", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Topik ini akan dipindahkan ke kategori", "fork_topic_instruction": "Klik posting yang kamu ingin cabangkan", "fork_no_pids": "Tidak ada posting yang dipilih!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Topik berhasil dicabangkan! Klik disini untuk menuju topik yang telah dicabangkan.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Masukkan judul topik di sini...", diff --git a/public/language/id/unread.json b/public/language/id/unread.json index 27ace0a281..f7bc4cf56b 100644 --- a/public/language/id/unread.json +++ b/public/language/id/unread.json @@ -6,5 +6,8 @@ "selected": "Terpilih", "all": "Semua", "all_categories": "Semua Kategori", - "topics_marked_as_read.success": "Topik ditandai sudah dibaca!" + "topics_marked_as_read.success": "Topik ditandai sudah dibaca!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/id/user.json b/public/language/id/user.json index 4610dd8f43..1d7e1a2ae1 100644 --- a/public/language/id/user.json +++ b/public/language/id/user.json @@ -6,6 +6,7 @@ "postcount": "Post Count", "email": "Email", "confirm_email": "Konfirmasi Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Topik per Halaman", "posts_per_page": "Posting per Halaman", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Pengaturan Penelusuran", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Gunakan Pencarian Di dalam Topik", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/id/users.json b/public/language/id/users.json index 7beb3ead46..23c5728a82 100644 --- a/public/language/id/users.json +++ b/public/language/id/users.json @@ -2,6 +2,7 @@ "latest_users": "Pengguna Terakhir", "top_posters": "Posting Terbanyak", "most_reputation": "Reputasi Terbanyak", + "most_flags": "Most Flags", "search": "Pencarian", "enter_username": "Masukkan nama pengguna untuk mencari", "load_more": "Tampilkan Lebih Banyak", diff --git a/public/language/it/category.json b/public/language/it/category.json index 2b41032053..a4dd47ce50 100644 --- a/public/language/it/category.json +++ b/public/language/it/category.json @@ -10,7 +10,11 @@ "share_this_category": "Condividi questa Categoria", "watch": "Osserva", "ignore": "Ignora", - "watch.message": "Non stai seguendo gli aggiornamenti di questa categoria", - "ignore.message": "Da ora saranno ignorati gli aggiornamenti di questa categoria", + "watching": "Seguito", + "ignoring": "Ignorato", + "watching.description": "Mostra discussione in non letti", + "ignoring.description": "Non mostrare discussione in non letti", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Categorie osservate" } \ No newline at end of file diff --git a/public/language/it/email.json b/public/language/it/email.json index 6dc40a7a16..f30f93b9f5 100644 --- a/public/language/it/email.json +++ b/public/language/it/email.json @@ -5,8 +5,8 @@ "greeting_no_name": "Ciao", "greeting_with_name": "Ciao %1", "welcome.text1": "Grazie per esserti registrato su %1!", - "welcome.text2": "Per attivare completamente il tuo account dobbiamo verificare che sei il proprietario dell'indiritto email con cui ti sei registrato.", - "welcome.text3": "Un Amministratora ha accettato la tua registrazione. Puoi adesso collegarti con il tuo nome utente e la tua password.", + "welcome.text2": "Per attivare completamente il tuo account dobbiamo verificare che sei il proprietario dell'indirizzo email con cui ti sei registrato.", + "welcome.text3": "Un Amministratora ha accettato la tua registrazione. Adesso puoi collegarti con il tuo nome utente/ password.", "welcome.cta": "Clicca qui per confermare il tuo indirizzo email", "invitation.text1": "%1 ti ha invitato a entrare in %2", "invitation.ctr": "Clicca qui per creare il tuo account.", @@ -17,17 +17,18 @@ "reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.", "reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.", "digest.notifications": "Hai una notifica non letta da %1:", - "digest.latest_topics": "Ultimi argomenti su %1", + "digest.latest_topics": "Ultime discussioni su %1", "digest.cta": "Clicca qui per visitare %1", "digest.unsub.info": "Questo sommario ti è stato inviato perché lo hai sottoscritto nelle tue impostazioni.", "digest.no_topics": "Non ci sono state discussioni attive nell'ultimo %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "giorno", + "digest.week": "settimana", + "digest.month": "mese", + "digest.subject": "Sommario per %1", "notif.chat.subject": "Nuovo messaggio in chat da %1", "notif.chat.cta": "Clicca qui per continuare la conversazione", "notif.chat.unsub.info": "Questa notifica di chat ti è stata inviata perché l'hai scelta nelle impostazioni.", - "notif.post.cta": "Clicca qui per leggere il topic completo.", + "notif.post.cta": "Clicca qui per leggere la discussione completa", "notif.post.unsub.info": "Questo post ti è stato notificato in base alle tue impostazioni di sottoscrizione.", "test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "unsub.cta": "Clicca qui per modificare queste impostazioni", diff --git a/public/language/it/error.json b/public/language/it/error.json index 25358c4310..b25b46dd30 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -14,22 +14,27 @@ "invalid-password": "Password non valida", "invalid-username-or-password": "Si prega di specificare sia un nome utente che una password", "invalid-search-term": "Termine di ricerca non valido", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "Non siamo riusciti a farti connettere, probabilmente perché la sessione è scaduta. Per favore riprova.", + "invalid-pagination-value": "Valore di paginazione non valido, deve essere almeno %1 ed al massimo %2", "username-taken": "Nome utente già preso", "email-taken": "Email già esistente", "email-not-confirmed": "La tua Email deve essere ancora confermata, per favore clicca qui per confermare la tua Email.", "email-not-confirmed-chat": "Non potrai chattare finchè non avrai confermato la tua email, per favore clicca qui per farlo ora.", + "email-not-confirmed-email-sent": "La tua email non è ancora stata confermata, per favore controlla la tua casella di posta elettronica per l'email di conferma.", "no-email-to-confirm": "Questo forum richiede la conferma dell'indirizzo email, per favore clicca qui per inserirne uno", "email-confirm-failed": "Non possiamo confermare la tua email, per favore prova ancora più tardi.", "confirm-email-already-sent": "Email di conferma già inviata, per favore attendere %1 minuti per richiederne un'altra.", + "sendmail-not-found": "Non è stato possibile trovare l'eseguibile sendmail, per favore assicurati che sia installato ed eseguibile dall'utente su cui è installato NodeBB.", "username-too-short": "Nome utente troppo corto", "username-too-long": "Nome utente troppo lungo", - "password-too-long": "Password too long", + "password-too-long": "Password troppo lunga", "user-banned": "Utente bannato", + "user-banned-reason": "Utente bannato (Motivo: %1)", "user-too-new": "Devi attendere %1 secondi prima di creare il tuo primo post", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Purtroppo il tuo indirizzo IP è stato bannato da questa community. Se credi che ci sia stato un errore contatta un amministratore.", + "ban-expiry-missing": "Per favore fornisci una data finale per questo ban", "no-category": "La Categoria non esiste", - "no-topic": "Il Topic non esiste", + "no-topic": "La discussione non esiste", "no-post": "Il Post non esiste", "no-group": "Il Gruppo non esiste", "no-user": "L'User non esiste", @@ -38,37 +43,55 @@ "category-disabled": "Categoria disabilitata", "topic-locked": "Discussione Bloccata", "post-edit-duration-expired": "Ti è consentito modificare un post per %1 secondi dopo averlo inviato", + "post-edit-duration-expired-minutes": "Ti è permesso modificare i post solo per %1 minuto(i) dopo averli inviati", + "post-edit-duration-expired-minutes-seconds": "Ti è permesso modificare i post solo per %1 minuto(i) %secondo(i) dopo averli inviati", + "post-edit-duration-expired-hours": "Ti è permesso modificare i post solo per %1 ora(e) dopo averli inviati", + "post-edit-duration-expired-hours-minutes": "Ti è permesso modificare i post solo per %1 ora(e) %2 minuto(i) dopo averli inviati", + "post-edit-duration-expired-days": "Ti è permesso modificare i post solo per %1 giorno(i) dopo averli inviati", + "post-edit-duration-expired-days-hours": "Ti è permesso modificare i post solo per %1 giorno(i) %2 ora(e) dopo averli inviati", + "post-delete-duration-expired": "Ti è permesso eliminare i post solo per %1 secondo(i) dopo averli inviati", + "post-delete-duration-expired-minutes": "Ti è permesso eliminare i post solo per %1 minuto(i) dopo averli inviati", + "post-delete-duration-expired-minutes-seconds": "Ti è permesso eliminare i post solo per %1 minuto(i) %secondo(i) dopo averli inviati", + "post-delete-duration-expired-hours": "Ti è permesso eliminare i post solo per %1 ora(e) dopo averli inviati", + "post-delete-duration-expired-hours-minutes": "Ti è permesso eliminare i post solo per %1 giorno(i) %2 ora(e) dopo averli inviati", + "post-delete-duration-expired-days": "Ti è permesso eliminare i post solo per %1 giorno(i) dopo averli inviati", + "post-delete-duration-expired-days-hours": "Ti è permesso eliminare i post solo per %1 giorno(i) %2 ora(e) dopo averli inviati", + "cant-delete-topic-has-reply": "Non puoi eliminare la tua discussione se ha una risposta", + "cant-delete-topic-has-replies": "Non puoi eliminare la tua discussione se ha %1 risposte", "content-too-short": "Inserisci un testo più lungo. Il messaggio deve contenere almeno %1 caratteri.", "content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.", "title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.", "title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.", + "category-not-selected": "Category not selected.", "too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", "tag-too-long": "Per favore inserisci un tag più corto. I tags non dovrebbero essere più lunghi di %1 caratteri", - "not-enough-tags": "Tag non sufficienti. Gli argomenti devono avere almeno %1 Tag", - "too-many-tags": "Troppi Tag. Gli argomenti non possono avere più di %1 Tag", + "not-enough-tags": "Tag non sufficienti. Le discussioni devono avere almeno %1 Tag", + "too-many-tags": "Troppi Tag. Le discussioni non possono avere più di %1 Tag", "still-uploading": "Per favore attendere il completamento degli uploads.", "file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "guest-upload-disabled": "Il caricamento da ospite è stato disattivato", + "already-favourited": "Hai già aggiunto questo post ai favoriti", + "already-unfavourited": "Hai già tolto questo post dai favoriti", "cant-ban-other-admins": "Non puoi bannare altri amministratori!", "cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo", + "cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.", "invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1", "invalid-image-extension": "Estensione immagine non valida", "invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1", "group-name-too-short": "Nome del Gruppo troppo corto", + "group-name-too-long": "Il nome del gruppo è troppo lungo", "group-already-exists": "Il Gruppo esiste già", "group-name-change-not-allowed": "Il cambio di nome al Gruppo non è consentito", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", + "group-already-member": "Fa già parte di questo gruppo", + "group-not-member": "Non è membro di questo gruppo", "group-needs-owner": "Questo gruppo richiede almeno un proprietario.", "group-already-invited": "Questo utente è già stato invitato", "group-already-requested": "La tua richiesta di partecipazione è già stata inviata", "post-already-deleted": "Questo Post è già stato cancellato", "post-already-restored": "Questo Post è già stato ripristinato", - "topic-already-deleted": "Questo Topic è già stato cancellato", + "topic-already-deleted": "Questo topic è già stato eliminato", "topic-already-restored": "Questo Topic è già stato ripristinato", "cant-purge-main-post": "Non puoi svuotare il primo messaggio, elimina invece l'intera discussione", "topic-thumbnails-are-disabled": "Le anteprime della Discussione sono disabilitate.", @@ -78,13 +101,14 @@ "about-me-too-long": "Spiacenti, il testo non può essere più lungo di %1 caratteri.", "cant-chat-with-yourself": "Non puoi chattare con te stesso!", "chat-restricted": "Questo utente ha ristretto i suoi messaggi in chat alle persone che segue. Per poter chattare con te ti deve prima seguire.", - "chat-disabled": "Chat system disabled", + "chat-disabled": "Il sistema di chat è stato disabilitato", "too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.", "invalid-chat-message": "Messaggio chat non valido", "chat-message-too-long": "Il messaggio chat è troppo lungo", - "cant-edit-chat-message": "You are not allowed to edit this message", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio", + "cant-remove-last-user": "Non puoi rimuovere l'ultimo utente", + "cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio", + "already-voting-for-this-post": "Hai già votato per questo post", "reputation-system-disabled": "Il sistema di reputazione è disabilitato.", "downvoting-disabled": "Il Downvoting è disabilitato", "not-enough-reputation-to-downvote": "Non hai i privilegi per votare negativamente questo post", @@ -95,9 +119,13 @@ "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server", "wrong-login-type-email": "Per favore usa la tua email per accedere", "wrong-login-type-username": "Per favore usa il tuo nome utente per accedere", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "invite-maximum-met": "Hai invitato il massimo numero di persone possibili (%1 su %2).", + "no-session-found": "Nessuna sessione valida di login trovata!", + "not-in-room": "L'utente non è in questa stanza", + "no-users-in-room": "Nessun utente in questa stanza", + "cant-kick-self": "Non puoi espellerti dal gruppo", + "no-users-selected": "Nessun utente selezionato", + "invalid-home-page-route": "Percorso della pagina iniziale non valido", + "invalid-session": "Discrepanza della sessione", + "invalid-session-text": "Sembra che la tua sessione non sia più attiva, oppure non corrisponde con il server. Per favore ricarica la pagina." } \ No newline at end of file diff --git a/public/language/it/global.json b/public/language/it/global.json index 3e19f1688b..d4754d8171 100644 --- a/public/language/it/global.json +++ b/public/language/it/global.json @@ -9,6 +9,8 @@ "404.message": "Sembra tu sia arrivato ad una pagina che non esiste. Torna alla <a href='%1/'>home page</a>.", "500.title": "Errore interno.", "500.message": "Oops! Qualcosa non funziona come si deve!", + "400.title": "Richiesta non valida.", + "400.message": "Sembra che questo link sia stato mal formulato, per favore ricontrolla e riprova. Altrimenti ritorna alla <a href='%1/'>home page</a>.", "register": "Registrazione", "login": "Login", "please_log_in": "Per favore Accedi", @@ -17,6 +19,7 @@ "welcome_back": "Bentornato", "you_have_successfully_logged_in": "Login avvenuto con successo", "save_changes": "Salva cambiamenti", + "save": "Save", "close": "Chiudi", "pagination": "Paginazione", "pagination.out_of": "%1 di %2", @@ -49,9 +52,11 @@ "users": "Utenti", "topics": "Discussioni", "posts": "Post", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", + "best": "Migliore", + "upvoters": "Hanno votato positivamente", + "upvoted": "Apprezzati", + "downvoters": "Hanno negato negativamente", + "downvoted": "Votato negativamente", "views": "Visualizzazioni", "reputation": "Reputazione", "read_more": "per saperne di più", @@ -59,15 +64,15 @@ "posted_ago_by_guest": "scritto %1 di Ospite", "posted_ago_by": "scritto %1 di %2", "posted_ago": "postato %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", + "posted_in": "postato in %1", + "posted_in_by": "postato in %1 da %2", "posted_in_ago": "postato in %1 %2", "posted_in_ago_by": "postato in %1 %2 da %3", "user_posted_ago": "%1 ha postato %2", "guest_posted_ago": "Ospite ha scritto %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "ultima modifica di %1", "norecentposts": "Nessun Post Recente", - "norecenttopics": "Nessun Argomento Recente", + "norecenttopics": "Nessuna Discussione Recente", "recentposts": "Post Recenti", "recentips": "Indirizzi IP Recentemente Loggati", "away": "Non disponibile", @@ -85,10 +90,13 @@ "unfollow": "Non seguire", "delete_all": "Elimina Tutto", "map": "Mappa", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "sessions": "Sessioni di Login", + "ip_address": "Indirizzo IP", + "enter_page_number": "Inserisci il numero della pagina", + "upload_file": "Carica file", + "upload": "Carica", + "allowed-file-types": "Le estensioni permesse dei file sono %1", + "unsaved-changes": "Hai delle modifiche non salvate. Sei sicuro che vuoi lasciare la pagina?", + "reconnecting-message": "Sembra che la tua connessione a %1 sia stata persa, per favore attenti mentre proviamo a riconnetterti.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/it/groups.json b/public/language/it/groups.json index ca2d7f898a..e79ff1c869 100644 --- a/public/language/it/groups.json +++ b/public/language/it/groups.json @@ -24,7 +24,7 @@ "details.has_no_posts": "I membri di questo gruppo non hanno ancora postato.", "details.latest_posts": "Ultimi Post", "details.private": "Privato", - "details.disableJoinRequests": "Disable join requests", + "details.disableJoinRequests": "Disabilita le richieste d'adesione", "details.grant": "Concedi / Rimuovi la Proprietà", "details.kick": "Espelli", "details.owner_options": "Amministratore del Grupo", @@ -41,7 +41,7 @@ "details.hidden": "Nascosto", "details.hidden_help": "Se abilitato, questo gruppo non sarà visibile nella lista dei gruppi e gli utenti dovranno essere invitati manualmente", "details.delete_group": "Elimina il Gruppo", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "I gruppi privati sono disabilitati dal livello del sistema, questa opzione non fa nulla", "event.updated": "I dettagli del Gruppo sono stati aggiornati", "event.deleted": "Il gruppo \"%1\" è stato eliminato", "membership.accept-invitation": "Accetta l'invito", @@ -50,5 +50,7 @@ "membership.leave-group": "Lascia il Gruppo", "membership.reject": "Rifiuta", "new-group.group_name": "Nome Gruppo:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Carica copertina del gruppo", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/it/login.json b/public/language/it/login.json index f04d6cdfb4..1561a8484b 100644 --- a/public/language/it/login.json +++ b/public/language/it/login.json @@ -5,7 +5,8 @@ "remember_me": "Ricordami?", "forgot_password": "Password dimenticata?", "alternative_logins": "Accessi Alternativi", - "failed_login_attempt": "Tentativo di accesso fallito, prova ancora.", + "failed_login_attempt": "Tentativo di accesso fallito", "login_successful": "Sei entrato con successo!", - "dont_have_account": "Non hai un account?" + "dont_have_account": "Non hai un account?", + "logged-out-due-to-inactivity": "Sei stato disconnesso dal Pannello di Controllo Amministratore per inattività" } \ No newline at end of file diff --git a/public/language/it/modules.json b/public/language/it/modules.json index fd58d80dd1..4f5fc93596 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -6,9 +6,9 @@ "chat.user_typing": "%1 sta scrivendo...", "chat.user_has_messaged_you": "%1 ti ha scritto.", "chat.see_all": "Vedi tutte le chat", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Segna tutti i messaggi come già letti", "chat.no-messages": "Si prega di selezionare un destinatario per vedere la cronologia dei messaggi", - "chat.no-users-in-room": "No users in this room", + "chat.no-users-in-room": "Nessun utente in questa stanza", "chat.recent-chats": "Chat Recenti", "chat.contacts": "Contatti", "chat.message-history": "Cronologia Messaggi", @@ -17,9 +17,9 @@ "chat.seven_days": "7 Giorni", "chat.thirty_days": "30 Giorni", "chat.three_months": "3 Mesi", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?", "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", + "chat.add-users-to-room": "Aggiungi utenti alla stanza", "composer.compose": "Componi", "composer.show_preview": "Visualizza Anteprima", "composer.hide_preview": "Nascondi Anteprima", @@ -28,11 +28,21 @@ "composer.discard": "Sei sicuro di voler scartare questo post?", "composer.submit_and_lock": "Invia e Blocca", "composer.toggle_dropdown": "Mostra/Nascondi menu a discesa", - "composer.uploading": "Uploading %1", + "composer.uploading": "Caricamento %1", + "composer.formatting.bold": "Grassetto", + "composer.formatting.italic": "Corsivo", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Barrato", + "composer.formatting.link": "Collegamento", + "composer.formatting.picture": "Immagine", + "composer.upload-picture": "Carica immagine", + "composer.upload-file": "Carica file", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Annulla", "bootbox.confirm": "Conferma", - "cover.dragging_title": "Cover Photo Positioning", - "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", - "cover.saved": "Cover photo image and position saved" + "cover.dragging_title": "Posizionando la foto copertina", + "cover.dragging_message": "Trascina l'immagine di copertina nella posizione desiderata e clicca su \"Salva\"", + "cover.saved": "Immagine di copertina e posizione salvati" } \ No newline at end of file diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index fc8f3bccc8..82fa25deec 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -5,32 +5,29 @@ "mark_all_read": "Segna tutte le notifiche come già lette", "back_to_home": "Indietro a %1", "outgoing_link": "Link in uscita", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "Stai lasciando %1", "continue_to": "Continua a %1", "return_to": "Ritorna a %1", "new_notification": "Nuova Notifica", "you_have_unread_notifications": "Hai notifiche non lette.", "new_message_from": "Nuovo messaggio da <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> ha votato positivamente il tuo post in <strong>%2</strong>.", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", - "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", + "upvoted_your_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> hanno apprezzato il tuo post in <strong>%3</strong>.", + "upvoted_your_post_in_multiple": "<strong>%1</strong> ed altri %2 hanno apprezzato il tuo post in <strong>%3</strong>.", + "moved_your_post": "<strong>%1</strong> ha spostato il tuo post su <strong>%2</strong>", + "moved_your_topic": "<strong>%1</strong> è stato spostato <strong>%2</strong>", "user_flagged_post_in": "<strong>%1</strong> ha segnalato un post in <strong>%2</strong>", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", + "user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un post in <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> ed altri %2 hanno segnalato un post in <strong>%3</strong>", "user_posted_to": "<strong>%1</strong> ha postato una risposta a: <strong>%2</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", - "user_posted_topic": "<strong>%1</strong> ha postato un nuovo Topic: <strong>%2</strong>", + "user_posted_to_dual": "<strong>%1</strong> e <strong>%2</strong> hanno postato una risposta su: <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> ed altri %2 hanno postato una risposta su: <strong>%3</strong>", + "user_posted_topic": "<strong>%1</strong> ha postato una nuova discussione: <strong>%2</strong>", "user_started_following_you": "<strong>%1</strong> ha iniziato a seguirti.", - "user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", + "user_started_following_you_dual": "<strong>%1</strong> e <strong>%2</strong> hanno iniziato a seguirti.", + "user_started_following_you_multiple": "<strong>%1</strong> ed altri %2 hanno iniziato a seguirti.", "new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "Ci sono <strong>%1</strong> richieste di registrazione che attendono di essere esaminate.", "email-confirmed": "Email Confermata", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", diff --git a/public/language/it/pages.json b/public/language/it/pages.json index dd6de7ed92..3e943ba0e7 100644 --- a/public/language/it/pages.json +++ b/public/language/it/pages.json @@ -6,17 +6,19 @@ "popular-month": "Discussioni popolari questo mese", "popular-alltime": "Discussioni più popolari di sempre", "recent": "Discussioni Recenti", - "flagged-posts": "Flagged Posts", + "flagged-posts": "Post Segnalati", "users/online": "Utenti Online", "users/latest": "Ultimi Utenti", "users/sort-posts": "Utenti maggiori contributori", "users/sort-reputation": "Utenti con la reputazione più alta", - "users/banned": "Banned Users", + "users/banned": "Utenti Bannati", + "users/most-flags": "Gli utenti più segnalati", "users/search": "Ricerca Utenti", "notifications": "Notifiche", "tags": "Tags", "tag": "Discussioni taggate \"%1\"", "register": "Registrati", + "registration-complete": "Registration complete", "login": "Autenticati", "reset": "Resetta password", "categories": "Categorie", @@ -25,22 +27,23 @@ "chats": "Chat", "chat": "In chat con %1", "account/edit": "Modificando \"%1\"", - "account/edit/password": "Editing password of \"%1\"", - "account/edit/username": "Editing username of \"%1\"", - "account/edit/email": "Editing email of \"%1\"", + "account/edit/password": "Modificando la password di \"%1\"", + "account/edit/username": "Modificando il nome utente di \"%1\"", + "account/edit/email": "Modificando l'email di \"%1\"", + "account/info": "Informazioni dell'account", "account/following": "Persone seguite da %1", "account/followers": "Persone che seguono %1", "account/posts": "Post creati da %1", "account/topics": "Discussioni create da %1", "account/groups": "Gruppi di %1", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Impostazioni Utente", "account/watched": "Discussioni osservate da %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "account/upvoted": "Post apprezzati da %1", + "account/downvoted": "Post votati negativamente da %1", + "account/best": "I migliori post di %1", + "confirm": "Email Confermata", "maintenance.text": "%1 è attualmente in manutenzione. Per favore ritorna più tardi.", "maintenance.messageIntro": "Inoltre, l'amministratore ha lasciato questo messaggio:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "throttled.text": "%1 non è al momento disponibile a causa di un carico eccessivo. Per favore ritorna più tardi." } \ No newline at end of file diff --git a/public/language/it/recent.json b/public/language/it/recent.json index c61e51f150..a32a7b5d3c 100644 --- a/public/language/it/recent.json +++ b/public/language/it/recent.json @@ -6,7 +6,7 @@ "year": "Anno", "alltime": "Sempre", "no_recent_topics": "Non ci sono discussioni recenti.", - "no_popular_topics": "Non ci sono argomenti popolari.", + "no_popular_topics": "Non ci sono discussioni popolari.", "there-is-a-new-topic": "C'è un nuovo topic.", "there-is-a-new-topic-and-a-new-post": "C'è un nuovo topic e un nuovo post.", "there-is-a-new-topic-and-new-posts": "C'è una nuova discussione e %1 nuovi post.", diff --git a/public/language/it/register.json b/public/language/it/register.json index 373ca07801..e3afd5aed2 100644 --- a/public/language/it/register.json +++ b/public/language/it/register.json @@ -1,5 +1,6 @@ { "register": "Registrazione", + "cancel_registration": "Cancella Registrazione", "help.email": "Come opzione predefinita, il tuo indirizzo email non verrà reso pubblico.", "help.username_restrictions": "Un nome utente unico, di almeno %1 caratteri e al massimo di %2. Gli altri utenti ti possono menzionare usando @<span id='yourUsername'>username</span>.", "help.minimum_password_length": "La lunghezza della password deve essere di almeno %1 caratteri.", @@ -15,5 +16,8 @@ "alternative_registration": "Altri metodi di registrazione", "terms_of_use": "Termini di Utilizzo", "agree_to_terms_of_use": "Accetto i Termini di Utilizzo", - "registration-added-to-queue": "La tua registrazione è stata aggiunta alla coda di moderazione. Riceverai una mail quando verrà accettata da un amministratore." + "terms_of_use_error": "Devi accettare i Termini d'Utilizzo", + "registration-added-to-queue": "La tua registrazione è stata aggiunta alla coda di moderazione. Riceverai una mail quando verrà accettata da un amministratore.", + "interstitial.intro": "Abbiamo bisogno di qualche informazione in più prima di poter creare il tuo account.", + "interstitial.errors-found": "Non abbiamo potuto completare la tua registrazione:" } \ No newline at end of file diff --git a/public/language/it/search.json b/public/language/it/search.json index a3e73da887..9234c43389 100644 --- a/public/language/it/search.json +++ b/public/language/it/search.json @@ -24,10 +24,10 @@ "one-year": "Un anno", "sort-by": "Ordina per", "last-reply-time": "Ora dell'ultima risposta", - "topic-title": "Titolo argomento", + "topic-title": "Titolo discussione", "number-of-replies": "Numero di risposte", "number-of-views": "Numero di visite", - "topic-start-date": "Data inizio argomento", + "topic-start-date": "Discussione iniziata", "username": "Nome utente", "category": "Categoria", "descending": "In ordine decrescente", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 8bbcf4c4c2..dff02fccca 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -5,7 +5,7 @@ "no_topics_found": "Nessuna discussione trovata!", "no_posts_found": "Nessun post trovato!", "post_is_deleted": "Questo post è eliminato!", - "topic_is_deleted": "Questa discussione é stata eliminata", + "topic_is_deleted": "Questa discussione è stata eliminata", "profile": "Profilo", "posted_by": "Pubblicato da: %1", "posted_by_guest": "Scritto da Ospite", @@ -26,23 +26,47 @@ "tools": "Strumenti", "flag": "Segnala", "locked": "Bloccato", - "bookmark_instructions": "Clicca qui per tornare all'ultimo post non letto in questo thread", + "pinned": "Appeso", + "moved": "Spostato", + "bookmark_instructions": "Clicca qui per tornare all'ultimo post letto in questa discussione.", "flag_title": "Segnala questo post per la moderazione", "flag_success": "Questo post è stato contrassegnato per la moderazione.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Questa discussione è stata cancellata. Solo gli utenti con diritti di gestione possono vederla.", "following_topic.message": "Da ora riceverai notifiche quando qualcuno posterà in questa discussione.", - "not_following_topic.message": "Non riceverai più notifiche da questa discussione.", + "not_following_topic.message": "Vedrai questa discussione nella lista delle discussioni non lette, ma non riceverai notifiche quando qualcuno risponde a questa discussione.", + "ignoring_topic.message": "Non vedrai più questa discussione tra la lista dei non letti. Sarai notificato in caso qualcuno ti menzioni o se un tuo post viene votato positivamente.", "login_to_subscribe": "Si prega di accedere o registrarsi per potersi iscrivere a questa discussione.", "markAsUnreadForAll.success": "Discussione segnata come non letta per tutti.", "mark_unread": "Segna come non letto", - "mark_unread.success": "Topic marked as unread.", + "mark_unread.success": "Discussione è stata marcata come non letta.", "watch": "Osserva", "unwatch": "Non osservare più", "watch.title": "Ricevi notifiche di nuove risposte in questa discussione", "unwatch.title": "Smetti di osservare questa discussione", "share_this_post": "Condividi questo Post", + "watching": "Seguito", + "not-watching": "Non Seguito", + "ignoring": "Ignorato", + "watching.description": "Notificami sulle nuove risposte.<br/>Mostra la discussione tra le non lette.", + "not-watching.description": "Non notificarmi sulle nuove risposte.<br/>Mostra la discussione fra le non lette se la categoria non è ignorata.", + "ignoring.description": "Non notificarmi sulle nuove risposte.<br/>Non mostrare la discussione fra le non lette.", "thread_tools.title": "Strumenti per la Discussione", - "thread_tools.markAsUnreadForAll": "Segna come non letto", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Fissa Discussione", "thread_tools.unpin": "Sblocca Discussione", "thread_tools.lock": "Blocca Discussione", @@ -52,7 +76,7 @@ "thread_tools.fork": "Dividi Discussione", "thread_tools.delete": "Elimina Discussione", "thread_tools.delete-posts": "Cancella post", - "thread_tools.delete_confirm": "Sei sicuro di voler cancellare questa discussione?", + "thread_tools.delete_confirm": "Sei sicuro di voler eliminare questa discussione?", "thread_tools.restore": "Ripristina Discussione", "thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?", "thread_tools.purge": "Svuota Discussione", @@ -65,9 +89,9 @@ "disabled_categories_note": "Le Categorie disabilitate sono in grigio", "confirm_move": "Sposta", "confirm_fork": "Dividi", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Favorito", + "favourites": "Favoriti", + "favourites.has_no_favourites": "Non hai ancora aggiunto nessun post tra i favoriti", "loading_more_posts": "Caricamento altri post", "move_topic": "Sposta Discussione", "move_topics": "Sposta Discussioni", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Questa discussione verrà spostata nella categoria", "fork_topic_instruction": "Clicca sui post che vuoi dividere", "fork_no_pids": "Nessun post selezionato!", + "fork_pid_count": "%1 post selezionati", "fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.", "delete_posts_instruction": "Clicca sui post che vuoi cancellare/eliminare", "composer.title_placeholder": "Inserisci qui il titolo della discussione...", @@ -84,7 +109,7 @@ "composer.discard": "Annulla", "composer.submit": "Invia", "composer.replying_to": "Rispondendo a %1", - "composer.new_topic": "Nuovo Argomento", + "composer.new_topic": "Nuova Discussione", "composer.uploading": "caricamento...", "composer.thumb_url_label": "Incolla l'URL di una immagine per la discussione", "composer.thumb_title": "Aggiungi un'immagine a questa discussione", @@ -101,10 +126,10 @@ "newest_to_oldest": "Da Nuovi a Vecchi", "most_votes": "Più votati", "most_posts": "Ulteriori post", - "stale.title": "Preferisci creare un nuovo topic?", + "stale.title": "Preferisci creare una nuova discussione?", "stale.warning": "Il topic al quale stai rispondendo è abbastanza vecchio. Vorresti piuttosto creare un nuovo topic in riferimento a questo nella tua risposta?", - "stale.create": "Crea un topic nuovo", - "stale.reply_anyway": "Rispondi a questo topic comunque", + "stale.create": "Crea una nuova discussione", + "stale.reply_anyway": "Rispondi comunque a questa discussione", "link_back": "Re: [%1](%2)", "spam": "Spam", "offensive": "Offensivo", diff --git a/public/language/it/unread.json b/public/language/it/unread.json index e2e6018a47..f03f876183 100644 --- a/public/language/it/unread.json +++ b/public/language/it/unread.json @@ -6,5 +6,8 @@ "selected": "Selezionati", "all": "Tutti", "all_categories": "Tutte le categorie", - "topics_marked_as_read.success": "Discussione marcata come letta!" + "topics_marked_as_read.success": "Discussione marcata come letta!", + "all-topics": "Tutte le Discussioni", + "new-topics": "Nuova Discussione", + "watched-topics": "Discussioni seguite" } \ No newline at end of file diff --git a/public/language/it/uploads.json b/public/language/it/uploads.json index 1622cb5693..fcc163f5c7 100644 --- a/public/language/it/uploads.json +++ b/public/language/it/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Sto caricando il file...", + "select-file-to-upload": "Seleziona un file da caricare!", + "upload-success": "File caricato con successo!", + "maximum-file-size": "Massimo %1 kb" } \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json index 6202ea0bd8..1562ecfc2f 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -6,6 +6,7 @@ "postcount": "Numero post", "email": "Email", "confirm_email": "Conferma Email", + "account_info": "Informazioni dell'account", "ban_account": "BAN dell'account", "ban_account_confirm": "Sei sicuro di voler bannare questo utente?", "unban_account": "Togli il BAN", @@ -22,7 +23,7 @@ "profile": "Profilo", "profile_views": "Visite al profilo", "reputation": "Reputazione", - "favourites": "Bookmarks", + "favourites": "Preferiti", "watched": "Osservati", "followers": "Da chi è seguito", "following": "Chi segue", @@ -30,17 +31,17 @@ "signature": "Firma", "birthday": "Data di nascita", "chat": "Chat", - "chat_with": "Chat with %1", + "chat_with": "Chatta con %1", "follow": "Segui", "unfollow": "Smetti di seguire", "more": "Altro", "profile_update_success": "Profilo aggiornato correttamente!", "change_picture": "Cambia Foto", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Modifica il nome utente", + "change_email": "Modifica Email", "edit": "Modifica", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Modifica Profilo", + "default_picture": "Icona di default", "uploaded_picture": "Foto caricata", "upload_new_picture": "Carica una nuova foto", "upload_new_picture_from_url": "Carica nuova immagine da URL", @@ -56,54 +57,73 @@ "password": "Password", "username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è <strong>%1</strong>", "password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.", "upload_picture": "Carica foto", "upload_a_picture": "Carica una foto", "remove_uploaded_picture": "Elimina foto caricata", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Carica immagine di copertina", "settings": "Impostazioni", "show_email": "Mostra la mia Email", - "show_fullname": "Vedi il Mio Nome Completo", + "show_fullname": "Mostra il mio nome completo", "restrict_chats": "Abilita messaggi in chat soltanto dagli utenti che seguo", "digest_label": "Iscriviti al Sommario", - "digest_description": "Abbonati agli aggiornamenti via email di questo forum (nuove notifiche e argomenti) secondo una pianificazione impostata", + "digest_description": "Abbonati agli aggiornamenti via email di questo forum (nuove notifiche e discussioni) secondo una pianificazione impostata", "digest_off": "Spento", "digest_daily": "Quotidiano", "digest_weekly": "Settimanale", "digest_monthly": "Mensile", "send_chat_notifications": "Invia una email se arriva un nuovo messaggio di chat e non sono online", - "send_post_notifications": "Invia una email quando le risposte sono fatte a discussioni a cui sono sottoscritto", + "send_post_notifications": "Invia una email quando ci sono nuove risposte a discussioni a cui sono sottoscritto", "settings-require-reload": "Alcuni cambiamenti di impostazioni richiedono un ricaricamento. Clicca qui per ricaricare la pagina.", "has_no_follower": "Questo utente non è seguito da nessuno :(", "follows_no_one": "Questo utente non segue nessuno :(", "has_no_posts": "Questo utente non ha ancora scritto niente.", "has_no_topics": "Questo utente non ha ancora avviato discussioni.", "has_no_watched_topics": "Questo utente non sta osservando discussioni.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.", + "has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post", + "has_no_voted_posts": "Questo utente non ha post votati", "email_hidden": "Email Nascosta", "hidden": "nascosta", "paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi", "topics_per_page": "Discussioni per Pagina", "posts_per_page": "Post per Pagina", "notification_sounds": "Riproduci un suono quando si riceve una notifica", + "notifications_and_sounds": "Notifiche e Suoni", + "incoming-message-sound": "Suono messaggio in entrata", + "outgoing-message-sound": "Suono messaggio in uscita", + "notification-sound": "Suono di notifica", + "no-sound": "No sound", "browsing": "Impostazioni di Navigazione", "open_links_in_new_tab": "Apri i link web in una nuova pagina", "enable_topic_searching": "Abilita la ricerca negli argomenti", "topic_search_help": "Se abilitata, la ricerca negli argomenti ignorerà il comportamento predefinito del browser per consentirti di cercare all'interno delle discussioni, anziché soltanto nel contenuto visibile a schermo", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Segui le discussioni a cui hai risposto", - "follow_topics_you_create": "Segui le discussioni che hai iniziato", - "grouptitle": "Seleziona il titolo del gruppo che vorresti vedere", + "delay_image_loading": "Ritarda il caricamento delle immagini", + "image_load_delay_help": "Se selezionato, le immagini nelle discussioni non saranno caricate finché non sono visibili nello schermo", + "scroll_to_my_post": "Dopo aver postato una risposta, mostra il nuovo post", + "follow_topics_you_reply_to": "Segui le discussioni a cui rispondi", + "follow_topics_you_create": "Segui le discussioni che crei", + "grouptitle": "Titolo del Gruppo", "no-group-title": "Nessun titolo al gruppo", "select-skin": "Seleziona uno Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", - "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", + "select-homepage": "Seleziona una Pagina Iniziale", + "homepage": "Pagina iniziale", + "homepage_description": "Seleziona una pagina da usare come pagina iniziale o \"Nessuna\" per usare quella di default.", + "custom_route": "Percorso della Homepage personalizzato", + "custom_route_help": "Inserisci qui un nome percorso, senza nessuno slash precedente (p.es. \"recent\", o \"popular\")", "sso.title": "Servizi Single-Sign-On", "sso.associated": "Associa con", - "sso.not-associated": "Clicca qui per associare con" + "sso.not-associated": "Clicca qui per associare con", + "info.latest-flags": "Ultime Segnalazioni", + "info.no-flags": "Non è stato trovato nessun post segnalato", + "info.ban-history": "Storico dei Ban recenti", + "info.no-ban-history": "Questo utente non è mai stato bannato", + "info.banned-until": "Bannato fino %1", + "info.banned-permanently": "Bannato permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Non è stata data nessuna motivazione.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/it/users.json b/public/language/it/users.json index b4770d2df5..3ca0056679 100644 --- a/public/language/it/users.json +++ b/public/language/it/users.json @@ -2,6 +2,7 @@ "latest_users": "Ultimi Utenti", "top_posters": "Maggiori contributori", "most_reputation": "Reputazione più alta", + "most_flags": "Più segnalati", "search": "Cerca", "enter_username": "Inserisci il nome utente da cercare", "load_more": "Carica altri", @@ -16,5 +17,5 @@ "unread_topics": "Discussioni non lette", "categories": "Categorie", "tags": "Tag", - "no-users-found": "No users found!" + "no-users-found": "Nessun utente trovato!" } \ No newline at end of file diff --git a/public/language/ja/category.json b/public/language/ja/category.json index 33cf774f14..71161923cd 100644 --- a/public/language/ja/category.json +++ b/public/language/ja/category.json @@ -10,7 +10,11 @@ "share_this_category": "このカテゴリを共有する", "watch": "ウォッチする", "ignore": "無視する", - "watch.message": "このカテゴリの更新をウォッチする", - "ignore.message": "このカテゴリの更新を無視する", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "ウォッチ中のカテゴリ" } \ No newline at end of file diff --git a/public/language/ja/email.json b/public/language/ja/email.json index 86af5f6131..5cfeeb34f1 100644 --- a/public/language/ja/email.json +++ b/public/language/ja/email.json @@ -24,6 +24,7 @@ "digest.day": "日", "digest.week": "週", "digest.month": "月", + "digest.subject": "Digest for %1", "notif.chat.subject": "%1さんからの新しいチャットメッセージがあります。", "notif.chat.cta": "クリックして会話を続ける", "notif.chat.unsub.info": "このチャットの通知はあなたの購読設定により送られました。", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index 7deddf6c53..100b4c7099 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -14,20 +14,25 @@ "invalid-password": "無効なパスワード", "invalid-username-or-password": "ユーザー名とパスワードの両方を指定してください", "invalid-search-term": "無効な検索ワード", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "ユーザー名は既に使われています", "email-taken": "メールアドレスは既に使われています", "email-not-confirmed": "あなたのメールアドレスはまだ確認されていません。メールアドレスを確認するためにはここをクリックしてください。", "email-not-confirmed-chat": "チャットを行うにはメールアドレスの確認を行う必要があります。メールアドレスを確認するためにはここをクリックしてください。", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "このフォーラムを利用するにはメールアドレスの確認を行う必要があります。メールアドレスを確認するためにはここをクリックしてください。", "email-confirm-failed": "メールアドレスの確認が出来ませんでした。再度お試しください。", "confirm-email-already-sent": "確認のメールは既に送信されています。再度送信するには、%1分後に再度お試しください。", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "ユーザー名が短すぎます", "username-too-long": "ユーザー名が長すぎます", "password-too-long": "パスワードが長すぎます", "user-banned": "ユーザーは停止されています", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "申し訳ありません。登録後に投稿を行うには%1秒お待ち下さい。", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "カテゴリは存在しません", "no-topic": "トピックは存在しません", "no-post": "投稿は存在しません", @@ -38,10 +43,26 @@ "category-disabled": "この板は無効された", "topic-locked": "スレッドがロックされた", "post-edit-duration-expired": "あなたが%1秒後に投稿を編集する事が許されます", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "より長く投稿を書いて下さい。投稿にはせめて%1文字が必要です。", "content-too-long": "より短く投稿を書いて下さい。投稿が%1文字以上が許されません。", "title-too-short": "より長くタイトルを書いて下さい。タイトルはせめて%1文字が必要です。", "title-too-long": "より短くタイトルを書いて下さい。タイトルは%1文字以上が許されません。", + "category-not-selected": "Category not selected.", "too-many-posts": "あなたは%1秒間に一つの投稿しか許されます-少し待ってまた投稿してください", "too-many-posts-newbie": "あなたは%2評判を得ているまで、新しいユーザーとしては、一度だけごとに%1秒を投稿することができます - 再び投稿する前にお待ちください", "tag-too-short": "より長くタグを入力して下さい。タグはせめて%1文字が必要です。", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "ほかの管理者を停止することはできません!", "cant-remove-last-admin": "あなたが唯一の管理者です。管理者としてあなた自身を削除する前に、管理者として別のユーザーを追加します。", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "無効なイメージタイプです。許可された種類は: %1", "invalid-image-extension": "無効なイメージのエクステンション", "invalid-file-type": "無効なファイルタイプです。許可された種類は: %1", "group-name-too-short": "グループ名は短すぎます。", + "group-name-too-long": "Group name too long", "group-already-exists": "グループ名はすでに存在しています", "group-name-change-not-allowed": "グループ名の修正はできません", "group-already-member": "既にこのグループの一部であります", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/ja/global.json b/public/language/ja/global.json index 82cc2ea677..ef6a6a08c5 100644 --- a/public/language/ja/global.json +++ b/public/language/ja/global.json @@ -7,8 +7,10 @@ "403.login": "権限を持っている場合は<a href='%1/login'>ログイン</a>すると閲覧出来ます。", "404.title": "見つかりません", "404.message": "あなたは存在してないページを訪問してます。<a href='%1/'>ホームページ</a>に戻ります。", - "500.title": "内部エラー", + "500.title": "Internal Error.", "500.message": "何か問題が発生しているようです。", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "登録", "login": "ログイン", "please_log_in": "ログインください", @@ -17,6 +19,7 @@ "welcome_back": "おかえりなさい", "you_have_successfully_logged_in": "ログインできました", "save_changes": "保存する", + "save": "Save", "close": "閉じる", "pagination": "ページ", "pagination.out_of": "%2部の%1", @@ -50,7 +53,9 @@ "topics": "スレッド", "posts": "ポスト", "best": "最高", + "upvoters": "Upvoters", "upvoted": "投票増加", + "downvoters": "Downvoters", "downvoted": "投票減少", "views": "閲覧数", "reputation": "評価", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ja/groups.json b/public/language/ja/groups.json index d108373457..218fb64183 100644 --- a/public/language/ja/groups.json +++ b/public/language/ja/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "拒否", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/ja/language.json b/public/language/ja/language.json index e210a4285a..ecd635ee30 100644 --- a/public/language/ja/language.json +++ b/public/language/ja/language.json @@ -1,5 +1,5 @@ { "name": "日本語", - "code": "ja_JP", + "code": "ja", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/ja/login.json b/public/language/ja/login.json index 916593a30d..30e7097351 100644 --- a/public/language/ja/login.json +++ b/public/language/ja/login.json @@ -5,7 +5,8 @@ "remember_me": "ログイン情報を記憶", "forgot_password": "パスワードを忘れましたか?", "alternative_logins": "ほかのログイン方法", - "failed_login_attempt": "ログインに失敗しました.ユーザー名やパスワードをご確認ください。", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "ログインしました!", - "dont_have_account": "アカウントをもっていませんか?" + "dont_have_account": "アカウントをもっていませんか?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json index 204c392bbb..c4b8124768 100644 --- a/public/language/ja/modules.json +++ b/public/language/ja/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json index 5fff05542b..6af28822d1 100644 --- a/public/language/ja/notifications.json +++ b/public/language/ja/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/ja/pages.json b/public/language/ja/pages.json index 717c66327e..1b96f7efac 100644 --- a/public/language/ja/pages.json +++ b/public/language/ja/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "ほとんどの投稿を持つユーザー", "users/sort-reputation": "一番評価の高いユーザー", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "ユーザーを検索", "notifications": "通知", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "カテゴリ", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/ja/register.json b/public/language/ja/register.json index a19fd68a34..3d300df3d4 100644 --- a/public/language/ja/register.json +++ b/public/language/ja/register.json @@ -1,5 +1,6 @@ { "register": "登録", + "cancel_registration": "Cancel Registration", "help.email": "初期設定ではメールアドレスは公開されません。", "help.username_restrictions": "%1から%2 文字までのユニークなユーザー名.ツイッター(twitter)の@<span id='yourUsername'>username</span> 方式でメンションすることができます。", "help.minimum_password_length": "パスワードには最小 %1 文字が必要です。", @@ -15,5 +16,8 @@ "alternative_registration": "ほかの登録方法", "terms_of_use": "利用規約", "agree_to_terms_of_use": "利用規約に同意する", - "registration-added-to-queue": "あなたの登録申請は承認キューに追加されました。管理者によって受け入れられた時に、メールを受信します。" + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "あなたの登録申請は承認キューに追加されました。管理者によって受け入れられた時に、メールを受信します。", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index f24579e205..0493792872 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -26,12 +26,30 @@ "tools": "ツール", "flag": "フラグ", "locked": "ロック", - "bookmark_instructions": "ここを押して、最後の未読の投稿戻ります", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "リポートする", "flag_success": "このポストをリポートしました。", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "このトピックが削除されました。トピック管理権を持っているユーザーにしか読めません。", "following_topic.message": "このスレッドが更新された際に通知を受け取ります。", - "not_following_topic.message": "このスレッドの更新通知を停止しました。", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "このスレッドを購読するためにログインが必要です。", "markAsUnreadForAll.success": "すべてのスレッドを未読にしました。", "mark_unread": "未読として示す", @@ -41,8 +59,14 @@ "watch.title": "新しいポストの通知を受ける", "unwatch.title": "このトピックの通知を停止します", "share_this_post": "ポストを共有", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "トピックのツール", - "thread_tools.markAsUnreadForAll": "未読にする", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "スレッドを最上部に固定", "thread_tools.unpin": "スレッドの固定を解除", "thread_tools.lock": "スレッドをロック", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "スレッドはこちらのカテゴリへ移動", "fork_topic_instruction": "フォークしたいポストをクリックして", "fork_no_pids": "ポストが選択されていません!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "トピックをフォークするのに成功しました。ここを押して、このフォークしたトピックに行きます。", "delete_posts_instruction": "削除または粛清するには、当てはまる投稿を押してください", "composer.title_placeholder": "スレッドのタイトルを入力して...", diff --git a/public/language/ja/unread.json b/public/language/ja/unread.json index 66a8e02906..54446f4116 100644 --- a/public/language/ja/unread.json +++ b/public/language/ja/unread.json @@ -6,5 +6,8 @@ "selected": "選択済み", "all": "全て", "all_categories": "全てのカテゴリ", - "topics_marked_as_read.success": "すべてのスレッドを既読にしました。" + "topics_marked_as_read.success": "すべてのスレッドを既読にしました。", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 539809537d..f4220e7287 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -6,6 +6,7 @@ "postcount": "投稿数", "email": "メール", "confirm_email": "メールアドレスを確認", + "account_info": "Account Info", "ban_account": "BANアカウント", "ban_account_confirm": "本当にこのユーザーをBANしますか?", "unban_account": "禁止アカウント解除します", @@ -88,14 +89,21 @@ "topics_per_page": "ページ毎のスレッド数", "posts_per_page": "ページ毎のポスト数", "notification_sounds": "通知の時に音を鳴らします", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "ブラウジングの設定", "open_links_in_new_tab": "外リンクを新しいタブに開きます", "enable_topic_searching": "インートピックの検索を有効にします", "topic_search_help": "有効にしたら、インートピックの検索はブラウザの既定機能を無視して、スクリーンに示したよりトピック内からの全部を検索します", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "返信したトピックをフォローします", - "follow_topics_you_create": "投稿したトピックをフォローします", - "grouptitle": "好きなグループ名を選んで下さい", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "グループ名がありません", "select-skin": "スキンを選んで下さい", "select-homepage": "ホームページを選んで下さい", @@ -105,5 +113,17 @@ "custom_route_help": "ここにルート名を入れます。手前にスラッシュを入れないこと(例:\"recent\"、\"popular\"など)", "sso.title": "シングルサインオンサービス", "sso.associated": "関連付けられています", - "sso.not-associated": "ここを押して、関連付けられています" + "sso.not-associated": "ここを押して、関連付けられています", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/ja/users.json b/public/language/ja/users.json index feb467657d..65fafee4d4 100644 --- a/public/language/ja/users.json +++ b/public/language/ja/users.json @@ -2,6 +2,7 @@ "latest_users": "新しいユーザー", "top_posters": "最も投稿したユーザー", "most_reputation": "最も評価されたユーザー", + "most_flags": "Most Flags", "search": "検索", "enter_username": "検索するユーザー名を入力してください", "load_more": "もっと見る", diff --git a/public/language/ko/category.json b/public/language/ko/category.json index 6670e7b9c7..6e0cb5e177 100644 --- a/public/language/ko/category.json +++ b/public/language/ko/category.json @@ -10,7 +10,11 @@ "share_this_category": "이 카테고리를 공유", "watch": "관심 주제", "ignore": "관심 해제", - "watch.message": "이 카테고리에 올라오는 글을 주시하고 있습니다.", - "ignore.message": "이 카테고리에 올라오는 글을 무시합니다.", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "읽지 않은 주제를 표시합니다", + "ignoring.description": "읽지 않은 주제를 표시하지 않습니다", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "관심 카테고리" } \ No newline at end of file diff --git a/public/language/ko/email.json b/public/language/ko/email.json index ff07b4a927..07a668917b 100644 --- a/public/language/ko/email.json +++ b/public/language/ko/email.json @@ -24,6 +24,7 @@ "digest.day": "일", "digest.week": "주", "digest.month": "월", + "digest.subject": "%1님을 위한 다이제스트", "notif.chat.subject": "%1님이 대화 메시지를 보냈습니다.", "notif.chat.cta": "대화를 계속하려면 여기를 클릭하세요.", "notif.chat.unsub.info": "이 대화 알림은 사용자의 구독 설정에 따라 전송되었습니다.", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index edb0a3a88d..0934b4c439 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -14,20 +14,25 @@ "invalid-password": "올바르지 않은 비밀번호입니다.", "invalid-username-or-password": "사용자 이름과 패스워드를 모두 설정해주세요.", "invalid-search-term": "올바르지 않은 검색어입니다.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "올바르지 않은 값입니다. 최소 1%에서 최대 2%까지 설정해야 합니다.", "username-taken": "이미 사용 중인 사용자 이름입니다.", "email-taken": "이미 사용 중인 이메일입니다.", "email-not-confirmed": "아직 이메일이 인증되지 않았습니다. 여기를 누르면 인증 메일을 발송할 수 있습니다.", "email-not-confirmed-chat": "아직 이메일이 인증되지 않았습니다. 대화기능은 인증 후에 사용이 가능합니다.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "이메일 인증이 필요합니다. 이곳을 클릭하여 이메일 입력하세요.", "email-confirm-failed": "이메일 인증이 실패하였습니다. 잠시 후에 다시 시도하세요.", "confirm-email-already-sent": "인증 메일이 이미 발송되었습니다. %1 분 이후에 재 발송이 가능합니다.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "사용자 이름이 너무 짧습니다.", "username-too-long": "사용자 이름이 너무 깁니다.", "password-too-long": "패스워드가 너무 깁니다.", "user-banned": "차단된 사용자입니다.", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "죄송합니다, 첫 번째 게시물은 %1 초 후에 작성할 수 있습니다.", "blacklisted-ip": "죄송하지만, 당신의 IP는 이 커뮤니티로부터 차단되었습니다. 만약 에러라는 생각이 드신다면 관리자에게 연락해주세요.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "존재하지 않는 카테고리입니다.", "no-topic": "존재하지 않는 주제입니다.", "no-post": "존재하지 않는 게시물입니다.", @@ -38,10 +43,26 @@ "category-disabled": "비활성화된 카테고리입니다.", "topic-locked": "잠긴 주제입니다.", "post-edit-duration-expired": "게시물의 수정은 작성한 시간으로부터 %1초 후에 가능합니다.", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "게시물의 내용이 너무 짧습니다. 내용은 최소 %1자 이상이어야 합니다.", "content-too-long": "게시물의 내용이 너무 깁니다. 내용은 최대 %1자 이내로 작성할 수 있습니다.", "title-too-short": "제목이 너무 짧습니다. 제목은 최소 %1자 이상이어야 합니다.", "title-too-long": "제목이 너무 깁니다. 제목은 최대 %1자 이내로 작성할 수 있습니다.", + "category-not-selected": "Category not selected.", "too-many-posts": "새 게시물 작성은 %1초마다 가능합니다 - 조금 천천히 작성해주세요.", "too-many-posts-newbie": "신규 사용자는 %2 만큼의 인지도를 얻기 전까지 %1초마다 게시물을 작성할 수 있습니다. 조금 천천히 작성해주세요.", "tag-too-short": "꼬리표가 너무 짧습니다. 꼬리표는 최소 %1자 이상이어야 합니다.", @@ -55,10 +76,12 @@ "already-unfavourited": "이미 이 게시물을 북마크 해제했습니다.", "cant-ban-other-admins": "다른 관리자를 차단할 수 없습니다.", "cant-remove-last-admin": "귀하는 유일한 관리자입니다. 관리자를 그만두시기 전에 다른 사용자를 관리자로 선임하세요.", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "올바르지 않은 이미지입니다. 사용가능한 유형: %1", "invalid-image-extension": "올바르지 않은 이미지 확장자입니다.", "invalid-file-type": "올바르지 않은 파일 유형입니다. 사용가능한 유형: %1", "group-name-too-short": "그룹 이름이 너무 짧습니다.", + "group-name-too-long": "Group name too long", "group-already-exists": "이미 존재하는 그룹입니다.", "group-name-change-not-allowed": "그룹 이름의 변경은 불가합니다.", "group-already-member": "이미 이 그룹에 속해있습니다.", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "편집 할 수 있는 권한이 없습니다.", "cant-remove-last-user": "마지막 사용자를 삭제할 수 없습니다.", "cant-delete-chat-message": "메세지를 지울 권한이 없습니다.", + "already-voting-for-this-post": "이미 이 게시글에 투표하셨습니다.", "reputation-system-disabled": "인지도 기능이 비활성 상태입니다.", "downvoting-disabled": "비추천 기능이 비활성 상태입니다.", "not-enough-reputation-to-downvote": "인지도가 낮아 이 게시물에 반대할 수 없습니다.", @@ -99,5 +123,9 @@ "no-session-found": "로그인 세션을 찾을 수 없습니다.", "not-in-room": "없는 사용자입니다.", "no-users-in-room": "사용자가 없습니다.", - "cant-kick-self": "스스로 이 그룹을 탈퇴할 수 없습니다." + "cant-kick-self": "스스로 이 그룹을 탈퇴할 수 없습니다.", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/ko/global.json b/public/language/ko/global.json index 9669750309..ce67b536f6 100644 --- a/public/language/ko/global.json +++ b/public/language/ko/global.json @@ -7,8 +7,10 @@ "403.login": "<a href='/login'>로그인</a>되어 있는지 확인해 주세요.", "404.title": "페이지를 찾을 수 없습니다.", "404.message": "존재하지 않는 페이지에 접근하였습니다. <a href='%1/'>홈 페이지</a>로 이동합니다.", - "500.title": "내부 오류가 발생했습니다.", + "500.title": "Internal Error.", "500.message": "알 수 없는 오류가 발생했습니다.", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "회원가입", "login": "로그인", "please_log_in": "로그인해 주세요.", @@ -17,6 +19,7 @@ "welcome_back": "환영합니다.", "you_have_successfully_logged_in": "성공적으로 로그인했습니다.", "save_changes": "저장", + "save": "Save", "close": "닫기", "pagination": "페이지", "pagination.out_of": "%2개 중 %1개", @@ -50,7 +53,9 @@ "topics": "주제", "posts": "게시물", "best": "베스트", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "비추됨", "views": "조회 수", "reputation": "인기도", @@ -90,5 +95,8 @@ "enter_page_number": "페이지 번호를 입력하세요", "upload_file": "파일 업로드", "upload": "업로드", - "allowed-file-types": "사용가능한 파일 유형: %1" + "allowed-file-types": "사용가능한 파일 유형: %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ko/groups.json b/public/language/ko/groups.json index 18eb6a0b07..266cee3877 100644 --- a/public/language/ko/groups.json +++ b/public/language/ko/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "그룹 나가기", "membership.reject": "거절", "new-group.group_name": "그룹명:", - "upload-group-cover": "그룹 커버 업로드" + "upload-group-cover": "그룹 커버 업로드", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/ko/login.json b/public/language/ko/login.json index 61413fcb8a..9245c84185 100644 --- a/public/language/ko/login.json +++ b/public/language/ko/login.json @@ -5,7 +5,8 @@ "remember_me": "로그인 유지", "forgot_password": "비밀번호 초기화", "alternative_logins": "다른 방법으로 로그인", - "failed_login_attempt": "로그인에 실패했습니다.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "성공적으로 로그인했습니다.", - "dont_have_account": "계정이 없으신가요?" + "dont_have_account": "계정이 없으신가요?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index 99aea82699..caca46f119 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "잠금 상태로 작성 완료", "composer.toggle_dropdown": "내려서 확인하기", "composer.uploading": "%1 업로드 중", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "확인", "bootbox.cancel": "취소", "bootbox.confirm": "확인", diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json index cf7b14f2b0..a6c9dd68e6 100644 --- a/public/language/ko/notifications.json +++ b/public/language/ko/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> 님과 다른 %2 명이 <strong>%3</strong>의 내 게시물을 추천했습니다.", "moved_your_post": "<strong>%1</strong>님이 귀하의 게시물을 <strong>%2</strong>로 옮겼습니다.", "moved_your_topic": "<strong>%1</strong> 이 <strong>%2</strong> 로 옮겨졌습니다.", - "favourited_your_post_in": "<strong>%1</strong>님이 <strong>%2</strong>의 내 게시물을 북마크 했습니다.", - "favourited_your_post_in_dual": "<strong>%1</strong> 님과 <strong>%2</strong> 님이 <strong>%3</strong>의 내 게시물을 북마크 했습니다.", - "favourited_your_post_in_multiple": "<strong>%1</strong> 님과 다른 %2 명이 <strong>%3</strong>의 내 게시물을 북마크 했습니다.", "user_flagged_post_in": "<strong>%1</strong>님이 <strong>%2</strong>의 게시물을 신고했습니다.", "user_flagged_post_in_dual": "<strong>%1</strong> 님과 <strong>%2</strong> 님이 <strong>%3</strong> 안의 게시물에 플래그를 세웠습니다.", "user_flagged_post_in_multiple": "<strong>%1</strong> 님과 %2 명의 다른 유저들이 <strong>%3</strong> 안의 게시물에 플래그를 세웠습니다.", diff --git a/public/language/ko/pages.json b/public/language/ko/pages.json index 65130b5b29..83267c472d 100644 --- a/public/language/ko/pages.json +++ b/public/language/ko/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "가장 많은 게시물을 작성한 사용자", "users/sort-reputation": "가장 높은 인지도를 가진 사용자", "users/banned": "차단된 유저", + "users/most-flags": "Most flagged users", "users/search": "사용자 검색", "notifications": "알림", "tags": "태그", "tag": "\"%1\" 꼬리표가 달린 주제", "register": "회원가입", + "registration-complete": "Registration complete", "login": "로그인", "reset": "패스워드 초기화", "categories": "카테고리", @@ -28,12 +30,13 @@ "account/edit/password": "\"%1\" 의 패스워드 변경", "account/edit/username": "\"%1\" 의 사용자명 변경", "account/edit/email": "\"%1\" 의 이메일 변경", + "account/info": "Account Info", "account/following": "%1 명이 팔로우", "account/followers": "%1 명을 팔로우", "account/posts": "%1 님이 작성한 게시물", "account/topics": "%1 님이 생성한 주제", "account/groups": "%1님의 그룹", - "account/favourites": "%1님의 북마크된 게시물", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "사용자 설정", "account/watched": "%1님이 지켜보는 주제", "account/upvoted": "%1 님이 upvote한 게시물", diff --git a/public/language/ko/register.json b/public/language/ko/register.json index b02a35f48e..a2be020b58 100644 --- a/public/language/ko/register.json +++ b/public/language/ko/register.json @@ -1,5 +1,6 @@ { "register": "회원가입", + "cancel_registration": "Cancel Registration", "help.email": "입력하신 이메일 주소는 공개되지 않으며, 설정을 통해 공개하실 수 있습니다.", "help.username_restrictions": "%1자 이상 %2자 이하의 고유한 이름을 입력하세요. @<span id='yourUsername'>username</span> 같은 방식으로 다른 사람들을 언급할 수 있습니다.", "help.minimum_password_length": "비밀번호는 최소 %1자로 제한됩니다.", @@ -15,5 +16,8 @@ "alternative_registration": "다른 방법으로 회원가입", "terms_of_use": "이용약관", "agree_to_terms_of_use": "이용약관에 동의합니다.", - "registration-added-to-queue": "회원가입이 요청되었습니다. 관리자의 승인 후 메일이 발송됩니다." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "회원가입이 요청되었습니다. 관리자의 승인 후 메일이 발송됩니다.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index 056ccb5a8f..1783ea7f83 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -26,12 +26,30 @@ "tools": "도구", "flag": "신고", "locked": "잠김", - "bookmark_instructions": "여기를 누르면 마지막으로 읽지 않은 포스트로 이동합니다.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "이 스레드에서 읽은 마지막 게시글로 이동하시려면 여기를 클릭하세요.", "flag_title": "이 게시물을 신고", "flag_success": "이 게시물은 신고되었습니다.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "이 주제는 삭제되었습니다. 주제 관리 권한이 있는 사용자만 볼 수 있습니다.", "following_topic.message": "이제 이 주제에 새 답글이 달리면 알림을 받습니다.", - "not_following_topic.message": "더 이상 이 주제의 새 답글을 알리지 않습니다.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "이 주제의 알림을 받기 위해서는 로그인해야 합니다.", "markAsUnreadForAll.success": "모든 사용자에 대해 읽지 않음으로 표시했습니다.", "mark_unread": "읽지 않음으로 표시", @@ -41,8 +59,14 @@ "watch.title": "이 주제의 새 답글 알리기", "unwatch.title": "이 주제에 대한 관심을 해제합니다.", "share_this_post": "이 게시물 공유", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "주제 관리", - "thread_tools.markAsUnreadForAll": "모두에게 읽지 않음으로 표시", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "상단 고정", "thread_tools.unpin": "상단 고정 해제", "thread_tools.lock": "잠금", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "이 주제를 지정한 카테고리로 이동합니다.", "fork_topic_instruction": "분리할 게시물을 선택하세요.", "fork_no_pids": "게시물이 선택되지 않았습니다.", + "fork_pid_count": "%1 post(s) selected", "fork_success": "주제가 분리되었습니다! 분리된 주제를 보려면 여기를 클릭하세요.", "delete_posts_instruction": "삭제할 게시물을 선택하세요.", "composer.title_placeholder": "여기에 제목을 입력하세요.", diff --git a/public/language/ko/unread.json b/public/language/ko/unread.json index 56f631af80..527a990c08 100644 --- a/public/language/ko/unread.json +++ b/public/language/ko/unread.json @@ -6,5 +6,8 @@ "selected": "선택된 주제", "all": "전체", "all_categories": "모든 카테고리", - "topics_marked_as_read.success": "성공적으로 읽음으로 표시했습니다." + "topics_marked_as_read.success": "성공적으로 읽음으로 표시했습니다.", + "all-topics": "모든 주제", + "new-topics": "새 주제", + "watched-topics": "읽은 주제" } \ No newline at end of file diff --git a/public/language/ko/user.json b/public/language/ko/user.json index b86ed76b19..14da55592c 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -6,6 +6,7 @@ "postcount": "게시물 작성 수", "email": "이메일", "confirm_email": "이메일 확인", + "account_info": "Account Info", "ban_account": "차단된 계정", "ban_account_confirm": "이 사용자를 차단하시겠습니까?", "unban_account": "차단 해제", @@ -88,14 +89,21 @@ "topics_per_page": "페이지 당 주제 수", "posts_per_page": "페이지 당 게시물 수", "notification_sounds": "알림 수신시 소리로 알려주기", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "페이지 열기", "open_links_in_new_tab": "외부 링크를 새로운 탭을 사용하여 열람", "enable_topic_searching": "주제 내 검색 허용", "topic_search_help": "활성화 된후 브라우저의 기본 페이지 검색 기능을 연관 주제 검색 기능으로 대신하고 화면에 보여지는 것 뿐만 아니라 주제와 연관된 모든것을 검색합니다.", + "delay_image_loading": "이미지 로딩 지연", + "image_load_delay_help": "활성화 되면 이미지 위치로 스크롤을 움직이기 전까지 이미지가 로드 되지 않습니다.", "scroll_to_my_post": "답글 게시 후 새 포스트 보여주기", - "follow_topics_you_reply_to": "답글 단 게시물을 팔로우 합니다.", - "follow_topics_you_create": "생성한 주제를 팔로우 합니다.", - "grouptitle": "표시할 그룹 이름을 선택하세요.", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "그룹 주제", "no-group-title": "그룹 이름이 없습니다.", "select-skin": "스킨 선택", "select-homepage": "홈페이지 선택", @@ -105,5 +113,17 @@ "custom_route_help": "라우팅 이름을 앞쪽 '/' 없이 입력해주세요 (예: \"최근 목록\", \"인기 게시물\")", "sso.title": "통합 인증 서비스", "sso.associated": "연관짓기 - ", - "sso.not-associated": "이 곳을 클리하여 연관지으세요." + "sso.not-associated": "이 곳을 클리하여 연관지으세요.", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/ko/users.json b/public/language/ko/users.json index a3ba794ca2..738cbe8f05 100644 --- a/public/language/ko/users.json +++ b/public/language/ko/users.json @@ -2,6 +2,7 @@ "latest_users": "가입일", "top_posters": "게시물 수", "most_reputation": "인기도", + "most_flags": "Most Flags", "search": "검색", "enter_username": "검색할 사용자 이름을 입력하세요.", "load_more": "더 보기", diff --git a/public/language/lt/category.json b/public/language/lt/category.json index f93c1a5a90..5bae8755ef 100644 --- a/public/language/lt/category.json +++ b/public/language/lt/category.json @@ -1,16 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", + "category": "Kategorija", + "subcategories": "Subkategorijos", "new_topic_button": "Nauja tema", "guest-login-post": "Prisijungti įrašų paskelbimui", "no_topics": "<strong>Šioje kategorijoje temų nėra.</strong><br/>Kodėl gi jums nesukūrus naujos?", "browsing": "naršo", - "no_replies": "Niekas dar neatsakė", - "no_new_posts": "No new posts.", + "no_replies": "Nėra atsakymų", + "no_new_posts": "Nėra naujų pranešimų.", "share_this_category": "Pasidalinti šią kategoriją", "watch": "Stebėti", - "ignore": "Nepaisyti", - "watch.message": "Jūs dabar stebite atnaujinimus iš šios kategorijos", - "ignore.message": "Jūs dabar ignoruojate atnaujinimus iš šios kategorijos", - "watched-categories": "Watched categories" + "ignore": "Ignoruoti", + "watching": "Stebima", + "ignoring": "Ignoruojama", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Stebimos kategorijos" } \ No newline at end of file diff --git a/public/language/lt/email.json b/public/language/lt/email.json index cd68d27182..d4f24c89e5 100644 --- a/public/language/lt/email.json +++ b/public/language/lt/email.json @@ -21,9 +21,10 @@ "digest.cta": "Kad aplankyti %1, spauskite čia", "digest.unsub.info": "Ši santrauka buvo išsiųsta į tavo prenumeratos nustatymus", "digest.no_topics": "Nebuvo aktyvių temų praeityje %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "diena", + "digest.week": "savaitė", + "digest.month": "mėnuo", + "digest.subject": "Digest for %1", "notif.chat.subject": "Nauja pokalbio žinutė gauta iš %1", "notif.chat.cta": "Pokalbio pratęsimui spauskite čia", "notif.chat.unsub.info": "Šios žinutės perpėjimas buvo išsiųstas į tavo prenumeratos nustatymus", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index e23088894e..f9e6d7772e 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -14,20 +14,25 @@ "invalid-password": "Klaidingas slaptažodis", "invalid-username-or-password": "Prašome nurodyti tiek vartotojo vardą, tiek ir slaptažodį", "invalid-search-term": "Neteisingas paieškos terminas", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Vartotojo vardas jau užimtas", "email-taken": "El. pašto adresas jau užimtas", "email-not-confirmed": "Jūsų el. paštas nepatvirtintas, prašome paspausti čia norint jį patvirtinti.", "email-not-confirmed-chat": "Jūs negalite bendrauti, kol jūsų el.paštas nėra patvirtintas, prašome spausti čia kad aktyvuoti jūsų el.paštą", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Šis forumas reikalauja patvirtinimo el. paštu prašome spausti čia el. adreso įrašymui", "email-confirm-failed": "Negalime patvirtinti jūsų el. adreso, prašom bandyti vėliau.", "confirm-email-already-sent": "Patvirtinimo laiškas išsiųstas, prašome palaukti %1 minute(s) kad išsiųstume kita", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Slapyvardis per trumpas", "username-too-long": "Vartotojo vardas per ilgas", "password-too-long": "Password too long", "user-banned": "Vartotojas užblokuotas", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Atsiprašome, jūs įpareigoti palaukti %1 sekunde(s) prieš rašant pirmą pranešimą", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Tokios kategorijos nėra", "no-topic": "Tokios temos nėra", "no-post": "Tokio įrašo nėra", @@ -38,10 +43,26 @@ "category-disabled": "Kategorija išjungta", "topic-locked": "Tema užrakinta", "post-edit-duration-expired": "Jums galima redaguoti pranešims tik %1 sekunde(s) po parašymo", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Prašome parašyti ilgesni pranešimą. Pranešimas turi sudaryti mažiausiai %1 simboli(us)", "content-too-long": "Prašome parašyti trumpesnį pranešimą. Pranešimas negali sudaryti daugiau %1 simboli(us)", "title-too-short": "Prašome įvesti ilgesni pavadinimą. Pavadinimas turi sudaryti mažiausiai %1 simboli(us)", "title-too-long": "Prašome įvesti trumpersnį pavadinimą. Pavadinimas negali sudaryti daugiau %1 simboli(us)", + "category-not-selected": "Category not selected.", "too-many-posts": "Jus galite rašyti kas %1 sekunde(s) - prašome palaukti prieš rašant dar kartą", "too-many-posts-newbie": "Kadangi esate naujas narys, jūs galite tik rašyti kas %1 sekunde(s) kol jūs pasieksite %2 reputacija - prašome palaukti prieš rašant dar kartą", "tag-too-short": "Prašome įvesti ilgesnę žymą. Žyma turi sudaryti mažiausiai %1 simboli(us)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Jūs negalite užblokuoti kitų administratorių!", "cant-remove-last-admin": "Jūs esate vienintelis administratorius. Pridėkite kitą vartotoja kaip administratorių prieš pašalindamas save", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Neteisingas vaizdo tipas. Leidžiami tipai :%1", "invalid-image-extension": "Neteisingas vaizdo plėtinys", "invalid-file-type": "Neteisingas failo tipas. Leidžiami tipai: %1", "group-name-too-short": "Grupės pavadinimas per trumpas", + "group-name-too-long": "Group name too long", "group-already-exists": "Tokia grupė jau egzistuoja", "group-name-change-not-allowed": "Grupės pavadinimas keitimas neleidžiamas", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputacijos sistema išjungta.", "downvoting-disabled": "Downvoting yra išjungtas", "not-enough-reputation-to-downvote": "Jūs neturite pakankamai reputacijos balsuoti prieš šį pranešimą", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/lt/global.json b/public/language/lt/global.json index 5ecfc4b4ab..763572f29d 100644 --- a/public/language/lt/global.json +++ b/public/language/lt/global.json @@ -7,8 +7,10 @@ "403.login": "Tikriausiai tu turėtum <a href='%1/login'>pabandyt prisijungt</a>?", "404.title": "Nerasta", "404.message": "Pasirodo sėdi puslapyje kurio net nėra. Grįžk į <a href='%1/'>namų puslapį</a>.", - "500.title": "Vidinė klaida.", + "500.title": "Internal Error.", "500.message": "Oops! Atrodo, kad kažkas nutiko!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registruotis", "login": "Prisijungti", "please_log_in": "Prašome prisijungti", @@ -17,6 +19,7 @@ "welcome_back": "Sveiki sugrįžę", "you_have_successfully_logged_in": "Jūs sėkmingai prisijungėte", "save_changes": "Išsaugoti pakeitimus", + "save": "Save", "close": "Uždaryti", "pagination": "Numeracija", "pagination.out_of": "%1 iš %2", @@ -50,7 +53,9 @@ "topics": "Temos", "posts": "Pranešimai", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Peržiūros", "reputation": "Reputacija", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/lt/groups.json b/public/language/lt/groups.json index cbb1b4118f..50ce9cc980 100644 --- a/public/language/lt/groups.json +++ b/public/language/lt/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Palikti Grupę", "membership.reject": "Atšaukti", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/lt/login.json b/public/language/lt/login.json index dd18d2949c..69a861749f 100644 --- a/public/language/lt/login.json +++ b/public/language/lt/login.json @@ -5,7 +5,8 @@ "remember_me": "Prisiminti?", "forgot_password": "Užmiršote slaptažodį?", "alternative_logins": "Alternatyvūs prisijungimo būdai", - "failed_login_attempt": "Prisijungti nepavyko, bandykite dar kartą.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Jūs sėkmingai prisijungėte!", - "dont_have_account": "Neturite paskyros?" + "dont_have_account": "Neturite paskyros?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json index 252dd3bfa3..579beeb269 100644 --- a/public/language/lt/modules.json +++ b/public/language/lt/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Pateikti ir užrakinti", "composer.toggle_dropdown": "Perjungti Nukritimą", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json index 640f9bde08..1e8f1b20bf 100644 --- a/public/language/lt/notifications.json +++ b/public/language/lt/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong>pagrįso nuomone čia <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/lt/pages.json b/public/language/lt/pages.json index 9eb817a26c..421fb51462 100644 --- a/public/language/lt/pages.json +++ b/public/language/lt/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Pranešimai", "tags": "Žymos", "tag": "Temos su šia žymą \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/lt/register.json b/public/language/lt/register.json index 52cf32608c..193d745318 100644 --- a/public/language/lt/register.json +++ b/public/language/lt/register.json @@ -1,5 +1,6 @@ { "register": "Registruotis", + "cancel_registration": "Cancel Registration", "help.email": "Pagal nutylėjimą, jūsų el. paštas nebus viešai matomas.", "help.username_restrictions": "Unikalus vartotojo vardas %1-%2 simbolių ilgio. Kiti vartotojai galės jus minėti @<span id='yourUsername'>vartotojas</span>.", "help.minimum_password_length": "Jūsų slaptažodis turi būti mažiausiai %1 simbolių.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatyvūs registracijos būdai", "terms_of_use": "Naudojimo sąlygos", "agree_to_terms_of_use": "Aš sutinku su vartojimo sąlygomis", - "registration-added-to-queue": "Jūsų registracijos prašymas buvo pridėtas į laukiančiųjų sąrašą. Jūs gausite el.paštu laišką kada administratorius patvirtins jus" + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Jūsų registracijos prašymas buvo pridėtas į laukiančiųjų sąrašą. Jūs gausite el.paštu laišką kada administratorius patvirtins jus", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index a12bdd4c4e..b2239c392d 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -26,12 +26,30 @@ "tools": "Įrankiai", "flag": "Pažymėti", "locked": "Užrakinta", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Pažymėti ši pranešimą moderatoriams", "flag_success": "Šis pranešimas buvo pažymėtas moderatorių patikrinimui.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Ši tema buvo ištrinta. Tik Vartotojai su temos redagavimo privilegijomis gali matyti ja", "following_topic.message": "Dabar jūs gausite pranešimus kai kas nors atrašys šioje temoje.", - "not_following_topic.message": "Jūs daugiau negausite pranešimų iš šios temos.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Norėdami prenumeruoti šią temą, prašome prisiregistruoti arba prisijungti.", "markAsUnreadForAll.success": "Tema visiems vartotojams pažymėta kaip neskaityta.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Gauti pranešimą apie naujus įrašus šioje temoje", "unwatch.title": "Baigti šios temos stebėjimą", "share_this_post": "Dalintis šiuo įrašu", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Temos priemonės", - "thread_tools.markAsUnreadForAll": "Pažymėti kaip neskaitytą", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Prisegti temą", "thread_tools.unpin": "Atsegti temą", "thread_tools.lock": "Užrakinti temą", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Ši tema bus perkelta į kategoriją", "fork_topic_instruction": "Pažymėkite ant įrašų, kuriuos norite perkelti į naują temą", "fork_no_pids": "Nepasirinktas joks įrašas!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Sėkmingai išsišakota iš temos! Spausk čia kad nueitu į išsišakota temą", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Įrašykite temos pavadinimą...", diff --git a/public/language/lt/unread.json b/public/language/lt/unread.json index 37c42dc40d..88b8b1464f 100644 --- a/public/language/lt/unread.json +++ b/public/language/lt/unread.json @@ -6,5 +6,8 @@ "selected": "Pasirinkti", "all": "Visi", "all_categories": "Visos kategorijos", - "topics_marked_as_read.success": "Temos pažymėtos kaip perskaitytos." + "topics_marked_as_read.success": "Temos pažymėtos kaip perskaitytos.", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/lt/uploads.json b/public/language/lt/uploads.json index 1622cb5693..a838cdf68d 100644 --- a/public/language/lt/uploads.json +++ b/public/language/lt/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "įkeliama...", + "select-file-to-upload": "Pasirinkite failą, kurį norite įkelti.", + "upload-success": "Failas įkeltas sėkmingai!", + "maximum-file-size": "Daugiausiai %1 kb" } \ No newline at end of file diff --git a/public/language/lt/user.json b/public/language/lt/user.json index 13f3b36b7b..92c82942ee 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -6,13 +6,14 @@ "postcount": "Įrašų kiekis", "email": "El. paštas", "confirm_email": "Patvirtinti el. paštą", + "account_info": "Paskyros informacija", "ban_account": "Užblokuoti Paskyrą", "ban_account_confirm": "Jūs tikrai norite užblokuoti šį vartotoją?", "unban_account": "Atblokuoti Paskyrą", "delete_account": "Ištrinti paskyrą", "delete_account_confirm": "Ar tikrai norite ištrinti savo paskyrą? <br /> <strong> Šis veiksmas yra negrįžtamas, ir jūs negalėsite susigrąžinti jokių duomenų </ strong> <br /> <br /> Įveskite savo vardą, kad patvirtintumėte, jog norite panaikinti šią paskyrą.", "delete_this_account_confirm": "Ar jūs tikrai norite ištrint šią paskyrą? <br /><strong>Šis veiksmas nebesugražinamas ir jūs nebegalėsite atgauti jokių duomenų</strong><br /><br />", - "account-deleted": "Account deleted", + "account-deleted": "Paskyra ištrinta", "fullname": "Vardas ir pavardė", "website": "Tinklalapis", "location": "Vieta", @@ -22,7 +23,7 @@ "profile": "Profilis", "profile_views": "Profilio peržiūros", "reputation": "Reputacija", - "favourites": "Bookmarks", + "favourites": "Žymės", "watched": "Peržiūrėjo", "followers": "Sekėjai", "following": "Seka", @@ -30,17 +31,17 @@ "signature": "Parašas", "birthday": "Gimimo diena", "chat": "Susirašinėti", - "chat_with": "Chat with %1", + "chat_with": "Susirašinėti su %1", "follow": "Sekti", "unfollow": "Nesekti", "more": "Daugiau", "profile_update_success": "Profilis sėkmingai atnaujintas!", "change_picture": "Pakeisti paveikslėlį", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Keisti vartotojo vardą", + "change_email": "Keisti el. pašto adresą", "edit": "Redaguoti", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Redaguoti profilį", + "default_picture": "Standartinis paveikslėlis", "uploaded_picture": "Įkeltas paveikslėlis", "upload_new_picture": "Įkelti naują paveikslėlį", "upload_new_picture_from_url": "Įkelti naują paveikslėlį iš URL", @@ -55,12 +56,12 @@ "confirm_password": "Patvirtinkite slaptažodį", "password": "Slaptažodis", "username_taken_workaround": "Jūsų norimas vartotojo vardas jau užimtas, todėl mes jį šiek tiek pakeitėme. Dabar jūs esate žinomas kaip <strong>%1</strong>", - "password_same_as_username": "Your password is the same as your username, please select another password.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_username": "Jūsų slaptažodis sutampa su Jūsų vartotojo vardu. Dėl saugumo, prašome naudoti kitą slaptažodį.", + "password_same_as_email": "Jūsų slaptažodis sutampa su Jūsų el. pašto adresu. Dėl saugumo, prašome naudoti kitą slaptažodį.", "upload_picture": "Įkelti paveikslėlį", "upload_a_picture": "Įkelti paveikslėlį", - "remove_uploaded_picture": "Remove Uploaded Picture", - "upload_cover_picture": "Upload cover picture", + "remove_uploaded_picture": "Ištrinti paveikslėlį", + "upload_cover_picture": "Įkelti viršelio nuotrauką", "settings": "Nustatymai", "show_email": "Rodyti mano el. paštą viešai", "show_fullname": "Rodyti mano vardą ir pavardę", @@ -79,31 +80,50 @@ "has_no_posts": "Šis vartotojas pakolkas neparašė jokių pranešimų", "has_no_topics": "Šis vartotojas pakolkas nesukūrė jokių temų", "has_no_watched_topics": "Šis vartotojas pakolkas nestebėjo jokių temų", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Šis narys dar neturi teigiamai įvertintų pranešimų.", + "has_no_downvoted_posts": "Šis narys dar neturi neigiamai įvertintų pranešimų.", + "has_no_voted_posts": "Šis narys dar neturi įvertintų pranešimų.", "email_hidden": "El. paštas paslėptas", "hidden": "paslėptas", "paginate_description": "Puslapiavimas temų ir pranešimų, vietoj kad naudoti judėjimą su pelytė į viršų ir į apačia", "topics_per_page": "Temų puslapyje", "posts_per_page": "Pranešimų puslapyje", "notification_sounds": "Paleisti garsą kai jūs gaunate pranešimą", + "notifications_and_sounds": "Pranešimai ir garsai", + "incoming-message-sound": "Gaunamos žinutės garsas", + "outgoing-message-sound": "Siunčiamos žinutės garsas", + "notification-sound": "Pranešimo garsas", + "no-sound": "No sound", "browsing": "Naršymo nustatymai", "open_links_in_new_tab": "Atidaryti išeinančias nuorodas naujam skirtuke", "enable_topic_searching": "Įjungti Temų Ieškojimą ", "topic_search_help": "Jeigu įjungtas, temų ieškojimas, nepaisys naršyklės puslapio ieškojimo, ir pradės ieškoti tik toje temoje kuri bus rodoma ekrane", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Sekti tas temas kur atrašai tu", - "follow_topics_you_create": "Sekti tas temas kurias sukuri tu", - "grouptitle": "Pasirinkite grupės pavadinimą kurį norėtumėte kad atvaizduotu", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "Jei įjungta, paveikslėliai temose nesikraus, kol nebus iki jų nuslinkta", + "scroll_to_my_post": "Po parašyto atsakymo, rodyti naują pranešimą", + "follow_topics_you_reply_to": "Peržiūrėti temas, kuriose Jūs atsakėte", + "follow_topics_you_create": "Peržiūrėti temas, kurias Jūs sukūrėte", + "grouptitle": "Grupės pavadinimas", "no-group-title": "Nėra grupės pavadinimo", - "select-skin": "Select a Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", + "select-skin": "Pasirinkite išvaizdą", + "select-homepage": "Pasirinkite pagrindinį puslapį", + "homepage": "Pagrindinis puslapis", + "homepage_description": "Pasirinkite puslapį kaip savo pagrindinį, arba pasirinkite \"Joks\" norėdami naudoti standartinį pagrindinį puslapį.", + "custom_route": "Pagrindinio puslapio vieta", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "Nerasta pažymėtų pranešimų", + "info.ban-history": "Blokavimų istorija", + "info.no-ban-history": "Šis narys nebuvo užblokuotas.", + "info.banned-until": "Užblokuotas iki %1", + "info.banned-permanently": "Užblokuotas visam laikui", + "info.banned-reason-label": "Priežastis", + "info.banned-no-reason": "Be priežasties", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/lt/users.json b/public/language/lt/users.json index e349dc32b6..ed6ea22471 100644 --- a/public/language/lt/users.json +++ b/public/language/lt/users.json @@ -2,6 +2,7 @@ "latest_users": "Paskutiniai vartotojai", "top_posters": "Geriausi autoriai", "most_reputation": "Didžiausia reputacija", + "most_flags": "Most Flags", "search": "Ieškoti", "enter_username": "Įrašykite vartotojo vardą paieškai", "load_more": "Įkelti daugiau", @@ -16,5 +17,5 @@ "unread_topics": "Neperskaitytos temos", "categories": "Kategorijos", "tags": "Žymos", - "no-users-found": "No users found!" + "no-users-found": "Nerasta vartotojų." } \ No newline at end of file diff --git a/public/language/ms/category.json b/public/language/ms/category.json index e345d4b65f..3b9d9455a4 100644 --- a/public/language/ms/category.json +++ b/public/language/ms/category.json @@ -10,7 +10,11 @@ "share_this_category": "Kongsi kategori ini", "watch": "Melihat", "ignore": "Abai", - "watch.message": "Anda sedang melihat kemaskini dari kategori ini", - "ignore.message": "Anda sedang mengabaikan kemaskini dari kategori ini", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Kategori Dilihat" } \ No newline at end of file diff --git a/public/language/ms/email.json b/public/language/ms/email.json index 01dc9523e3..eb5affbabd 100644 --- a/public/language/ms/email.json +++ b/public/language/ms/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Pesanan baru diterima dari %1", "notif.chat.cta": "Klik sini untuk meneruskan perbualan", "notif.chat.unsub.info": "Pemberitahuan sembang ini dihantar berdasarkan tetapan langganan anda.", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 7840393a92..1872e8eddd 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -14,20 +14,25 @@ "invalid-password": "Kata laluan salah!", "invalid-username-or-password": "Sila tentukan kedua-dua nama pengguna dan kata laluan", "invalid-search-term": "Terma pencarian tak sah", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Nombor halaman tidak sah, mesti tidak kurang dari %1 dan tidak lebih dari %2", "username-taken": "Nama pengguna telah digunakan", "email-taken": "Emel telah digunakan", "email-not-confirmed": "Emel anda belum disahkan lagi, sila klik sini untuk mengesahkan emel anda.", "email-not-confirmed-chat": "Anda tidak dibenarkan sembang sehingga emel disahkan, sila sahkan emel anda.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Forum ini memerlukan pengesahan emel, sila klik sini untuk memasukkan emel", "email-confirm-failed": "Kami tidak dapat memastikan emel anda, sila cuba lagi nanti", "confirm-email-already-sent": "Pengesahan emel telah dihantar, sila tunggu %1 minit() untuk menghantar yang baru.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Nama pengunna terlalu pendek", "username-too-long": "Nama pengunna terlalu panjang", "password-too-long": "Kata laluan terlalu panjang", "user-banned": "Pengguna diharamkan", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Maaf, anda dikehendaki menunggu %1 saat() sebelum membuat kiriman pertama anda", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategori tidak wujud", "no-topic": "Topik tidak wujud", "no-post": "Kiriman tidak wujud", @@ -38,10 +43,26 @@ "category-disabled": "Kategori dilumpuhkan", "topic-locked": "Topik Dikunci", "post-edit-duration-expired": "Anda hanya dibenarkan menyunting kiriman selepas %1 saat() berlalu", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Sila masukkan kiriman yang lebih panjang. Kiriman mesti mengandungi sekurang-kurangnya %1 aksara().", "content-too-long": "Sila masukkan kiriman yang lebih ringkas. Kiriman mesti mengandungi tidak lebih %1 aksara().", "title-too-short": "Sila masukkan tajuk yang lebih panjang. Tajuk mesti mengandungi sekurang-kurangnya %1 aksara().", "title-too-long": "Sila masukkan tajuk yang lebih ringkas. Tajuk mesti mengandungi tidak lebih %1 aksara().", + "category-not-selected": "Category not selected.", "too-many-posts": "Anda hanya boleh mengirim sekali setiap %1 saat() - sila tunggu sebelum kiriman seterusnya", "too-many-posts-newbie": "Sebagai pengguna baru, anda hanya boleh mengirim sekali setiap %1 saat() sehinnga anda mendapat %2 reputasi - sila tunggu sebelum kiriman seterusnya", "tag-too-short": "Sila masukkan tag yang lebih panjang. Tag mesti mengandungi sekurang-kurangnya %1 aksara()", @@ -55,10 +76,12 @@ "already-unfavourited": "Anda telah pun membuang penanda untuk kiriman ini", "cant-ban-other-admins": "Anda tidak boleh haramkan admin / pentadbir!", "cant-remove-last-admin": "Anda satu-satunya pentadbir. Tambah pentadbir lain sebelum membuang diri anda sebagai pentadbir", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Jenis imej tak sah. Jenis yang dibenarkan ialah: %1", "invalid-image-extension": "Sambungan imej tak sah", "invalid-file-type": "Jenis fail tak sah. Jenis fail yang dibenarkan ialah: %1", "group-name-too-short": "Nama kumpulan terlalu pendek", + "group-name-too-long": "Group name too long", "group-already-exists": "Kumpulan telah wujud", "group-name-change-not-allowed": "Pengubahan nama kumpulan tidak dibenarkan", "group-already-member": "Sudah pun sebahagian dari kumpulan ini", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Anda tidak dibenarkan menyunting mesej ini", "cant-remove-last-user": "Anda tidak boleh membuang pengguna akhir", "cant-delete-chat-message": "Anda tidak dibenarkan memadamkan mesej ini", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistem reputasi dilumpuhkan.", "downvoting-disabled": "Undi turun dilumpuhkan", "not-enough-reputation-to-downvote": "Anda tidak mempunyai reputasi mencukupi untuk mengundi turun kiriman ini", @@ -99,5 +123,9 @@ "no-session-found": "Tiada sesyen log masuk dijumpai", "not-in-room": "Pengguna tiada dalam bilik", "no-users-in-room": "Tiada pengguna dalam bilik ini", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/ms/global.json b/public/language/ms/global.json index 4e71e55dfa..12238c3c06 100644 --- a/public/language/ms/global.json +++ b/public/language/ms/global.json @@ -7,8 +7,10 @@ "403.login": "Mungkin anda boleh <a href='%1/login'>cuba log masuk</a>?", "404.title": "tidak dijumpai", "404.message": "Halaman yang diminta tidak wujud. Kembali ke <a href='%1/'>halaman utama</a>.", - "500.title": "ralat dalaman", + "500.title": "Internal Error.", "500.message": "Oops! Macam ada yang tidak kena", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Daftar", "login": "Log Masuk", "please_log_in": "Sila log masuk", @@ -17,6 +19,7 @@ "welcome_back": "Selamat kembali", "you_have_successfully_logged_in": "Anda telah berjaya log masuk", "save_changes": "Simpan perubahan", + "save": "Save", "close": "Tutup", "pagination": "Mukasurat", "pagination.out_of": "%1 daripada %2", @@ -50,7 +53,9 @@ "topics": "Topik", "posts": "Kiriman", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Lihat", "reputation": "Reputasi", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ms/groups.json b/public/language/ms/groups.json index 3c84b65c91..df34778d33 100644 --- a/public/language/ms/groups.json +++ b/public/language/ms/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Keluar Kumpulan", "membership.reject": "Tolak", "new-group.group_name": "Nama Kumpulan:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/ms/login.json b/public/language/ms/login.json index 7536a7c973..a9de503fdf 100644 --- a/public/language/ms/login.json +++ b/public/language/ms/login.json @@ -5,7 +5,8 @@ "remember_me": "Ingatkan Saya", "forgot_password": "Lupa Kata Laluan?", "alternative_logins": "Log Masuk Alternatif", - "failed_login_attempt": "Log masuk gagal, sila cuba lagi.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Anda berjaya log masuk!", - "dont_have_account": "Tiada akaun?" + "dont_have_account": "Tiada akaun?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json index 996443d174..7ae33cbc09 100644 --- a/public/language/ms/modules.json +++ b/public/language/ms/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Hantar dan Kunci", "composer.toggle_dropdown": "Togol Kebawah", "composer.uploading": "Memuat naik %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "Ok", "bootbox.cancel": "Batal", "bootbox.confirm": "Pasti", diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json index dab972b82d..722b7f3482 100644 --- a/public/language/ms/notifications.json +++ b/public/language/ms/notifications.json @@ -13,15 +13,12 @@ "new_message_from": "Pesanan baru daripada <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> telah mengundi naik kiriman and di <strong>%2</strong>.", "upvoted_your_post_in_dual": "<strong>%1</strong>dan <strong>%2</strong> telah menambah undi pada kiriman anda di <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> dan %2 lagi telah menambah undi pada kiriman anda di <strong>%3</strong>. ", + "upvoted_your_post_in_multiple": "<strong>%1</strong> dan %2 lagi telah menambah undi pada kiriman anda di <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> telah memindahkan kiriman anda ke <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> telah memindahkan <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> menanda kiriman anda di <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> dan <strong>%2</strong> telah menanda kiriman anda pada <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> dan %2 lagi telah mendanda kiriman anda pada <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> dan %2 lagi telah menanda kiriman anda pada <strong>%3</strong>", "user_posted_to": "<strong>%1</strong> telah membalas kiriman kepada: <strong>%2</strong>", "user_posted_to_dual": "<strong>%1</strong> dan <strong>%2</strong> membalas kiriman : <strong>%3</strong>", "user_posted_to_multiple": "<strong>%1</strong> dan %2 lagu membalas kiriman: <strong>%3</strong>", @@ -30,7 +27,7 @@ "user_started_following_you_dual": "<strong>%1</strong> dan <strong>%2</strong> mula mengikuti anda.", "user_started_following_you_multiple": "<strong>%1</strong> dan %2 lagi mula mengikuti anda.", "new_register": "<strong>%1</strong> menghantar jemputan pendaftaran.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "Ada <strong>%1</strong> permohonan ingin daftar yang sedang menunggu pengesahan.", "email-confirmed": "Emel Disahkan", "email-confirmed-message": "Terima kasih kerana mengesahkan emel anda. Akaun anda telah diaktifkan sepenuhnya.", "email-confirm-error-message": "Berlaku masalah semasa mengesahkan emel anda. Mungkin kod tidak sah atau tamat tempoh.", diff --git a/public/language/ms/pages.json b/public/language/ms/pages.json index c215ed4197..516187f424 100644 --- a/public/language/ms/pages.json +++ b/public/language/ms/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Pengguna Mengikut Kiriman Terbanyak", "users/sort-reputation": "Pengguna Mengikut Reputasi Terbanyak", "users/banned": "Pengguna Diharam", + "users/most-flags": "Most flagged users", "users/search": "Carian Pengguna", "notifications": "Makluman", "tags": "Tag", "tag": "Topik ditag di bawwah \"%1\"", "register": "Daftar Akaun", + "registration-complete": "Registration complete", "login": "Log Masuk Ke Akaun Anda", "reset": "Set Semula Kata Laluan", "categories": "Kategori", @@ -28,12 +30,13 @@ "account/edit/password": "Mengemaskini kata laluan \"%1\"", "account/edit/username": "Mengemaskini nama pengguna \"%1\"", "account/edit/email": "Mengemaskini email \"%1\"", + "account/info": "Account Info", "account/following": "Mengikut %1 orang", "account/followers": "Diikuti oleh %1", "account/posts": "Kiriman oleh %1", "account/topics": "Topik olej %1", "account/groups": "Kumpulan %1", - "account/favourites": "Kiriman Ditanda Oleh %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Tetapan Pengguna", "account/watched": "Topik Diperhati Oleh %1", "account/upvoted": "Kiriman diundi naik oleh %1", diff --git a/public/language/ms/register.json b/public/language/ms/register.json index b0a6d79507..9079daca41 100644 --- a/public/language/ms/register.json +++ b/public/language/ms/register.json @@ -1,5 +1,6 @@ { "register": "Mendaftar", + "cancel_registration": "Cancel Registration", "help.email": "E-mel akan disembunyikan daripada orang ramai.", "help.username_restrictions": "Cuba satu nama pengguna yang unik di antara %1 dan %2 aksara. Orang lain boleh menyebut anda dengan @<span id='yourUsername'>nama pengguna</ span>.", "help.minimum_password_length": "Panjang kata laluan anda hendaklah sekurang-kurangnya %1 aksara.", @@ -15,5 +16,8 @@ "alternative_registration": "Pendaftaran Alternatif", "terms_of_use": "Terma Penggunaan", "agree_to_terms_of_use": "Saya bersetuju dengan Terma Penggunaan", - "registration-added-to-queue": "Pendaftaran anda sedang dimasukkan ke barisan pengesahan. Anda akan menerima emel setelah diterima oleh pentadbir." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Pendaftaran anda sedang dimasukkan ke barisan pengesahan. Anda akan menerima emel setelah diterima oleh pentadbir.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index 8c12c5a409..266fd059f6 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -26,12 +26,30 @@ "tools": "Perkakas", "flag": "Tanda", "locked": "Kunci", - "bookmark_instructions": "Klik disini untuk kembali ke kiriman terakhir yang anda telah baca di thread ini.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Tanda kiriman ini untuk diselia", "flag_success": "Kiriman ini telah ditandakan untuk diselia", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Topik ini telah dipadam. Hanya pengguna dengan kuasa pengurusan boleh melihatnya.", "following_topic.message": "Anda akan menerima makluman apabila ada kiriman ke dalam topik ini", - "not_following_topic.message": "Anda tidak lagi akan menerima makluman daripada topik ini", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Sila daftar atau log masuk untuk melanggani topik ini", "markAsUnreadForAll.success": "Topik ditanda sebagai belum dibaca untuk semua", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Akan dimaklumkan sekiranya ada balasan dalam topik ini", "unwatch.title": "Berhenti melihat topik ini", "share_this_post": "Kongsi kiriman ini", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Perkakas Topik", - "thread_tools.markAsUnreadForAll": "Tanda sebagai belum dibaca", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pinkan topik", "thread_tools.unpin": "Batalkan pin topik", "thread_tools.lock": "Kunci topik", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Topik ini akan dipindahkan kepada kategori", "fork_topic_instruction": "Klik kiriman yang anda hendak salin", "fork_no_pids": "Tiada kiriman yang dipilih", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Berjaya menyalin topik. Klik sini untuk ke topik yang disalin.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Masukkan tajuk topik disini", diff --git a/public/language/ms/unread.json b/public/language/ms/unread.json index 6e23443905..3e132e6f2c 100644 --- a/public/language/ms/unread.json +++ b/public/language/ms/unread.json @@ -6,5 +6,8 @@ "selected": "Dipilih", "all": "Semua", "all_categories": "Semua Kategori", - "topics_marked_as_read.success": "Topik ditandakan sebagai sudah dibaca" + "topics_marked_as_read.success": "Topik ditandakan sebagai sudah dibaca", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/ms/uploads.json b/public/language/ms/uploads.json index 1622cb5693..c637c43fad 100644 --- a/public/language/ms/uploads.json +++ b/public/language/ms/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Sedang memuatnaik fail...", + "select-file-to-upload": "Pilih fail yang hendak dimuatnaik!", + "upload-success": "Muatnaik fail berjaya!", + "maximum-file-size": "Maksima %1 kb" } \ No newline at end of file diff --git a/public/language/ms/user.json b/public/language/ms/user.json index 39d38cc538..ba0db632de 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -6,6 +6,7 @@ "postcount": "Jumlah Kiriman", "email": "Emel", "confirm_email": "Pastikan Emel", + "account_info": "Account Info", "ban_account": "Haramkan Akaun", "ban_account_confirm": "Adakah anda pasti ingin menyekat pengguna ini?", "unban_account": "Buang Sekatan Akaun", @@ -88,14 +89,21 @@ "topics_per_page": "Topik setiap muka", "posts_per_page": "Kiriman setiap muka", "notification_sounds": "Mainkan bunyi apabila anda mendapat pemberitahuan", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Melihat-lihat Tetapan", "open_links_in_new_tab": "Buka pautan luar di tab yang baru", "enable_topic_searching": "Aktifkan Pencarian Dalam-Topik", "topic_search_help": "Jika diaktifkan, pencarian dalam-topik akan membatalkan fungsi asal pencarian pelayan dan membenarkan anda untuk mencari seluruh topik, daripada menunjukkan apa yang terdapat pada skrin sahaja", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Ikut topik yang anda balas", - "follow_topics_you_create": "Ikut topik yang anda buat", - "grouptitle": "Pilih nama kumpulan yang anda ingin tunjukkan", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "Tiada nama kumpulan", "select-skin": "Pilih skin", "select-homepage": "Pilih Laman Utama", @@ -105,5 +113,17 @@ "custom_route_help": "Masukkan nama laluan disini, tanpa garis miring (cth: \"terkini\", atau \"popular\")", "sso.title": "Servis Satu Log Masuk", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/ms/users.json b/public/language/ms/users.json index feb8d17edc..426f26a3e0 100644 --- a/public/language/ms/users.json +++ b/public/language/ms/users.json @@ -2,6 +2,7 @@ "latest_users": "Pengguna terkini", "top_posters": "Pengirim terbanyak", "most_reputation": "Reputasi terbaik", + "most_flags": "Most Flags", "search": "Cari", "enter_username": "Masukkan nama pengguna untuk carian", "load_more": "Muat lagi", diff --git a/public/language/nb/category.json b/public/language/nb/category.json index a0c1922641..759889274b 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -10,7 +10,11 @@ "share_this_category": "Del denne kategorien", "watch": "Overvåk", "ignore": "Ignorer", - "watch.message": "Du overvåker nå oppdateringer fra denne kategorien", - "ignore.message": "Du ignorerer nå oppdateringer fra denne kategorien", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Overvåkede kategorier" } \ No newline at end of file diff --git a/public/language/nb/email.json b/public/language/nb/email.json index 4e542e97a7..6377d85fbf 100644 --- a/public/language/nb/email.json +++ b/public/language/nb/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Ny samtalemelding mottatt fra %1", "notif.chat.cta": "Klikk her for å fortsette samtalen", "notif.chat.unsub.info": "Denne samtale-varselen ble sendt til deg basert på dine innstillinger for abonnering.", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index 773e406837..b5be6dd71f 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ugyldig passord", "invalid-username-or-password": "Vennligst spesifiser både et brukernavn og passord", "invalid-search-term": "Ugyldig søkeord", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Brukernavn opptatt", "email-taken": "E-post opptatt", "email-not-confirmed": "E-posten din har ikke blitt bekreftet enda, vennligst klikk for å bekrefte din e-post.", "email-not-confirmed-chat": "Du kan ikke chatte før e-posten din er bekreftet, vennligst klikk her for å bekrefte e-postadressen.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Dette forumet krever e-postbekreftelse, vennligst klikk her for å skrive inn en e-post", "email-confirm-failed": "Vi kunne ikke bekrefte e-posten din, vennligst prøv igjen senere.", "confirm-email-already-sent": "E-post for bekreftelse er allerede sendt, vennligst vent %1 minutt(er) for å sende en til.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Brukernavnet er for kort", "username-too-long": "Brukernavnet er for langt", "password-too-long": "Password too long", "user-banned": "Bruker utestengt", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Beklager, du må vente %1 sekund(er) før du oppretter ditt første innlegg", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Kategorien eksisterer ikke", "no-topic": "Emne eksisterer ikke", "no-post": "Innlegg eksisterer ikke", @@ -38,10 +43,26 @@ "category-disabled": "Kategori deaktivert", "topic-locked": "Emne låst", "post-edit-duration-expired": "Du har bare lov til å redigere innlegg i %1 sekund(er) etter at det er sendt", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Vennligst skriv et lengre innlegg. Innlegg må inneholde minst %1 tegn.", "content-too-long": "Vennligst skriv et kortere innlegg. Innlegg kan ikke være lengre enn %1 tegn.", "title-too-short": "Vennligst skriv en lengre tittel. Titler må inneholde minst %1 tegn.", "title-too-long": "Vennligst skriv en kortere tittel. Tittel kan ikke være lengre enn %1 tegn.", + "category-not-selected": "Category not selected.", "too-many-posts": "Du kan bare poste en gang per %1 sekund(er) – vennligst vent før du poster igjen", "too-many-posts-newbie": "Som ny bruker kan du bare poste en gang per %1. sekund(er), før du har opparbeidet %2 i rykte – vennligst vent før du poster igjen", "tag-too-short": "Vennligst skriv et lengre emneord. Disse må være på minst %1 tegn", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Du kan ikke utestenge andre administratorer!", "cant-remove-last-admin": "Du er den eneste administratoren. Legg til en annen bruker som administrator før du fjerner deg selv.", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Ugyldig bildetype. Tilatte typer er: %1", "invalid-image-extension": "Ugyldig bildefiltype", "invalid-file-type": "Ugyldig filtype. Tillatte typer er: %1", "group-name-too-short": "Gruppenavnet er for kort", + "group-name-too-long": "Group name too long", "group-already-exists": "Gruppe eksisterer allerede", "group-name-change-not-allowed": "Endring av gruppenavn er ikke tillatt", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Ryktesystemet er deaktivert.", "downvoting-disabled": "Nedstemming er deaktivert", "not-enough-reputation-to-downvote": "Du har ikke nok rykte til å nedstemme det innlegget", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/nb/global.json b/public/language/nb/global.json index 6a7e5f4165..7ed463830b 100644 --- a/public/language/nb/global.json +++ b/public/language/nb/global.json @@ -7,8 +7,10 @@ "403.login": "Kanskje du skal <a href='%1/login'>prøve å logge inn</a>?", "404.title": "Ikke funnet", "404.message": "Du har funnet en side som ikke eksisterer. Returner til <a href='%1'>startsiden</a>?", - "500.title": "Intern feil.", + "500.title": "Internal Error.", "500.message": "Oops! Ser ut som noe gikk galt!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registrer", "login": "Logg inn", "please_log_in": "Vennligst logg inn", @@ -17,6 +19,7 @@ "welcome_back": "Velkommen tilbake", "you_have_successfully_logged_in": "Du har blitt logget inn", "save_changes": "Lagre endringer", + "save": "Save", "close": "Lukk", "pagination": "Paginering", "pagination.out_of": "%1 ut av %2", @@ -50,7 +53,9 @@ "topics": "Emner", "posts": "Innlegg", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Visninger", "reputation": "Rykte", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/nb/groups.json b/public/language/nb/groups.json index 9354de5542..71e267d64f 100644 --- a/public/language/nb/groups.json +++ b/public/language/nb/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Forlat gruppe", "membership.reject": "Avslå", "new-group.group_name": "Gruppenavn:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/nb/login.json b/public/language/nb/login.json index 785a7defc4..3a78a7c66f 100644 --- a/public/language/nb/login.json +++ b/public/language/nb/login.json @@ -5,7 +5,8 @@ "remember_me": "Husk meg?", "forgot_password": "Glemt passord?", "alternative_logins": "Alternativ innlogging", - "failed_login_attempt": "Mislykket innloggingsforsøk, vennligst prøv igjen.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Du har blitt logget inn!", - "dont_have_account": "Har du ikke en konto?" + "dont_have_account": "Har du ikke en konto?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index a25a971028..3a71afce68 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Send og lås", "composer.toggle_dropdown": "Veksle nedtrekksfelt", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Avbryt", "bootbox.confirm": "Bekreft", diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json index f532425c00..f9580f623c 100644 --- a/public/language/nb/notifications.json +++ b/public/language/nb/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> har flagget et innlegg i <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/nb/pages.json b/public/language/nb/pages.json index cda5867418..dd5c207a96 100644 --- a/public/language/nb/pages.json +++ b/public/language/nb/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Brukere med flest innlegg", "users/sort-reputation": "Brukere med best rykte", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "Brukersøk", "notifications": "Varsler", "tags": "Emneord", "tag": "Emner under emneord \"%1\"", "register": "Registrer en konto", + "registration-complete": "Registration complete", "login": "Logg inn på kontoen din", "reset": "Tilbakestill passordet ditt", "categories": "Kategorier", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "Personer %1 følger", "account/followers": "Personer som følger %1", "account/posts": "Innlegg opprettet av %1", "account/topics": "Emner opprettet av %1", "account/groups": "%1 sine grupper", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Brukerinnstillinger", "account/watched": "Innlegg overvåket av %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/nb/register.json b/public/language/nb/register.json index fe42138403..56735e0762 100644 --- a/public/language/nb/register.json +++ b/public/language/nb/register.json @@ -1,5 +1,6 @@ { "register": "Registrer", + "cancel_registration": "Cancel Registration", "help.email": "Som standard, holdes din e-post skjult for offentligheten.", "help.username_restrictions": "Et unikt brukernavn mellom %1 og %2 tegn. Andre kan nevne deg med @<span id='yourUsername'>brukernavn</span>.", "help.minimum_password_length": "Ditt passord må være minst %1 tegn.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternativ registrering", "terms_of_use": "Vilkårene for bruk", "agree_to_terms_of_use": "Jeg godtar vilkårene for bruk", - "registration-added-to-queue": "Din registrering har blitt lagt til i godkjenningskøen. Du vil motta en e-post når denne blir akseptert av en administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Din registrering har blitt lagt til i godkjenningskøen. Du vil motta en e-post når denne blir akseptert av en administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 3e37dc2ba7..88d2b0e85a 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -26,12 +26,30 @@ "tools": "Verktøy", "flag": "Rapporter", "locked": "Låst", - "bookmark_instructions": "Klikk her for å returnere til siste uleste emne i denne tråden.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Rapporter dette innlegget for granskning", "flag_success": "Dette innlegget har blitt rapportert.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Dette emnet har blitt slettet. Bare brukere med emnehåndterings-privilegier kan se den.", "following_topic.message": "Du vil nå motta varsler når noen skriver i denne tråden.", - "not_following_topic.message": "Du vil ikke lenger motta varsler fra denne tråden.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Vennligst registrer deg eller logg inn for å abonnere på denne tråden.", "markAsUnreadForAll.success": "Tråd markert som ulest for alle.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Bli varslet om nye svar i dette emnet", "unwatch.title": "Slutt å overvåke dette emnet", "share_this_post": "Del ditt innlegg", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Emneverktøy", - "thread_tools.markAsUnreadForAll": "Marker som ulest", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Fest tråd", "thread_tools.unpin": "Ufest tråd", "thread_tools.lock": "Lås tråd", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Dette emnet vil bli flyttet til kategorien", "fork_topic_instruction": "Trykk på innleggene du vil forgrene", "fork_no_pids": "Ingen innlegg valgt!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Dette emnet ble forgrenet! Klikk for å gå til forgrenet emne.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Skriv din tråd-tittel her", diff --git a/public/language/nb/unread.json b/public/language/nb/unread.json index f36bd28af3..c22af54291 100644 --- a/public/language/nb/unread.json +++ b/public/language/nb/unread.json @@ -6,5 +6,8 @@ "selected": "Valgte", "all": "Alle", "all_categories": "Alle kategorier", - "topics_marked_as_read.success": "Emner merket som lest!" + "topics_marked_as_read.success": "Emner merket som lest!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 48cc0e0359..8e5a07a38a 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -6,6 +6,7 @@ "postcount": "Antall innlegg", "email": "E-post", "confirm_email": "Bekfreft e-post", + "account_info": "Account Info", "ban_account": "Utesteng kont", "ban_account_confirm": "Vil du virkelig utestenge denne brukeren?", "unban_account": "Opphev utestenging", @@ -88,14 +89,21 @@ "topics_per_page": "Tråd per side", "posts_per_page": "Innlegg per side", "notification_sounds": "Spill en lyd når du mottar et varsel", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Surfeinnstillinger", "open_links_in_new_tab": "Åpne utgående lenker i en ny fane", "enable_topic_searching": "Aktiver søk-i-emne", "topic_search_help": "Hvis søk-i-emne er aktivert, overstyres nettleserens standard sidesøk og gir mulighet til å søke gjennom hele emnet, ikke bare det som vises på skjermen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Følg emner du besvarer", - "follow_topics_you_create": "Følg emner du oppretter", - "grouptitle": "Velg gruppetittelen du vil vise", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "Ingen gruppetittel", "select-skin": "Velg et skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Assosiert med", - "sso.not-associated": "Klikk her for å assosiere med" + "sso.not-associated": "Klikk her for å assosiere med", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/nb/users.json b/public/language/nb/users.json index 0cfb6f31fa..c491a4d7e2 100644 --- a/public/language/nb/users.json +++ b/public/language/nb/users.json @@ -2,6 +2,7 @@ "latest_users": "Seneste brukere", "top_posters": "Flest innlegg", "most_reputation": "Best rykte", + "most_flags": "Most Flags", "search": "Søk", "enter_username": "Skriv ett brukernavn for å søke", "load_more": "Last flere", diff --git a/public/language/nl/category.json b/public/language/nl/category.json index c65576b9dd..15a78d7d92 100644 --- a/public/language/nl/category.json +++ b/public/language/nl/category.json @@ -10,7 +10,11 @@ "share_this_category": "Deel deze categorie", "watch": "Volgen", "ignore": "Negeren", - "watch.message": "Van deze categorie worden nu meldingen ontvangen", - "ignore.message": "Er worden geen meldingen van deze categorie ontvangen", + "watching": "Volgend", + "ignoring": "Negerend", + "watching.description": "Toon ongelezen onderwerpen", + "ignoring.description": "Toon geen onderwerpen onder ongelezen onderwerpen", + "watch.message": "Van deze categorie en alle sub-categorieën worden nu meldingen ontvangen ", + "ignore.message": " Er worden geen meldingen van deze categorie en alle sub-categorieën ontvangen ", "watched-categories": "Categorieën die bekeken zijn." } \ No newline at end of file diff --git a/public/language/nl/email.json b/public/language/nl/email.json index 829ad4fcdc..c010b77769 100644 --- a/public/language/nl/email.json +++ b/public/language/nl/email.json @@ -24,6 +24,7 @@ "digest.day": "dag", "digest.week": "week", "digest.month": "maand", + "digest.subject": "Samenvatting voor %1", "notif.chat.subject": "Nieuw chatbericht van %1", "notif.chat.cta": "Klik hier om het gesprek te hervatten", "notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 9b6c912a23..f680a96487 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ongeldig wachtwoord", "invalid-username-or-password": "Geef zowel een gebruikersnaam als wachtwoord op", "invalid-search-term": "Ongeldig zoekterm", + "csrf-invalid": "We konden u niet aanmelden, waarschijnlijk door een verlopen sessie. Probeer het a.u.b. nogmaals.", "invalid-pagination-value": "Invalide paginering waarde. De waarde moet op z'n minst %1 zijn en niet hoger dan %2 zijn.", "username-taken": "Gebruikersnaam is al in gebruik ", "email-taken": "E-mailadres is al in gebruik", "email-not-confirmed": "Het e-mailadres van dit account is nog niet bevestigd, klik hier om je e-mailadres te bevestigen.", "email-not-confirmed-chat": "Het gebruik van chatfunctionaliteit is pas toegestaan na validatie van het e-mailadres.", + "email-not-confirmed-email-sent": "Je email adres is niet bevestigd, kijk of je de bevestigingsmail hebt ontvangen.", "no-email-to-confirm": "Dit berichtenforum vereist bevestiging per e-mail, klik hier om een e-mailadres te registreren", "email-confirm-failed": "Helaas kon het e-mailadres niet bevestigd worden, probeer het later nog eens.", "confirm-email-already-sent": "Bevestigingsmail is zojuist al verzonden, wacht alsjeblieft %1 minuut (minuten) voordat je opnieuw een bevestigingsmail aanvraagt.", + "sendmail-not-found": "De sendmail executable kon niet worden gevonden, zorg ervoor dat deze is geïnstalleerd en dat de gebruiker die NodeBB draait deze kan uitvoeren.", "username-too-short": "Gebruikersnaam is te kort", "username-too-long": "Gebruikersnaam is te lang", "password-too-long": "Wachtwoord is te lang", "user-banned": "Gebruiker verbannen", + "user-banned-reason": "Gebruiker verbannen (Reden: 1%)", "user-too-new": "Helaas, het is een vereiste om %1 seconde(n) te wachten voordat het eerste bericht geplaatst kan worden.", "blacklisted-ip": "Sorry, uw IP-adres is verbannen uit deze community. Als u meent dat dit onterecht is, neem dan contact op met een beheerder.", + "ban-expiry-missing": "Geef een einddatum op voor deze ban.", "no-category": "Categorie bestaat niet", "no-topic": "Onderwerp bestaat niet", "no-post": "Bericht bestaat niet", @@ -38,10 +43,26 @@ "category-disabled": "Categorie uitgeschakeld", "topic-locked": "Onderwerp gesloten", "post-edit-duration-expired": "Het is slechts toegestaan om binnen %1 seconde(n) na plaatsen van het bericht, deze te bewerken.", + "post-edit-duration-expired-minutes": "Je kunt berichten pas %1 minuten na het plaatsen aanpassen.", + "post-edit-duration-expired-minutes-seconds": "Je kunt berichten pas %1 minuten en %2 seconden na het plaatsen aanpassen.", + "post-edit-duration-expired-hours": "Je kunt berichten pas %1 uur na het plaatsen aanpassen.", + "post-edit-duration-expired-hours-minutes": "Je kunt berichten pas %1 uur en %2 minuten na het plaatsen aanpassen.", + "post-edit-duration-expired-days": "Je kunt berichten pas %1 dagen na het plaatsen aanpassen.", + "post-edit-duration-expired-days-hours": "Je kunt berichten pas %1 dagen en %2 uur na het plaatsen aanpassen.", + "post-delete-duration-expired": "Je kunt berichten pas %1 seconden na het plaatsen verwijderen.", + "post-delete-duration-expired-minutes": "Je kunt berichten pas %1 minuten na het plaatsen verwijderen.", + "post-delete-duration-expired-minutes-seconds": "Je kunt berichten pas %1 minuten %2 seconden na het plaatsen verwijderen.", + "post-delete-duration-expired-hours": "Je kunt berichten pas %1 uur na het plaatsen verwijderen.", + "post-delete-duration-expired-hours-minutes": "Je kunt berichten pas %1 uur %2 minuten na het plaatsen verwijderen.", + "post-delete-duration-expired-days": "Je kunt berichten pas %1 dagen na het plaatsen verwijderen.", + "post-delete-duration-expired-days-hours": "Je kunt berichten pas %1 dag(en) %2 uur na het plaatsen verwijderen.", + "cant-delete-topic-has-reply": "Je kunt je topic niet verwijderen nadat iemand heeft gereageerd", + "cant-delete-topic-has-replies": "Je kunt je topic niet verwijderen als het %1 reacties heeft", "content-too-short": "Geef wat meer inhoud aan een bericht! Berichten dienen uit minimaal %1 teken(s) te bestaan.", "content-too-long": "Kort het bericht wat in, het aantal gebruikte tekens overschrijdt het ingestelde limiet want berichten mogen niet meer dan %1 teken(s) bevatten.", "title-too-short": "Geef een titel op die uit meer tekens bestaat. Titels dienen ten minste uit %1 teken(s) te bestaan.", "title-too-long": "Geef een kortere titel op. Titels mogen uit niet meer dan %1 teken(s) bestaan.", + "category-not-selected": "Categorie niet geselecteerd ", "too-many-posts": "Het is slechts toegestaan iedere %1 seconde(n) een bericht te plaatsen - wacht even voordat opnieuw een bericht verzonden wordt", "too-many-posts-newbie": "Nieuwe gebruikersaccounts zoals deze zijn begrensd en mogen slechts iedere %1 seconde(n) berichten plaatsen, tot het moment dat %2 reputatie verdiend is - wacht daarom even met opnieuw een bericht te plaatsten", "tag-too-short": "Geef een tag op die uit meer tekens bestaat. Tags dienen uit minimaal %1 teken(s) te bestaan.", @@ -55,10 +76,12 @@ "already-unfavourited": "Je hebt dit bericht al verwijderd uit je favorieten", "cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!", "cant-remove-last-admin": "Je bent de enige beheerder. Stel eerst een andere gebruiker als beheerder in voordat je jezelf geen beheerder meer maakt.", + "cant-delete-admin": "Verwijder administratieve rechten van dit account voordat je probeert deze te verwijderen", "invalid-image-type": "Ongeldig bestandstype afbeelding. Deze afbeelding is van een bestandstype dat niet ondersteund wordt. Toegestane bestandstypes voor afbeeldingsbestanden zijn: %1", "invalid-image-extension": "Ongeldig bestandstype afbeelding", "invalid-file-type": "Dit bestandstype wordt niet ondersteund. Toegestane bestandstypen zijn: %1", "group-name-too-short": "De groepsnaam is te kort", + "group-name-too-long": "Groepsnaam te lang", "group-already-exists": "Een groep met deze naam bestaat al", "group-name-change-not-allowed": "Het aanpassen van de groepsnaam is niet toegestaan", "group-already-member": "Deze gebruiker is al lid van deze groep", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Het is niet toegestaan om dit bericht aan te passen", "cant-remove-last-user": "Je kan de laatste gebruiker niet verwijderen", "cant-delete-chat-message": "Het is niet toegestaan om dit bericht te verwijderen", + "already-voting-for-this-post": "Je hebt al gestemd voor deze post.", "reputation-system-disabled": "Reputatie systeem is uitgeschakeld.", "downvoting-disabled": "Negatief stemmen is uitgeschakeld", "not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen", @@ -99,5 +123,9 @@ "no-session-found": "Geen login sessie gevonden!", "not-in-room": "Gebruiker niet in de chat", "no-users-in-room": "Er zijn geen gebruikers in deze chat", - "cant-kick-self": "Je kunt jezelf niet uit een groep schoppen" + "cant-kick-self": "Je kunt jezelf niet uit een groep schoppen", + "no-users-selected": "Geen gebruiker(s) geselecteerd", + "invalid-home-page-route": "Onbekende homepage route", + "invalid-session": "Verkeerde sessie combinatie", + "invalid-session-text": "Het lijkt erop dat je login sessie niet meer actief is of niet langer synchroon is met de server. Ververs de pagina." } \ No newline at end of file diff --git a/public/language/nl/global.json b/public/language/nl/global.json index dfa27b416f..d23baeb60d 100644 --- a/public/language/nl/global.json +++ b/public/language/nl/global.json @@ -7,8 +7,10 @@ "403.login": "Je kan proberen <a href='%1/login'>in te loggen</a>?", "404.title": "Niet gevonden", "404.message": "Deze pagina bestaat niet. Klik hier om naar de <a href='%1/'>hoofdpagina</a> van deze website te navigeren.", - "500.title": "Interne fout.", + "500.title": "Interne fout", "500.message": "Oeps! Ziet er naar uit dat iets fout ging!", + "400.title": "Foutief verzoek", + "400.message": "Het lijkt erop dat de link onjuist is. Kijk het nog eens na en probeer het opnieuw. Of ga terug naar de <a href='%1/'>startpagina</a>.", "register": "Registeren", "login": "Login", "please_log_in": "Aanmelden", @@ -17,6 +19,7 @@ "welcome_back": "Welkom terug", "you_have_successfully_logged_in": "Aanmelden succesvol", "save_changes": "Wijzigingen opslaan", + "save": "Opslaan", "close": "Sluiten", "pagination": "Paginering", "pagination.out_of": "%1 van %2", @@ -50,7 +53,9 @@ "topics": "Onderwerpen", "posts": "Berichten", "best": "Beste", + "upvoters": "Positieve stemmers", "upvoted": "Omhoog gestemd", + "downvoters": "Negatieve stemmers", "downvoted": "Omlaag gestemd", "views": "Weergaven", "reputation": "Reputatie", @@ -90,5 +95,8 @@ "enter_page_number": "Voer paginanummer in", "upload_file": "Upload bestand", "upload": "Upload", - "allowed-file-types": "Toegestane bestandstypen zijn %1" + "allowed-file-types": "Toegestane bestandstypen zijn %1", + "unsaved-changes": "Je hebt niet opgeslagen wijzigingen aangebracht. Weet je zeker dat je de pagina wilt verlaten?", + "reconnecting-message": "Het lijkt erop dat je verbinding naar %1 verloren is gegaan, wacht even terwijl we de verbinding proberen te herstellen.", + "play": "Afspelen" } \ No newline at end of file diff --git a/public/language/nl/groups.json b/public/language/nl/groups.json index 2bf421bd5b..fe5486a4ef 100644 --- a/public/language/nl/groups.json +++ b/public/language/nl/groups.json @@ -41,7 +41,7 @@ "details.hidden": "Niet getoond", "details.hidden_help": "Indien geactiveerd zal deze groep niet getoond worden in de groepslijst en zullen gebruikers handmatig uitgenodigd moeten worden.", "details.delete_group": "Groep verwijderen", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Private groepen zijn op systeemniveau uitgeschakeld, deze optie doet niets.", "event.updated": "Groepsdetails zijn bijgewerkt", "event.deleted": "De groep \"%1\" is verwijderd", "membership.accept-invitation": "Uitnodiging accepteren", @@ -50,5 +50,7 @@ "membership.leave-group": "Verlaat groep", "membership.reject": "Afwijzen", "new-group.group_name": "Groepsnaam:", - "upload-group-cover": "Upload groepscover" + "upload-group-cover": "Upload groepscover", + "bulk-invite-instructions": "Vul een lijst is met gebruikersnamen gescheiden met komma's om deze uit te nodigen voor deze groep", + "bulk-invite": "Massa uitnodiging" } \ No newline at end of file diff --git a/public/language/nl/login.json b/public/language/nl/login.json index 9b84acdc8a..e6d21e1fa6 100644 --- a/public/language/nl/login.json +++ b/public/language/nl/login.json @@ -5,7 +5,8 @@ "remember_me": "Aangemeld blijven?", "forgot_password": "Wachtwoord vergeten?", "alternative_logins": "Andere manieren van aanmelden", - "failed_login_attempt": "Aanmelden niet geslaagd. Probeer het nog eens.", + "failed_login_attempt": "Aanmelden mislukt", "login_successful": "Je bent succesvol ingelogd!", - "dont_have_account": "Geen gebruikersaccount?" + "dont_have_account": "Geen gebruikersaccount?", + "logged-out-due-to-inactivity": "Je bent uitgelogt van het admin control panel vanwege inactiviteit." } \ No newline at end of file diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index dd7de3cc62..7ac1846c59 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Bericht plaatsen en sluiten", "composer.toggle_dropdown": "Keuzelijst schakelen", "composer.uploading": "Uploaden van %1", + "composer.formatting.bold": "Vet", + "composer.formatting.italic": "Cursief", + "composer.formatting.list": "Lijst", + "composer.formatting.strikethrough": "Doorhalen", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Afbeelding", + "composer.upload-picture": "Upload afbeelding", + "composer.upload-file": "Upload bestand", + "composer.zen_mode": "Zen-modus", + "composer.select_category": "Selecteer een categorie", "bootbox.ok": "OK", "bootbox.cancel": "Annuleren", "bootbox.confirm": "Bevestig", diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index a3917e238e..c1efb52f43 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> en %2 andere hebben in gestemd in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> heeft je bericht verplaatst naar <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> heeft <strong>%2</strong> verplaatst", - "favourited_your_post_in": "<strong>%1</strong> heeft je bericht in <strong>%2</strong> aan zijn/haar favorieten toegevoegd.", - "favourited_your_post_in_dual": "<strong>%1</strong> en <strong>%2</strong> hebben je bericht in <strong>%3</strong> aan hun favorieten toegevoegd.", - "favourited_your_post_in_multiple": "<strong>%1</strong> en %2 anderen hebben je bericht in <strong>%3</strong> aan hun favorieten toegevoegd.", "user_flagged_post_in": "<strong>%1</strong> rapporteerde een bericht in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> en <strong>%2</strong> rapporteerde een bericht in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> en %2 andere rapporteede een bericht in <strong>%3</strong>", diff --git a/public/language/nl/pages.json b/public/language/nl/pages.json index 96f5f11a38..2973c983a5 100644 --- a/public/language/nl/pages.json +++ b/public/language/nl/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Gebruikers met de meeste berichten", "users/sort-reputation": "Gebruikers met de meeste reputatie", "users/banned": "Verbannen Gebruikers", + "users/most-flags": "Meest gemarkeerde gebruikers", "users/search": "Zoek Gebruiker", "notifications": "Notificaties", "tags": "Tags", "tag": "Onderwerpen geplaatst onder \"%1\"", "register": "Registeer een gebruikersaccount", + "registration-complete": "Registratie compleet", "login": "Login met u gebruikersaccount in", "reset": "Gebruikerswachtwoord opnieuw instellen", "categories": "Categorieën", @@ -28,12 +30,13 @@ "account/edit/password": "Wachtwoord van \"%1\" aanpassen", "account/edit/username": "Gebruikersnaam van \"%1\" aanpassen", "account/edit/email": "Email van \"%1\" aanpassen", + "account/info": "Gebruikersinformatie", "account/following": "Door %1 gevolgd", "account/followers": "Die %1 volgen", "account/posts": "Berichten geplaatst door %1", "account/topics": "Onderwerpen begonnen door %1", "account/groups": "%1's groepen", - "account/favourites": "%1's Favoriete Berichten", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Gebruikersinstellingen", "account/watched": "Berichten die door %1 bekeken worden", "account/upvoted": "Berichten omhoog gestemd door %1", diff --git a/public/language/nl/register.json b/public/language/nl/register.json index 70d1f69af3..8083d2b79c 100644 --- a/public/language/nl/register.json +++ b/public/language/nl/register.json @@ -1,5 +1,6 @@ { "register": "Registreren", + "cancel_registration": "Annuleer registratie", "help.email": "E-mailadressen zijn standaard verborgen voor andere gebruikers.", "help.username_restrictions": "Een unieke gebruikersnaam tussen %1 en %2 karakters. Anderen kunnen je vermelden met @<span id='yourUsername'>gebruikersnaam</span>.", "help.minimum_password_length": "Je wachtwoord moet tenminste %1 karakters lang zijn.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatieve registratie", "terms_of_use": "Gebruiksvoorwaarden", "agree_to_terms_of_use": "Ik ga akkoord met de gebruiksvoorwaarden", - "registration-added-to-queue": "Het registratieverzoek is toegevoegd aan de wachtrij. Een bericht wordt naar het opgegeven emailadres gestuurd wanneer de registratie is goedgekeurd." + "terms_of_use_error": "Je moet akkoord gaan met de service voorwaarden.", + "registration-added-to-queue": "Het registratieverzoek is toegevoegd aan de wachtrij. Een bericht wordt naar het opgegeven emailadres gestuurd wanneer de registratie is goedgekeurd.", + "interstitial.intro": "Je hebben meer informatie van je nodig voordat we je account kunnen aanmaken.", + "interstitial.errors-found": "Je konden jouw registratie niet afronden." } \ No newline at end of file diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index 5bff87a78c..c1cf28394a 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -26,12 +26,30 @@ "tools": "Extra", "flag": "Markeren", "locked": "Gesloten", - "bookmark_instructions": "Klik hier om naar het nieuwste ongelezen bericht te gaan.", + "pinned": "Pinned", + "moved": "Verplaatst", + "bookmark_instructions": "Klik hier om terug te keren naar de laatst gelezen post in deze thread.", "flag_title": "Bericht aan beheerders melden", "flag_success": "Dit bericht is gerapporteerd aan de beheerder.", + "flag_manage_title": "Gemarkeerd bericht in %1", + "flag_manage_history": "Activiteiten geschiedenis", + "flag_manage_no_history": "Geen geschiedenis te tonen", + "flag_manage_assignee": "Toekenning", + "flag_manage_state": "Staat", + "flag_manage_state_open": "Nieuw/Open", + "flag_manage_state_wip": "Wordt aan gewerkt", + "flag_manage_state_resolved": "Opgelost", + "flag_manage_state_rejected": "Afgewezen", + "flag_manage_notes": "Gedeelde notities", + "flag_manage_update": "Markering updaten", + "flag_manage_history_assignee": "Toegewezen aan %1", + "flag_manage_history_state": "Update status naar 1%", + "flag_manage_history_notes": "Update gemarkeerde notities", + "flag_manage_saved": "Markeering details geupdatet", "deleted_message": "Dit onderwerp is verwijderd. Alleen gebruikers met beheerrechten op onderwerpniveau kunnen dit inzien.", "following_topic.message": "Vanaf nu worden meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.", - "not_following_topic.message": "Je ontvangt geen notificaties meer over dit onderwerp.", + "not_following_topic.message": "Dit onderwerp zal verschijnen in de lijst van ongelezen onderwerpen, maar er zullen geen meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.", + "ignoring_topic.message": "Dit onderwerp zal niet meer verschijnen in de lijst van ongelezen berichten. U zult enkel een melding ontvangen wanneer u wordt genoemd, of wanneer er een positieve stem op uw reactie wordt gegeven.", "login_to_subscribe": "Log in or registreer om dit onderwerp te volgen.", "markAsUnreadForAll.success": "Onderwerp is voor iedereen als ongelezen gemarkeerd.", "mark_unread": "Ongelezen markeren", @@ -41,8 +59,14 @@ "watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp", "unwatch.title": "Dit onderwerp niet langer volgen", "share_this_post": "Deel dit bericht", + "watching": "Gevolgd", + "not-watching": "Niet gevolgd", + "ignoring": "Genegeerd", + "watching.description": "Stuur me een melding bij nieuwe reacties.<br/>Toon onderwerp bij de ongelezen onderwerpen.", + "not-watching.description": "Stuur me geen melding van nieuwe reacties.<br/>Toon onderwerp in ongelezen mits de categorie niet genegeerd wordt.", + "ignoring.description": "Stuur me geen melding van nieuwe reacties.<br/>Toon dit onderwerp niet onder de ongelezen onderwerpen.", "thread_tools.title": "Acties", - "thread_tools.markAsUnreadForAll": "Ongelezen markeren", + "thread_tools.markAsUnreadForAll": "Markeer alles als ongelezen", "thread_tools.pin": "Onderwerp vastpinnen", "thread_tools.unpin": "Onderwerp losmaken", "thread_tools.lock": "Onderwerp sluiten", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Dit onderwerp zal naar de categorie verplaatst worden", "fork_topic_instruction": "Klik op de berichten die afgesplitst moeten worden", "fork_no_pids": "Geen berichten geselecteerd!", + "fork_pid_count": "%1 bericht(en) geselecteerd", "fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.", "delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden", "composer.title_placeholder": "Voer hier de titel van het onderwerp in...", diff --git a/public/language/nl/unread.json b/public/language/nl/unread.json index 4a5fc11558..d5bed309e4 100644 --- a/public/language/nl/unread.json +++ b/public/language/nl/unread.json @@ -6,5 +6,8 @@ "selected": "Geselecteerd", "all": "Alles", "all_categories": "Alle categorieën", - "topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!" + "topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!", + "all-topics": "Alle onderwerpen", + "new-topics": "Nieuwe onderwerpen", + "watched-topics": "Bekeken onderwerpen" } \ No newline at end of file diff --git a/public/language/nl/uploads.json b/public/language/nl/uploads.json index 1622cb5693..c59223164d 100644 --- a/public/language/nl/uploads.json +++ b/public/language/nl/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Bestand word geüpload...", + "select-file-to-upload": "Selecteer een bestand om te uploaden!", + "upload-success": "Bestand succesvol geüpload!", + "maximum-file-size": "Maximaal %1 kb" } \ No newline at end of file diff --git a/public/language/nl/user.json b/public/language/nl/user.json index 70d646156b..a17760a41a 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -6,6 +6,7 @@ "postcount": "Aantal geplaatste berichten", "email": "E-mail", "confirm_email": "Bevestig e-mail", + "account_info": "Gebruikersinformatie", "ban_account": "Verban gebruiker", "ban_account_confirm": "Weet u zeker dat u deze gebruiker wilt verbannen?", "unban_account": "Verbanning intrekken", @@ -88,14 +89,21 @@ "topics_per_page": "Onderwerpen per pagina", "posts_per_page": "Berichten per pagina", "notification_sounds": "Speel een geluid af wanneer ik een notificatie ontvang", + "notifications_and_sounds": "Notificaties & Geluiden", + "incoming-message-sound": "Inkomend bericht geluid", + "outgoing-message-sound": "Uitgaand bericht geluid", + "notification-sound": "Notificatie geluid", + "no-sound": "Geen geluid", "browsing": "Instellingen voor bladeren", "open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad", "enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken", "topic_search_help": "Wanneer ingeschakeld zal de standaard zoekfunctie, met een aangepaste methode voor onderwerpen, overschreven worden", + "delay_image_loading": "Afbeeldingen Laden Uitstellen", + "image_load_delay_help": "Indien ingeschakeld zullen afbeeldingen in topics niet laden totdat ze het scherm inscrollen", "scroll_to_my_post": "Toon het nieuwe bericht na het plaatsen van een antwoord", - "follow_topics_you_reply_to": "Volg de onderwerpen waarop ik gereageerd heb", - "follow_topics_you_create": "Volg de onderwerpen waarvan ik de oorspronkelijke auteur ben", - "grouptitle": "Selecteer de groepstitel voor weergave", + "follow_topics_you_reply_to": "Ontvang meldingen van berichten waar je op hebt gereageerd", + "follow_topics_you_create": "Ontvang meldingen van berichten die je hebt gemaakt", + "grouptitle": "Groepstitel", "no-group-title": "Geen groepstitel", "select-skin": "Selecteer een skin", "select-homepage": "Selecteer een startpagina", @@ -105,5 +113,17 @@ "custom_route_help": "Voer een route naam hier, zonder enige voorafgaande schuine streep (zoals, \"recente\" of \"populaire\")", "sso.title": "Single Sign-on Services", "sso.associated": "Geassocieerd met", - "sso.not-associated": "Klik hier om geassocieerd te worden met" + "sso.not-associated": "Klik hier om geassocieerd te worden met", + "info.latest-flags": "Laatste markeringen", + "info.no-flags": "Geen gemarkeerde berichten gevonden", + "info.ban-history": "Recente verban-geschiedenis", + "info.no-ban-history": "Deze gebruiker is nooit eerder verbannen", + "info.banned-until": "Verbannen tot %1", + "info.banned-permanently": "Voor altijd verbannen", + "info.banned-reason-label": "Reden", + "info.banned-no-reason": "Geen reden opgegeven", + "info.username-history": "Gebruikersnaam geschiedenis", + "info.email-history": "Email geschiedenis", + "info.moderation-note": "Moderatie notitie", + "info.moderation-note.success": "Moderatie notitie opgeslagen" } \ No newline at end of file diff --git a/public/language/nl/users.json b/public/language/nl/users.json index 03dcb21bbb..16c8219e73 100644 --- a/public/language/nl/users.json +++ b/public/language/nl/users.json @@ -2,6 +2,7 @@ "latest_users": "Laatste gebruikers", "top_posters": "Meest actieve leden", "most_reputation": "Meeste reputatie", + "most_flags": "Meeste vlaggen", "search": "Zoeken", "enter_username": "Vul een gebruikersnaam in om te zoeken", "load_more": "Meer laden...", diff --git a/public/language/pl/category.json b/public/language/pl/category.json index bda340c895..9b88cda26d 100644 --- a/public/language/pl/category.json +++ b/public/language/pl/category.json @@ -10,7 +10,11 @@ "share_this_category": "Udostępnij tę kategorię", "watch": "Obserwuj", "ignore": "Ignoruj", - "watch.message": "Obserwujesz teraz aktualizacje tej kategorii", - "ignore.message": "Ignorujesz teraz aktualizacje tej kategorii", + "watching": "Obserwowanie", + "ignoring": "Ignorowanie", + "watching.description": "Pokaż tematy jako nieprzeczytane", + "ignoring.description": "Nie pokazuj tematów jako nieprzeczytane", + "watch.message": "Włączyłeś powiadomienia dla tej kategorii oraz subkategorii.", + "ignore.message": "Wyłączyłeś powiadomienia dla tej kategorii oraz subkategorii.", "watched-categories": "Obserwowane kategorie" } \ No newline at end of file diff --git a/public/language/pl/email.json b/public/language/pl/email.json index c33233781e..5146cc5cd2 100644 --- a/public/language/pl/email.json +++ b/public/language/pl/email.json @@ -21,9 +21,10 @@ "digest.cta": "Kliknij, by odwiedzić %1", "digest.unsub.info": "To podsumowanie zostało wysłane zgodnie z Twoimi ustawieniami.", "digest.no_topics": "Nie było żadnych aktywnych tematów w ciągu ostatnich %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "dzień", + "digest.week": "tydzień", + "digest.month": "miesiąc", + "digest.subject": "Digest for %1", "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.", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 196a9e6c77..2c13042f2e 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -14,20 +14,25 @@ "invalid-password": "Błędne Hasło", "invalid-username-or-password": "Proszę podać nazwę użytkownika i hasło", "invalid-search-term": "Błędne wyszukiwane wyrażenie", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "Nie mogliśmy Cię zalogować. Zapewne przyczyną jest wygasła sesja. Proszę spróbuj ponownie.", + "invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2", "username-taken": "Login zajęty", "email-taken": "Email zajęty", "email-not-confirmed": "Twój email nie został jeszcze potwierdzony. Proszę kliknąć tutaj by go potwierdzić.", "email-not-confirmed-chat": "Nie możesz prowadzić rozmów dopóki twój email nie zostanie potwierdzony, kliknij tutaj, aby potwierdzić swój email.", + "email-not-confirmed-email-sent": "Twój e-mail jeszcze nie został potwierdzony, proszę sprawdź swoją skrzynkę odbiorczą.", "no-email-to-confirm": "To forum wymaga weryfikacji przez email. Proszę kliknąć tutaj, aby wprowadzić adres.", "email-confirm-failed": "Nie byliśmy w stanie potwierdzić twojego email-a. Proszę spróbować później.", "confirm-email-already-sent": "Email potwierdzający został już wysłany, proszę odczekaj jeszcze %1 minut(y), aby wysłać kolejny.", + "sendmail-not-found": "Program sendmail nie został znaleziony, proszę upewnij się, że jest zainstalowany i możliwy do uruchomienia przez użytkownika uruchamiającego NodeBB.", "username-too-short": "Nazwa użytkownika za krótka", "username-too-long": "Zbyt długa nazwa użytkownika", - "password-too-long": "Password too long", + "password-too-long": "Hasło jest za długie", "user-banned": "Użytkownik zbanowany", + "user-banned-reason": "Użytkownik zbanowany (powód: %1)", "user-too-new": "Przepraszamy, musisz odczekać %1 sekund(y) przed utworzeniem pierwszego posta", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Twój adres IP został zablokowany na tej społeczności. Jeśli uważasz to za błąd, zgłoś to administratorowi", + "ban-expiry-missing": "Wprowadź datę końca blokady", "no-category": "Kategoria nie istnieje", "no-topic": "Temat nie istnieje", "no-post": "Post nie istnieje", @@ -38,10 +43,26 @@ "category-disabled": "Kategoria wyłączona.", "topic-locked": "Temat Zablokowany", "post-edit-duration-expired": "Możesz edytować posty tylko przez %1 sekund(y) po ich napisaniu", + "post-edit-duration-expired-minutes": "Możesz edytować posty tylko przez %1 minut(y) po ich napisaniu", + "post-edit-duration-expired-minutes-seconds": "Możesz edytować posty tylko przez %1 minut(y) i %2 sekund(y) po ich napisaniu", + "post-edit-duration-expired-hours": "Możesz edytować posty tylko przez %1 godzin(y) po ich napisaniu", + "post-edit-duration-expired-hours-minutes": "Możesz edytować posty tylko przez %1 godzin(y) i %2 minut(y) po ich napisaniu", + "post-edit-duration-expired-days": "Możesz edytować posty tylko przez %1 dzień (dni) po ich napisaniu", + "post-edit-duration-expired-days-hours": "Możesz edytować posty tylko przez %1 dzień (dni) i %2 godzin(y) po ich napisaniu", + "post-delete-duration-expired": "Możesz kasować posty przez %1 sekund(-y) po napisaniu", + "post-delete-duration-expired-minutes": "Możesz kasować posty przez %1 minut(-y) po napisaniu", + "post-delete-duration-expired-minutes-seconds": "Możesz kasować posty przez %1 minut(-y) i %2 sekund(-y) po napisaniu", + "post-delete-duration-expired-hours": "Możesz kasować posty przez %1 godzin(-y) po napisaniu", + "post-delete-duration-expired-hours-minutes": "Możesz kasować posty przez %1 godzin(-y) i %2 minut(-y) po napisaniu", + "post-delete-duration-expired-days": "Możesz kasować posty przez %1 dni po napisaniu", + "post-delete-duration-expired-days-hours": "Możesz kasować posty przez %1 dni i %2 godzin(-y) po napisaniu", + "cant-delete-topic-has-reply": "Nie możesz usunąć tematu zawierającego odpowiedź", + "cant-delete-topic-has-replies": "Nie możesz usunąć tematu zawierającego %1 odpowiedzi", "content-too-short": "Prosimy wpisać dłuższy post. Posty powinny zawierać co najmniej %1 znaków.", "content-too-long": "Prosimy wpisać krótszy post. Posty nie mogą zawierać więcej niż %1 znaków.", "title-too-short": "Prosimy podać dłuższy tytuł. Tytuły powinny zawierać co najmniej %1 znaków.", "title-too-long": "Prosimy podać krótszy tytuł. Tytuły nie mogą być dłuższe niż %1 znaków.", + "category-not-selected": "Nie wybrano kategorii.", "too-many-posts": "Możesz wysyłać posty raz na %1 sekund - prosimy poczekać przed ponownym dodaniem posta", "too-many-posts-newbie": "Jako nowy użytkownik możesz wysyłać posty raz na %1 sekund, dopóki nie zdobędziesz %2 reputacji - prosimy poczekać przed ponownym dodaniem posta", "tag-too-short": "Prosimy wprowadzić dłuższy tag. Tagi muszą posiadać przynajmniej %1 znak(ów)", @@ -55,14 +76,16 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Nie możesz zbanować innych adminów!", "cant-remove-last-admin": "Jesteś jedynym administratorem. Dodaj innego użytkownika jako administratora przed usunięciem siebie z tej grupy", + "cant-delete-admin": "Usuń uprawnienia administratora z tego konta przed próbą jego usunięcia.", "invalid-image-type": "Błędny typ obrazka. Dozwolone typy to: %1", "invalid-image-extension": "Błędne rozszerzenie pliku", "invalid-file-type": "Błędny typ pliku. Dozwolone typy to: %1", "group-name-too-short": "Nazwa grupy za krótka", + "group-name-too-long": "Nazwa grupy jest za długa", "group-already-exists": "Grupa już istnieje", "group-name-change-not-allowed": "Nie można zmieniać nazwy tej grupy.", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", + "group-already-member": "Już jesteś członkiem tej grupy", + "group-not-member": "Nie jesteś członkiem tej grupy", "group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela", "group-already-invited": "Ten użytkownik został już zaproszony", "group-already-requested": "Twoje podanie o członkostwo zostało już wysłane", @@ -78,13 +101,14 @@ "about-me-too-long": "Przepraszamy, twoja informacja o sobie nie może być dłuższa niż %1 znaków.", "cant-chat-with-yourself": "Nie możesz rozmawiać sam ze sobą!", "chat-restricted": "Ten użytkownik ograniczył swoje rozmowy. Musi cię śledzić, aby kontakt z nim był możliwy", - "chat-disabled": "Chat system disabled", + "chat-disabled": "System rozmów jest wyłączony", "too-many-messages": "Wysłałeś zbyt wiele wiadomości, prosimy chwilę poczekać.", "invalid-chat-message": "Nieprawidłowa wiadomość", "chat-message-too-long": "Wiadomość jest zbyt długa", - "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-edit-chat-message": "Nie jesteś upoważniony do edycji tej wiadomości", "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "cant-delete-chat-message": "Nie jesteś upoważniony do usunięcia tej wiadomości", + "already-voting-for-this-post": "Już zagłosowałeś na ten post", "reputation-system-disabled": "System reputacji jest wyłączony.", "downvoting-disabled": "Negatywna ocena postów jest wyłączona", "not-enough-reputation-to-downvote": "Masz za mało reputacji, aby negatywnie ocenić ten post", @@ -95,9 +119,13 @@ "parse-error": "Coś poszło nie tak podczas przetwarzania odpowiedzi serwera", "wrong-login-type-email": "Zaloguj się używając adresu email", "wrong-login-type-username": "Zaloguj się używając nazwy użytkownika", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "invite-maximum-met": "Zaprosiłeś maksymalną ilość osób (%1 z %2).", + "no-session-found": "Nie znaleziono sesji logowania", + "not-in-room": "Użytkownik nie jest w pokoju", + "no-users-in-room": "Brak użytkowników w pokoju", + "cant-kick-self": "Nie możesz wyrzucić samego siebie z grupy", + "no-users-selected": "Nie wybrano żadnych użytkowników", + "invalid-home-page-route": "Niepoprawny odnośnik strony domowej", + "invalid-session": "Niewłaściwa sesja", + "invalid-session-text": "Wygląda na to, że twoja sesja jest nieaktywna, lub nie zgadza się z serwerem. Proszę odświeżyć tę stronę." } \ No newline at end of file diff --git a/public/language/pl/global.json b/public/language/pl/global.json index 2aba9da79d..7354281f22 100644 --- a/public/language/pl/global.json +++ b/public/language/pl/global.json @@ -7,8 +7,10 @@ "403.login": "Może powinieneś się <a href='%1/login'>zalogować</a>?", "404.title": "Nie znaleziono", "404.message": "Wygląda na to, że trafiłeś na stronę, która nie istnieje. Wróć do <a href='%1/'>strony głównej</a>.", - "500.title": "Błąd wewnętrzny", + "500.title": "Wewnętrzny błąd.", "500.message": "Ups! Coś poszło nie tak.", + "400.title": "Złe zapytanie.", + "400.message": "Wygląda na to, że ten link jest niepoprawny, proszę go sprawdzić i spróbować ponownie. W przeciwnym wypadku wróć do <a href='%1/'>strona domowa</a>.", "register": "Zarejestruj się", "login": "Zaloguj się", "please_log_in": "Proszę się zalogować", @@ -17,6 +19,7 @@ "welcome_back": "Witamy ponownie!", "you_have_successfully_logged_in": "Zostałeś pomyślnie zalogowany.", "save_changes": "Zapisz zmiany", + "save": "Zapisz", "close": "Zamknij", "pagination": "Numerowanie stron", "pagination.out_of": "%1 z %2", @@ -49,23 +52,25 @@ "users": "Użytkownicy", "topics": "Tematy", "posts": "Posty", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", - "views": "wyświetleń", + "best": "Najlepsze", + "upvoters": "Głosujący za", + "upvoted": "Oddane głosy za", + "downvoters": "Głosujący przeciw", + "downvoted": "Oddane głosy przeciw", + "views": "Wyświetleń", "reputation": "Punkty reputacji", "read_more": "czytaj więcej", "more": "Więcej", "posted_ago_by_guest": "wysłany %1 przez Gościa", "posted_ago_by": "wysłany %1 przez %2", "posted_ago": "wysłany %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", + "posted_in": "napisane w %1", + "posted_in_by": "napisane w %1 przez %2", "posted_in_ago": "wysłany w %1 %2", "posted_in_ago_by": "wysłany w %1 %2 przez %3", "user_posted_ago": "%1 wysłał %2", "guest_posted_ago": "Gość wysłał %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "ostatnio edytowany przez %1", "norecentposts": "Brak ostatnich postów", "norecenttopics": "Brak Ostatnich Tematów", "recentposts": "Ostatnie posty", @@ -81,14 +86,17 @@ "updated.title": "Forum zaktualizowane", "updated.message": "To forum zostało zaktualizowane do najnowszej wersji. Kliknij tutaj by odświeżyć stronę", "privacy": "Prywatność", - "follow": "Obserwuj", + "follow": "Śledź", "unfollow": "Przestań śledzić", "delete_all": "Usuń wszystko", "map": "Mapa", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "sessions": "Sesje logowania", + "ip_address": "Adres IP", + "enter_page_number": "Wpisz numer strony", + "upload_file": "Załaduj plik", + "upload": "Załaduj", + "allowed-file-types": "Dozwolone typy plików %1", + "unsaved-changes": "Posiadasz niezapisane zmiany. Jesteś pewien, że chcesz opuścić stronę?", + "reconnecting-message": "Wygląda na to, że Twoje połączenie z %1 zostało przerwane. Proszę czekać gdy staramy się je odnowić.", + "play": "Odtwórz" } \ No newline at end of file diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json index 845409bb43..d80403decd 100644 --- a/public/language/pl/groups.json +++ b/public/language/pl/groups.json @@ -24,7 +24,7 @@ "details.has_no_posts": "Członkowie tej grupy nie napisali żadnych postów.", "details.latest_posts": "Ostatnie posty", "details.private": "Prywatna", - "details.disableJoinRequests": "Disable join requests", + "details.disableJoinRequests": "Wyłączono prośbę o dołączenie", "details.grant": "Nadaj/Cofnij prawa Właściciela", "details.kick": "Wykop", "details.owner_options": "Administracja grupy", @@ -41,7 +41,7 @@ "details.hidden": "Ukryty", "details.hidden_help": "Jeśli aktywowane, ta grupa nie będzie widoczna w wykazie grup, a użytkownicy będą musieli być zapraszani manualnie.", "details.delete_group": "Usuń Grupę", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Prywatne grupy zostały zablokowane w systemie, ta opcja nic nie zmienia.", "event.updated": "Dane grupy zostały zaktualizowane", "event.deleted": "Grupa \"%1\" została skasowana", "membership.accept-invitation": "Przyjmij Zaproszenie", @@ -50,5 +50,7 @@ "membership.leave-group": "Opuść Grupę", "membership.reject": "Odrzuć", "new-group.group_name": "Nazwa Grupy:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Załaduj zdjęcie tła grupy", + "bulk-invite-instructions": "Wprowadź listę oddzielonych przecinkami nazw użytkowników, których chcesz zaprosić do tej grupy", + "bulk-invite": "Wielki zaproszenie" } \ No newline at end of file diff --git a/public/language/pl/login.json b/public/language/pl/login.json index 1133f6afba..1bcd2c3cd5 100644 --- a/public/language/pl/login.json +++ b/public/language/pl/login.json @@ -7,5 +7,6 @@ "alternative_logins": "Alternatywne logowanie", "failed_login_attempt": "Nie udało się zalogować. Spróbuj ponownie.", "login_successful": "Zostałeś pomyślnie zalogowany.", - "dont_have_account": "Nie masz konta?" + "dont_have_account": "Nie masz konta?", + "logged-out-due-to-inactivity": "Zostałeś wylogowany z Panelu Administratora z powodu braku aktywności." } \ No newline at end of file diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index ea0d3c2c10..cce67284de 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -6,9 +6,9 @@ "chat.user_typing": "%1 pisze...", "chat.user_has_messaged_you": "%1 napisał do Ciebie", "chat.see_all": "Zobacz wszystkie rozmowy", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Oznacz wszystkie rozmowy jako przeczytane", "chat.no-messages": "Wybierz odbiorcę, by wyświetlić historię rozmów.", - "chat.no-users-in-room": "No users in this room", + "chat.no-users-in-room": "Brak użytkowników w tym pokoju", "chat.recent-chats": "Ostatnie rozmowy", "chat.contacts": "Kontakty", "chat.message-history": "Historia wiadomości", @@ -17,9 +17,9 @@ "chat.seven_days": "7 dni", "chat.thirty_days": "30 dni", "chat.three_months": "3 miesiące", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", + "chat.delete_message_confirm": "Jesteś pewny, że chcesz usunąć tą wiadomość?", + "chat.roomname": "Pokój Czatu %1", + "chat.add-users-to-room": "Dodaj użytkownika do pokoju czatu", "composer.compose": "Twórz", "composer.show_preview": "Pokaż Podgląd", "composer.hide_preview": "Ukryj Podgląd", @@ -28,11 +28,21 @@ "composer.discard": "Na pewno chcesz porzucić ten post?", "composer.submit_and_lock": "Prześlij i Zablokuj", "composer.toggle_dropdown": "Przełącz Listę Rozwijaną", - "composer.uploading": "Uploading %1", + "composer.uploading": "Wysyłanie %1", + "composer.formatting.bold": "Pogrubienie", + "composer.formatting.italic": "Pochylenie", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Przekreślenie", + "composer.formatting.link": "Odnośnik", + "composer.formatting.picture": "Obraz", + "composer.upload-picture": "Wyślij obraz", + "composer.upload-file": "Wyślij plik", + "composer.zen_mode": "Tryb Zen", + "composer.select_category": "Wybierz kategorie", "bootbox.ok": "OK", "bootbox.cancel": "Anuluj", "bootbox.confirm": "Potwierdź", - "cover.dragging_title": "Cover Photo Positioning", - "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", - "cover.saved": "Cover photo image and position saved" + "cover.dragging_title": "Pozycjonowanie tła", + "cover.dragging_message": "Przeciągnij i upuść zdjęcie na żądanym miejscu, po zakończeniu kliknij \"Zapisz\"", + "cover.saved": "Tło zapisane" } \ No newline at end of file diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json index 5873b93a47..bc111f1e74 100644 --- a/public/language/pl/notifications.json +++ b/public/language/pl/notifications.json @@ -5,32 +5,29 @@ "mark_all_read": "Oznacz wszystkie powiadomienia jako przeczytane", "back_to_home": "Wróć do %1", "outgoing_link": "Łącze wychodzące", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "Opuszczasz %1", "continue_to": "Kontynuuj do %1", "return_to": "Wróć do %1", "new_notification": "Nowe powiadomienie", "you_have_unread_notifications": "Masz nieprzeczytane powiadomienia.", "new_message_from": "Nowa wiadomość od <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> zagłosował na Twój post w <strong>%2</strong>", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", - "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", + "upvoted_your_post_in_dual": "<strong>%1</strong> oraz<strong>%2</strong> za na twój post w <strong>%3</strong>.", + "upvoted_your_post_in_multiple": "<strong>%1</strong> oraz %2 innych zagłosowało za na twój post w<strong>%3</strong>.", + "moved_your_post": "<strong>%1</strong> przeniósł twoj post do <strong>%2</strong>", + "moved_your_topic": "<strong>%1</strong> przeniósł <strong>%2</strong>", "user_flagged_post_in": "<strong>%1<strong> oflagował Twój post w <strong>%2</strong>", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", + "user_flagged_post_in_dual": "<strong>%1</strong> oraz <strong>%2</strong> oflagowali post w <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> oraz %2 innych oflagowali post w <strong>%3</strong>", "user_posted_to": "<strong>%1</strong> dodał odpowiedź do <strong>%2</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", + "user_posted_to_dual": "<strong>%1</strong> oraz <strong>%2</strong> dodali odpowiedzi do <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> oraz %2 innych dodali odpowiedzi do <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> wysłał nowy temat: <strong>%2</strong>", - "user_started_following_you": "<string>%1</strong> zaczął Cię śledzić.", - "user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", + "user_started_following_you": "<string>%1</strong> zaczął obserwować ciebie.", + "user_started_following_you_dual": "<strong>%1</strong> oraz <strong>%2</strong> obserwuje ciebie.", + "user_started_following_you_multiple": "<strong>%1</strong> oraz %2 innych obserwuje ciebie.", "new_register": "<strong>%1</strong> wysłał żądanie rejestracji.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "Są <strong>%1</strong> nowe żądania rejestracji.", "email-confirmed": "E-mail potwierdzony", "email-confirmed-message": "Dziękujemy za potwierdzenie maila. Twoje konto zostało aktywowane.", "email-confirm-error-message": "Wystąpił problem przy aktywacji, - kod jest błędny lub przestarzały", diff --git a/public/language/pl/pages.json b/public/language/pl/pages.json index a9def8e4aa..41465e987a 100644 --- a/public/language/pl/pages.json +++ b/public/language/pl/pages.json @@ -6,17 +6,19 @@ "popular-month": "Tematy popularne w tym miesiącu", "popular-alltime": "Wszystkie popularne tematy", "recent": "Ostatnie Tematy", - "flagged-posts": "Flagged Posts", + "flagged-posts": "Oflagowane posty", "users/online": "Dostępni Użytkownicy", "users/latest": "Nowi Użytkownicy", "users/sort-posts": "Użytkownicy z największą liczbą postów", "users/sort-reputation": "Użytkownicy z najwyższą reputacją", - "users/banned": "Banned Users", + "users/banned": "Zbanowani użytkownicy", + "users/most-flags": "Najczęściej oznaczani użytkownicy", "users/search": "Wyszukiwanie Użytkownków", "notifications": "Powiadomienia", "tags": "Tagi", "tag": "Tematy oznaczone jako \"%1\"", "register": "Utwórz konto", + "registration-complete": "Rejestracja przebiegła pomyślnie", "login": "Zaloguj się na swoje konto", "reset": "Zresetuj hasło do swojego konta", "categories": "Kategorie", @@ -25,22 +27,23 @@ "chats": "Rozmowy", "chat": "Rozmowa z %1", "account/edit": "Edytowanie \"%1\"", - "account/edit/password": "Editing password of \"%1\"", - "account/edit/username": "Editing username of \"%1\"", - "account/edit/email": "Editing email of \"%1\"", + "account/edit/password": "Edytowanie hasła \"%1\"", + "account/edit/username": "Edytowanie nazwy \"%1\"", + "account/edit/email": "Edytowanie adresu email \"%1\"", + "account/info": "Informacje o koncie", "account/following": "Obserwowani przez %1", "account/followers": "Obserwujący %1", "account/posts": "Posty napisane przez %1", "account/topics": "Tematy stworzone przez %1", "account/groups": "Grupy %1", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Ustawienia Użytkownika", "account/watched": "Tematy obserwowane przez %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "account/upvoted": "Posty za głosowane za przez %1", + "account/downvoted": "Posty za głosowane przeciw przez %1", + "account/best": "Najlepsze posty od %1", + "confirm": "E-mail potwierdzony", "maintenance.text": "Obecnie trwają prace konserwacyjne nad %1. Proszę wrócić później.", "maintenance.messageIntro": "Dodatkowo, administrator zostawił wiadomość:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "throttled.text": "%1 jest niedostępny z powodu obciążenia. Proszę wrócić później." } \ No newline at end of file diff --git a/public/language/pl/register.json b/public/language/pl/register.json index 84c0430d87..49c89fb6ac 100644 --- a/public/language/pl/register.json +++ b/public/language/pl/register.json @@ -1,5 +1,6 @@ { "register": "Rejestracja", + "cancel_registration": "Anuluj rejestrację", "help.email": "Domyślnie twój adres e-mail będzie ukryty.", "help.username_restrictions": "Unikalna nazwa użytkownika z min. %1 i maks. %2 znaków. Inni użytkownicy mogą ciebie zawołać pisząc @<span id='yourUsername'>nazwa użytkownika</span>.", "help.minimum_password_length": "Hasło musi mieć co najmniej %1 znaków.", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatywna rejestracja", "terms_of_use": "Warunki korzystania z serwisu", "agree_to_terms_of_use": "Zgadzam się na powyższe warunki", - "registration-added-to-queue": "Twoja rejestracja została dodana do kolejki oczekujących na akceptację. Otrzymasz email, kiedy zostanie zatwierdzona przez administratora." + "terms_of_use_error": "Musisz zaakceptować Terms of Use", + "registration-added-to-queue": "Twoja rejestracja została dodana do kolejki oczekujących na akceptację. Otrzymasz email, kiedy zostanie zatwierdzona przez administratora.", + "interstitial.intro": "Potrzebujemy dodatkowych informacji zanim przejdziemy dalej do utworzenia Twojego konta.", + "interstitial.errors-found": "Nie mogliśmy ukończyć procesu rejestracji:" } \ No newline at end of file diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 974a61dd94..a6b632d345 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Powiadamiaj mnie o nowych odpowiedziach w tym temacie", "quote": "Cytuj", "reply": "Odpowiedz", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "Odpowiedz na temat", "guest-login-reply": "Zaloguj się, aby odpowiedzieć.", "edit": "Edytuj", "delete": "Usuń", @@ -26,23 +26,47 @@ "tools": "Narzędzia", "flag": "Zgłoś", "locked": "Zablokowany", - "bookmark_instructions": "Kliknij tutaj, aby powrócic do ostatniego nieprzeczytanego postu w tym wątku.", + "pinned": "Przypięte", + "moved": "Przeniesione", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Zgłoś post do moderacji", "flag_success": "Ten post został oznaczony do moderacji.", + "flag_manage_title": "Oflagowanych postów do %1", + "flag_manage_history": "Historia wykonanych akcji", + "flag_manage_no_history": "Brak historii eventów ", + "flag_manage_assignee": "Beneficjant", + "flag_manage_state": "Stan", + "flag_manage_state_open": "Nowy/Otwarty", + "flag_manage_state_wip": "W trakcie prac", + "flag_manage_state_resolved": "Zdecydowany", + "flag_manage_state_rejected": "Odrzucono", + "flag_manage_notes": "Udostępniono notki", + "flag_manage_update": "Zaktualizowano status oflagowanych postów", + "flag_manage_history_assignee": "Przypisane do %1", + "flag_manage_history_state": "Zaktualizowano stan do %1", + "flag_manage_history_notes": "Zaktualizowano notki oflagowanych postów", + "flag_manage_saved": "Zaktualizowano oflagowane posty", "deleted_message": "Ten temat został skasowany. Tylko użytkownicy z uprawnieniami do zarządzania mogą go zobaczyć.", "following_topic.message": "Będziesz od teraz otrzymywał powiadomienia, gdy ktoś odpowie w tym temacie.", - "not_following_topic.message": "Nie będziesz już otrzymywał powiadomień z tego tematu.", + "not_following_topic.message": "Zobaczysz ten temat na liście nieprzeczytanych, ale nie otrzymasz żadnego powiadomienia dotyczącego tego tematu.", + "ignoring_topic.message": "Nie zobaczysz już tego tematu na liście nieprzeczytanych. Otrzymasz powiadomienie, kiedy zostaniesz wspomniany lub ktoś odda głos na twój post.", "login_to_subscribe": "Zaloguj się, aby subskrybować ten temat.", "markAsUnreadForAll.success": "Temat oznaczony jako nieprzeczytany dla wszystkich.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "Oznacz jako nieprzeczytany", + "mark_unread.success": "Temat oznaczony jako nieprzeczytany.", "watch": "Obserwuj", "unwatch": "Nie obserwuj", "watch.title": "Otrzymuj powiadomienia o nowych odpowiedziach w tym temacie", "unwatch.title": "Przestań obserwować ten temat", "share_this_post": "Udostępnij", + "watching": "Obserwuj", + "not-watching": "Nie obserwuj", + "ignoring": "Ignoruj", + "watching.description": "Powiadom mnie o nowych odpowiedziach<br/>Pokaż temat w nieprzeczytanych.", + "not-watching.description": "Nie informuj mnie o nowych odpowiedziach<br/>Pokaż temat w nieprzeczytanych, jeśli kategoria nie jest ignorowana.", + "ignoring.description": "Nie informuj mnie o nowych odpowiedziach<br/>Nie pokazuj tematu w nieprzeczytanych.", "thread_tools.title": "Narzędzia Tematu", - "thread_tools.markAsUnreadForAll": "Oznacz jako nieprzeczytany", + "thread_tools.markAsUnreadForAll": "Zaznacz wszystkie jako nieprzeczytane", "thread_tools.pin": "Przypnij Temat", "thread_tools.unpin": "Odepnij Temat", "thread_tools.lock": "Zablokuj Temat", @@ -51,7 +75,7 @@ "thread_tools.move_all": "Przenieś wszystko", "thread_tools.fork": "Skopiuj Temat", "thread_tools.delete": "Usuń Temat", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "Usuń posty", "thread_tools.delete_confirm": "Na pewno chcesz skasować ten temat?", "thread_tools.restore": "Przywróć Temat", "thread_tools.restore_confirm": "Na pewno chcesz przywrócić ten temat?", @@ -65,9 +89,9 @@ "disabled_categories_note": "Zablokowane kategorie zostały wyszarzone.", "confirm_move": "Przenieś", "confirm_fork": "Skopiuj", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Ulubione", + "favourites": "Ulubione", + "favourites.has_no_favourites": "Nie masz jeszcze ulubionych postów.", "loading_more_posts": "Załaduj więcej postów", "move_topic": "Przenieś Temat", "move_topics": "Przenieś Tematy", @@ -77,8 +101,9 @@ "topic_will_be_moved_to": "Ten temat zostanie przeniesiony do kategorii", "fork_topic_instruction": "Zaznacz posty, które chcesz sklonować", "fork_no_pids": "Nie zaznaczyłeś żadnych postów!", + "fork_pid_count": "wybrano %1 post(-ów)", "fork_success": "Udało się skopiować temat. Kliknij tutaj, aby do niego przejść.", - "delete_posts_instruction": "Click the posts you want to delete/purge", + "delete_posts_instruction": "Kliknij na posty, które chcesz usunąć", "composer.title_placeholder": "Wpisz tutaj tytuł tematu...", "composer.handle_placeholder": "Nazwa", "composer.discard": "Odrzuć", @@ -88,7 +113,7 @@ "composer.uploading": "wysyłanie...", "composer.thumb_url_label": "Wklej adres miniaturki tematu", "composer.thumb_title": "Dodaj miniaturkę do tego tematu", - "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_url_placeholder": "http://przykład.pl/thumb.png", "composer.thumb_file_label": "lub wyślij plik", "composer.thumb_remove": "Wyczyść pola", "composer.drag_and_drop_images": "Przeciągnij i upuść obrazek tutaj.", @@ -101,12 +126,12 @@ "newest_to_oldest": "Najpierw najnowsze", "most_votes": "Najwięcej głosów", "most_posts": "Najwięcej postów", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", + "stale.title": "Stworzyć nowy temat?", + "stale.warning": "Wątek na który chcesz udzielić odpowiedzi jest przestarzały. Czy nie chciałbyś, utworzyć nowego tematu zamiast udzielać się w tym.", + "stale.create": "Stwórz nowy temat", + "stale.reply_anyway": "Odpowiedź na ten temat mimo tego", "link_back": "Re: [%1](%2)", "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "offensive": "Obrażliwy", + "custom-flag-reason": "Wprowadź powód oznaczenia" } \ No newline at end of file diff --git a/public/language/pl/unread.json b/public/language/pl/unread.json index dbee60d817..872ae755d1 100644 --- a/public/language/pl/unread.json +++ b/public/language/pl/unread.json @@ -6,5 +6,8 @@ "selected": "Wybrane", "all": "Wszystkie", "all_categories": "Wszystkie kategorie", - "topics_marked_as_read.success": "Tematy zostały oznaczone jako przeczytane!" + "topics_marked_as_read.success": "Tematy zostały oznaczone jako przeczytane!", + "all-topics": "Wszystkie tematy", + "new-topics": "Nowe tematy", + "watched-topics": "Obserwowane tematy" } \ No newline at end of file diff --git a/public/language/pl/uploads.json b/public/language/pl/uploads.json index 1622cb5693..f29dc364f2 100644 --- a/public/language/pl/uploads.json +++ b/public/language/pl/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Wysyłanie pliku...", + "select-file-to-upload": "Zaznacz plik do wgrania!", + "upload-success": "Pomyślnie załadowano plik!", + "maximum-file-size": "Maksymalna wielkość %1 kb" } \ No newline at end of file diff --git a/public/language/pl/user.json b/public/language/pl/user.json index a5f79daab8..7d5f06b7b0 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -6,6 +6,7 @@ "postcount": "Liczba postów", "email": "Adres e-mail", "confirm_email": "Potwierdź e-mail", + "account_info": "Informacje o koncie", "ban_account": "Zbanuj Konto", "ban_account_confirm": "Na pewno chcesz zbanować tego użytkownika?", "unban_account": "Odbanuj Konto", @@ -20,27 +21,27 @@ "joined": "Dołączono", "lastonline": "Ostatnio online", "profile": "Profil", - "profile_views": "wyświetleń", - "reputation": "reputacji", - "favourites": "Bookmarks", + "profile_views": "Wyświetleń", + "reputation": "Reputacji", + "favourites": "Ulubione", "watched": "Obserwowane", - "followers": "Obserwujących", - "following": "Obserwowanych", + "followers": "Śledzących", + "following": "Śledzonych", "aboutme": "O mnie", "signature": "Sygnatura", "birthday": "Urodziny", "chat": "Rozmawiaj", - "chat_with": "Chat with %1", + "chat_with": "Rozmawiaj z %1", "follow": "Śledź", "unfollow": "Przestań śledzić", "more": "Więcej", "profile_update_success": "Profil został zaktualizowany pomyślnie!", "change_picture": "Zmień zdjęcie", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Zmień nazwę użytkownika", + "change_email": "Zmień adres e-mail", "edit": "Edytuj", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Edytuj profil", + "default_picture": "Domyślna ikona", "uploaded_picture": "Przesłane zdjęcie", "upload_new_picture": "Prześlij nowe zdjęcie", "upload_new_picture_from_url": "Wgraj zdjęcie z adresu URL", @@ -56,11 +57,11 @@ "password": "Hasło", "username_taken_workaround": "Wybrany login jest już zajęty, więc zmieniliśmy go trochę. Proponujemy <strong>%1</strong>", "password_same_as_username": "Twoje hasło jest takie samo jak nazwa użytkownika, prosimy wybrać inne hasło.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "Twoje hasło jest takie samo jak adres e-mail, prosimy wybrać inne hasło.", "upload_picture": "Prześlij zdjęcie", "upload_a_picture": "Prześlij zdjęcie", "remove_uploaded_picture": "Usuń Przesłane Zdjęcie", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Prześlij zdjęcie tła", "settings": "Ustawienia", "show_email": "Wyświetlaj mój adres e-mail", "show_fullname": "Wyświetlaj moją pełną nazwę", @@ -74,36 +75,55 @@ "send_chat_notifications": "Wyślij e-maila, jeśli dostanę nową wiadomość, a nie jestem on-line", "send_post_notifications": "Wyślij email, kiedy w tematach, które subskrybuję pojawią się odpowiedzi", "settings-require-reload": "Niektóre zmiany ustawień wymagają przeładowania. Kliknij tutaj, aby przeładować stronę.", - "has_no_follower": "Ten użytkownik nie ma jeszcze żadnych obserwujących", - "follows_no_one": "Użytkownik jeszcze nikogo nie obsweruje.", + "has_no_follower": "Ten użytkownik nie ma jeszcze żadnych śledzących", + "follows_no_one": "Użytkownik jeszcze nikogo nie śledzi.", "has_no_posts": "Ten użytkownik nic jeszcze nie napisał.", "has_no_topics": "Ten użytkownik nie stworzył jeszcze żadnych tematów.", "has_no_watched_topics": "Ten użytkownik nie obserwuje jeszcze żadnych tematów.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Ten użytkownik jeszcze nie głosował za w żadnym temacie", + "has_no_downvoted_posts": "Ten użytkownik jeszcze nie głosował przeciw w żadnym temacie.", + "has_no_voted_posts": "Ten użytkownik nie ma jeszcze ocenionych postów", "email_hidden": "Adres e-mail ukryty", "hidden": "ukryty", "paginate_description": "Dziel tematy i posty na strony zamiast używać nieskończonego przewijania", "topics_per_page": "Tematów na Stronę", "posts_per_page": "Postów na stronę", "notification_sounds": "Odtwarzaj dźwięk, gdy otrzymujesz powiadomienie", + "notifications_and_sounds": "Powiadomienia i dźwięki", + "incoming-message-sound": "Dźwięk przychodzącej wiadomości", + "outgoing-message-sound": "Dźwięk wychodzącej wiadomości", + "notification-sound": "Dźwięk powiadomienia", + "no-sound": "Bez dźwięku", "browsing": "Ustawienia szukania", "open_links_in_new_tab": "Otwieraj linki wychodzące w nowej karcie", "enable_topic_searching": "Odblokuj szukanie w temacie", "topic_search_help": "Jeśli włączone, wyszukiwanie w tematach zastąpi przeglądarkowe przeszukiwanie strony i pozwoli na przeszukanie całego tematu, zamiast ograniczonej zawartości aktualnie wyświetlonej na ekranie", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Śledź tematy, na które odpowiadasz", - "follow_topics_you_create": "Śledź tematy, które tworzysz", - "grouptitle": "Wybierz tytuł grupy, który chcesz wyświetlać", + "delay_image_loading": "Opóźnienie ładowania zdjęcia", + "image_load_delay_help": "Jeśli włączone, zdjęcia w temacie nie załadują się dopóki nie najedzie się", + "scroll_to_my_post": "Po napisaniu odpowiedzi, wyświetl najnowsze posty", + "follow_topics_you_reply_to": "Obserwuj tematy w których uczestniczysz", + "follow_topics_you_create": "Obserwuj tematy które utworzyłeś", + "grouptitle": "Tytuł grupy", "no-group-title": "Brak tytułu grupy", "select-skin": "Wybierz Skórkę", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", - "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", + "select-homepage": "Wybierz stronę startową", + "homepage": "Strona startowa", + "homepage_description": "Wybierz stroną jaką chcesz mieć ustawioną na domyślną, lub \"None\" jeśli chcesz używać domyślnej. ", + "custom_route": "Niestandardowa strona startowa", + "custom_route_help": "Wpisz niestandardowa stronę startową, bez slashu (np. \"recent\", albo \"popular\")", "sso.title": "Usługi Pojedynczego Logowania", "sso.associated": "Powiązane z", - "sso.not-associated": "Kliknij tutaj, aby powiązać z" + "sso.not-associated": "Kliknij tutaj, aby powiązać z", + "info.latest-flags": "Ostatnie flagi", + "info.no-flags": "Brak oflagowanych postów", + "info.ban-history": "Historia ostatnich banów", + "info.no-ban-history": "Ten użytkownik nigdy nie był zbanowany", + "info.banned-until": "Zbanowany do %1", + "info.banned-permanently": "Zbanowany permanentnie", + "info.banned-reason-label": "Powód", + "info.banned-no-reason": "Nie podano powodu.", + "info.username-history": "Historia nazwy użytkownika", + "info.email-history": "Historia adresu e-mail", + "info.moderation-note": "Notka moderatora", + "info.moderation-note.success": "Notka nie została zapisana" } \ No newline at end of file diff --git a/public/language/pl/users.json b/public/language/pl/users.json index f21c62380a..fbe4957dbe 100644 --- a/public/language/pl/users.json +++ b/public/language/pl/users.json @@ -2,6 +2,7 @@ "latest_users": "Nowi użytkownicy", "top_posters": "Najwięcej postów", "most_reputation": "Największa reputacja", + "most_flags": "Najwięcej flag", "search": "Szukaj", "enter_username": "Wpisz wyszukiwaną nazwę użytkownika", "load_more": "Wczytaj Więcej", @@ -16,5 +17,5 @@ "unread_topics": "Nieprzeczytane Tematy", "categories": "Kategorie", "tags": "Tagi", - "no-users-found": "No users found!" + "no-users-found": "Nie znaleziono pasujących użytkowników!" } \ No newline at end of file diff --git a/public/language/pt_BR/category.json b/public/language/pt_BR/category.json index 02ce999999..0152ea54d0 100644 --- a/public/language/pt_BR/category.json +++ b/public/language/pt_BR/category.json @@ -10,7 +10,11 @@ "share_this_category": "Compartilhar esta categoria", "watch": "Acompanhar", "ignore": "Ignorar", - "watch.message": "Agora você está acompanhando as atualizações desta categoria", - "ignore.message": "Agora você está ignorando as atualizações desta categoria", + "watching": "Assistindo", + "ignoring": "Ignorando", + "watching.description": "Mostrar tópicos em não-lido", + "ignoring.description": "Não mostrar tópicos em não-lido", + "watch.message": "Agora você está seguindo as atualizações desta categoria e de todas as subcategorias", + "ignore.message": "Agora você está ignorando as atualizações desta categoria e de todas as subcategorias", "watched-categories": "Categorias acompanhadas" } \ No newline at end of file diff --git a/public/language/pt_BR/email.json b/public/language/pt_BR/email.json index c773f3236b..64ef3928a9 100644 --- a/public/language/pt_BR/email.json +++ b/public/language/pt_BR/email.json @@ -24,6 +24,7 @@ "digest.day": "dia", "digest.week": "semana", "digest.month": "mês", + "digest.subject": "Resumo de %1", "notif.chat.subject": "Nova mensagem de chat recebida de %1", "notif.chat.cta": "Clique aqui para continuar a conversa", "notif.chat.unsub.info": "Esta notificação de chat foi enviada a você devido às suas configurações de assinatura.", diff --git a/public/language/pt_BR/error.json b/public/language/pt_BR/error.json index e681f5664f..291656a8e9 100644 --- a/public/language/pt_BR/error.json +++ b/public/language/pt_BR/error.json @@ -14,20 +14,25 @@ "invalid-password": "Senha Inválida", "invalid-username-or-password": "Por favor especifique ambos nome de usuário e senha", "invalid-search-term": "Termo de pesquisa inválido", + "csrf-invalid": "Nós não fomos capazes de logá-lo, provavelmente devido à uma sessão expirada. Por favor tente novamente.", "invalid-pagination-value": "Valor de paginação inválido, precisa ser entre no mínimo %1 e no máximo %2", "username-taken": "Nome de usuário já existe", "email-taken": "Email já cadastrado", "email-not-confirmed": "O seu email ainda não foi confirmado, por favor clique aqui para confirmar seu email.", "email-not-confirmed-chat": "Você não está habilitado a conversar até que seu email seja confirmado, por favor clique aqui para confirmar seu email.", + "email-not-confirmed-email-sent": "O seu email ainda não foi confirmado, por favor confira a sua caixa de entrada pelo email de confirmação.", "no-email-to-confirm": "Este fórum exige confirmação de email, por gentileza clique aqui para digitar um email", "email-confirm-failed": "Nós não pudemos confirmar seu email, por gentileza tente novamente mais tarde.", "confirm-email-already-sent": "O email de confirmação já foi enviado, por favor aguarde %1 minuto(s) para enviar outro.", + "sendmail-not-found": "O executável do sendmail não pôde ser encontrado, por favor se certifique de que ele está instalado e é executável pelo usuário que roda o NodeBB.", "username-too-short": "Nome de usuário muito curto", "username-too-long": "Nome de usuário muito longo", "password-too-long": "A senha é muito grande", "user-banned": "Usuário banido", + "user-banned-reason": "Usuário banido (Motivo: %1)", "user-too-new": "Desculpe, é necessário que você aguarde %1 segundo(s) antes de fazer o seu primeiro post.", "blacklisted-ip": "Desculpe, o seu endereço IP foi banido desta comunidade. Se você acha que isso é um engano, por favor contate um administrador.", + "ban-expiry-missing": "Por favor forneça uma data para o fim deste banimento", "no-category": "A categoria não existe", "no-topic": "O tópico não existe", "no-post": "O post não existe", @@ -38,10 +43,26 @@ "category-disabled": "Categoria desativada", "topic-locked": "Tópico Trancado", "post-edit-duration-expired": "Você só pode editar posts %1 segundo(s) após postar.", + "post-edit-duration-expired-minutes": "Você pode apenas editar posts por %1 minuto(s) após postar", + "post-edit-duration-expired-minutes-seconds": "Você pode apenas editar posts por %1 minuto(s) e %2 segundo(s) após postar", + "post-edit-duration-expired-hours": "Você pode apenas editar posts por %1 hora(s) após postar", + "post-edit-duration-expired-hours-minutes": "Você pode apenas editar posts por %1 hora(s) e %2 minuto(s) após postar", + "post-edit-duration-expired-days": "Você pode apenas editar posts por %1 dia(s) após postar", + "post-edit-duration-expired-days-hours": "Você pode apenas editar posts por %1 dia(s) e %2 hora(s) após postar", + "post-delete-duration-expired": "Você só pode deletar posts por %1 segundo(s) depois de postar", + "post-delete-duration-expired-minutes": "Você só pode deletar posts por %1 minuto(s) depois de postar", + "post-delete-duration-expired-minutes-seconds": "Você só pode deletar posts por %1 minuto(s) e %2 segundo(s) depois de postar", + "post-delete-duration-expired-hours": "Você só pode deletar posts por %1 hora(s) depois de postar", + "post-delete-duration-expired-hours-minutes": "Você só pode deletar posts por %1 hora(s) e %2 minutos(s) depois de postar", + "post-delete-duration-expired-days": "Você só pode deletar posts por %1 dia(s) depois de postar", + "post-delete-duration-expired-days-hours": "Você só pode deletar posts por %1 dia(s) e %2 hora(s) depois de postar", + "cant-delete-topic-has-reply": "Você não pode excluir o seu tópico após ele ter uma resposta", + "cant-delete-topic-has-replies": "Você não pode excluir o seu tópico após ele ter %1 respostas", "content-too-short": "Por favor digite um post maior. Posts precisam conter ao menos %1 caractere(s).", "content-too-long": "Por favor digite um post mais curto. Posts não podem ser maiores que %1 caractere(s)", "title-too-short": "Por favor digite um título maior. Títulos devem conter no mínimo %1 caractere(s)", "title-too-long": "Por favor digite um título menor. Títulos não podem ser maiores que %1 caractere(s).", + "category-not-selected": "Categoria não escolhida.", "too-many-posts": "Você pode postar uma vez a cada %1 segundo(s) - por favor aguarde antes de postar novamente", "too-many-posts-newbie": "Como novo usuário, você pode postar uma vez a cada %1 segundo(s) até que você tenha recebido reputação de %2 - por favor aguarde antes de postar novamente", "tag-too-short": "Por favor digite uma tag maior. Tags devem conter pelo menos %1 caractere(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "Você já removeu este post dos favoritos", "cant-ban-other-admins": "Você não pode banir outros administradores!", "cant-remove-last-admin": "Você é o único administrador. Adicione outro usuário como administrador antes de remover a si mesmo como admin", + "cant-delete-admin": "Remova o privilégio de administrador desta conta antes de tentar deletá-la.", "invalid-image-type": "Tipo inválido de imagem. Os tipos permitidos são: %1", "invalid-image-extension": "Extensão de imagem inválida", "invalid-file-type": "Tipo de arquivo inválido. Os tipos permitidos são: %1", "group-name-too-short": "Nome do grupo é muito curto", + "group-name-too-long": "O nome do grupo é muito extenso", "group-already-exists": "O grupo já existe", "group-name-change-not-allowed": "Sem permissão para alterar nome do grupo", "group-already-member": "Já faz parte deste grupo", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Você não tem permissão para editar esta mensagem", "cant-remove-last-user": "Você não pode excluir o último usuário", "cant-delete-chat-message": "Você não possui permissão para deletar esta mensagem", + "already-voting-for-this-post": "Você já votou neste post.", "reputation-system-disabled": "O sistema de reputação está desabilitado.", "downvoting-disabled": "Negativação está desabilitada", "not-enough-reputation-to-downvote": "Você não possui reputação suficiente para negativar este post", @@ -99,5 +123,9 @@ "no-session-found": "Nenhuma sessão de login encontrada!", "not-in-room": "O usuário não está na sala", "no-users-in-room": "Nenhum usuário nesta sala", - "cant-kick-self": "Você não pode kickar a si mesmo do grupo" + "cant-kick-self": "Você não pode kickar a si mesmo do grupo", + "no-users-selected": "Nenhuma escolha de usuário(s) foi feita", + "invalid-home-page-route": "Rota de página inicial inválida", + "invalid-session": "Erro de Sessão", + "invalid-session-text": "Parece que sua sessão de login não está mais ativa, ou não combina mais com a do servidor. Por gentileza, recarregue esta página." } \ No newline at end of file diff --git a/public/language/pt_BR/global.json b/public/language/pt_BR/global.json index cb79fa97d5..352f1633e7 100644 --- a/public/language/pt_BR/global.json +++ b/public/language/pt_BR/global.json @@ -7,8 +7,10 @@ "403.login": "Talvez você deveria <a href='%1/login'>tentar fazer login</a>?", "404.title": "Não Encontrado", "404.message": "Parece que você chegou à uma página que não existe. Voltar para a <a href='%1/'>página inicial</a>.", - "500.title": "Erro interno.", + "500.title": "Erro Interno.", "500.message": "Oops! Parece que algo deu errado!", + "400.title": "Solicitação Inválida.", + "400.message": "Parece que esse link contém informação inválida, por favor o verifique novamente. Senão, retorne para a <a href='%1/'>página inicial</a>.", "register": "Cadastrar", "login": "Login", "please_log_in": "Por Favor Efetue o Login", @@ -17,6 +19,7 @@ "welcome_back": "Bem-vindo de volta", "you_have_successfully_logged_in": "Você logou com sucesso", "save_changes": "Salvar Alterações", + "save": "Salvar", "close": "Fechar", "pagination": "Paginação", "pagination.out_of": "%1 de %2", @@ -50,7 +53,9 @@ "topics": "Tópicos", "posts": "Posts", "best": "Melhor", + "upvoters": "Cimavotadores", "upvoted": "Votado positivamente", + "downvoters": "Baixovotadores", "downvoted": "Votado negativamente", "views": "Visualizações", "reputation": "Reputação", @@ -90,5 +95,8 @@ "enter_page_number": "Digite o número da página", "upload_file": "Fazer upload de arquivo", "upload": "Upload", - "allowed-file-types": "Os tipos de arquivo permitidos são %1" + "allowed-file-types": "Os tipos de arquivo permitidos são %1", + "unsaved-changes": "Você tem alterações não salvas. Tem certeza que você deseja sair da página?", + "reconnecting-message": "Parece que sua conexão com %1 caiu, por favor aguarde enquanto tentamos reconectar.", + "play": "Executar" } \ No newline at end of file diff --git a/public/language/pt_BR/groups.json b/public/language/pt_BR/groups.json index ed5727555f..5a6cdd04f2 100644 --- a/public/language/pt_BR/groups.json +++ b/public/language/pt_BR/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Deixar Grupo", "membership.reject": "Rejeitar", "new-group.group_name": "Nome do Grupo:", - "upload-group-cover": "Fazer upload de capa do grupo" + "upload-group-cover": "Fazer upload de capa do grupo", + "bulk-invite-instructions": "Entre uma lista separada por vírgulas de nome usuários para convidar à este grupo", + "bulk-invite": "Convite em Lote" } \ No newline at end of file diff --git a/public/language/pt_BR/login.json b/public/language/pt_BR/login.json index 7b8299af75..91717bb8ce 100644 --- a/public/language/pt_BR/login.json +++ b/public/language/pt_BR/login.json @@ -5,7 +5,8 @@ "remember_me": "Lembrar de Mim?", "forgot_password": "Esqueceu a Senha?", "alternative_logins": "Logins Alternativos", - "failed_login_attempt": "Falha no login, por favor tente novamente.", + "failed_login_attempt": "Falha no Login", "login_successful": "Você logou com sucesso!", - "dont_have_account": "Não tem uma conta?" + "dont_have_account": "Não tem uma conta?", + "logged-out-due-to-inactivity": "Você saiu do Painel de Controle de Administração devido à inatividade" } \ No newline at end of file diff --git a/public/language/pt_BR/modules.json b/public/language/pt_BR/modules.json index 597e750246..602ef9dc03 100644 --- a/public/language/pt_BR/modules.json +++ b/public/language/pt_BR/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Enviar e Trancar", "composer.toggle_dropdown": "Alternar Dropdown", "composer.uploading": "Enviando %1", + "composer.formatting.bold": "Negrito", + "composer.formatting.italic": "Itálico", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Riscado", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Imagem", + "composer.upload-picture": "Fazer upload de Imagem", + "composer.upload-file": "Fazer upload de Arquivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Escolha uma categoria", "bootbox.ok": "OK", "bootbox.cancel": "Cancelar", "bootbox.confirm": "Confirmar", diff --git a/public/language/pt_BR/notifications.json b/public/language/pt_BR/notifications.json index ae335eca3a..2ef1d95178 100644 --- a/public/language/pt_BR/notifications.json +++ b/public/language/pt_BR/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> e %2 outros deram voto positivo ao seu post em <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> moveu seu post para <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> se mudou <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> favoritou o teu post em <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> favoritaram o teu post em <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> e %2 outros favoritaram o teu post em <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> sinalizou um post em <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> sinalizaram um post em <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> e %2 outros sinalizaram um post em <strong>%3</strong>", diff --git a/public/language/pt_BR/pages.json b/public/language/pt_BR/pages.json index ac5238bb22..4114c21982 100644 --- a/public/language/pt_BR/pages.json +++ b/public/language/pt_BR/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Usuários com mais posts", "users/sort-reputation": "Usuários com maior reputação", "users/banned": "Usuários Banidos", + "users/most-flags": "Usuários mais sinalizados", "users/search": "Pesquisa de Usuários", "notifications": "Notificações", "tags": "Tags", "tag": "Tópicos com a tag \"%1\"", "register": "Registrar uma conta", + "registration-complete": "Registro completado", "login": "Entrar na sua conta", "reset": "Redefinir a senha da sua conta", "categories": "Categorias", @@ -28,12 +30,13 @@ "account/edit/password": "Editando senha de \"%1\"", "account/edit/username": "Editando nome de usuário de \"%1\"", "account/edit/email": "Editando email de \"%1\"", + "account/info": "Informação da Conta", "account/following": "Pessoas que %1 segue", "account/followers": "Pessoas que seguem %1", "account/posts": "Posts feitos por %1", "account/topics": "Tópicos criados por %1", "account/groups": "Grupos de %1", - "account/favourites": "Posts Favoritados por %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Configurações de Usuário", "account/watched": "Tópicos assistidos por %1", "account/upvoted": "Posts votados positivamente por %1", diff --git a/public/language/pt_BR/register.json b/public/language/pt_BR/register.json index e641a7a791..1070dce7ec 100644 --- a/public/language/pt_BR/register.json +++ b/public/language/pt_BR/register.json @@ -1,5 +1,6 @@ { "register": "Cadastrar", + "cancel_registration": "Cancelar Cadastro", "help.email": "Por padrão seu email ficará invisível para o publico.", "help.username_restrictions": "Um nome de usuário único entre %1 e %2 caracteres. Os outros poderão te mencionar digitando @<span id='yourUsername'>usuário</span>.", "help.minimum_password_length": "Sua senha tem que ter no mínimo %1 caracteres.", @@ -15,5 +16,8 @@ "alternative_registration": "Cadastro Alternativo", "terms_of_use": "Termos de Uso", "agree_to_terms_of_use": "Eu concordo com os Termos de Uso", - "registration-added-to-queue": "O seu cadastro foi adicionado à fila de aprovação. Você receberá um email quando ele for aceito por um administrador." + "terms_of_use_error": "Você deve concordar com os Termos de Uso", + "registration-added-to-queue": "O seu cadastro foi adicionado à fila de aprovação. Você receberá um email quando ele for aceito por um administrador.", + "interstitial.intro": "Nós pedimos alguma informação adicional antes que você possa criar a sua conta.", + "interstitial.errors-found": "Nós não pudemos completar o seu cadastro:" } \ No newline at end of file diff --git a/public/language/pt_BR/topic.json b/public/language/pt_BR/topic.json index 5f2d6257c1..14d35366da 100644 --- a/public/language/pt_BR/topic.json +++ b/public/language/pt_BR/topic.json @@ -26,12 +26,30 @@ "tools": "Ferramentas", "flag": "Sinalizar", "locked": "Trancado", - "bookmark_instructions": "Clique aqui para retornar ao último post não lido nesta thread.", + "pinned": "Afixado", + "moved": "Movido", + "bookmark_instructions": "Clique aqui para retornar ao último post lido neste tópico.", "flag_title": "Sinalizar este post para moderação", "flag_success": "Este post foi sinalizado para ser moderado.", + "flag_manage_title": "Post sinalizado em %1", + "flag_manage_history": "Histórico de Atividades", + "flag_manage_no_history": "Nenhum histórico de eventos para mostrar", + "flag_manage_assignee": "Procurador", + "flag_manage_state": "Situação", + "flag_manage_state_open": "Novo/Aberto", + "flag_manage_state_wip": "Trabalho em Andamento", + "flag_manage_state_resolved": "Resolvido", + "flag_manage_state_rejected": "Rejeitado", + "flag_manage_notes": "Notas Compartilhadas", + "flag_manage_update": "Atualizar Status da Sinalização", + "flag_manage_history_assignee": "Procuração concedida para %1", + "flag_manage_history_state": "Situação atualizada para %1", + "flag_manage_history_notes": "Notas de sinalização atualizadas", + "flag_manage_saved": "Detalhes da Sinalização Atualizados", "deleted_message": "Este tópico foi deletado. Apenas usuários com privilégios de moderação de tópico podem vê-lo.", "following_topic.message": "Agora você receberá notificações quando alguém responder este tópico.", - "not_following_topic.message": "Você não receberá mais notificações sobre este tópico.", + "not_following_topic.message": "Você verá este tópico na lista de tópicos não-lidos, mas você não receberá notificações quendo alguém posta no tópico.", + "ignoring_topic.message": "Você não verá mais este tópico na lista de tópicos não lidos. Você será notificado quando você for mencionado ou sua postagem for votada positivamente.", "login_to_subscribe": "Por favor se cadastre ou entre para assinar à este tópico.", "markAsUnreadForAll.success": "Tópico marcado como não lido para todos.", "mark_unread": "Marcar como não lidas", @@ -41,8 +59,14 @@ "watch.title": "Seja notificado sobre novas respostas neste tópico", "unwatch.title": "Parar de acompanhar este tópico", "share_this_post": "Compartilhar este Post", + "watching": "Seguindo", + "not-watching": "Não Seguindo", + "ignoring": "Ignorando", + "watching.description": "Me notificar de novas respostas.<br/>Mostrar tópico em não-lidos.", + "not-watching.description": "Não me notificar de novas respostas.<br/>Mostrar tópico em não-lido se a categoria não estiver sendo ignorada.", + "ignoring.description": "Não me notificar de novas respostas.<br/>Não mostrar tópico em não-lido.", "thread_tools.title": "Ferramentas de Tópico", - "thread_tools.markAsUnreadForAll": "Marcar Não Lido", + "thread_tools.markAsUnreadForAll": "Marcar como não-lido para todos", "thread_tools.pin": "Fixar Tópico", "thread_tools.unpin": "Desfixar Tópico", "thread_tools.lock": "Trancar Tópico", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Este tópico será movido para a categoria", "fork_topic_instruction": "Clique nos posts que você quer ramificar", "fork_no_pids": "Nenhum post selecionado!", + "fork_pid_count": "%1 post(s) selecionado(s)", "fork_success": "Tópico ramificado com sucesso! Clique aqui para ir ao tópico ramificado.", "delete_posts_instruction": "Clique nos posts que você deseja deletar/limpar", "composer.title_placeholder": "Digite aqui o título para o seu tópico...", diff --git a/public/language/pt_BR/unread.json b/public/language/pt_BR/unread.json index bba5086d93..2ee62fbb68 100644 --- a/public/language/pt_BR/unread.json +++ b/public/language/pt_BR/unread.json @@ -6,5 +6,8 @@ "selected": "Selecionado", "all": "Todos", "all_categories": "Todas as categorias", - "topics_marked_as_read.success": "Tópicos marcados como lidos!" + "topics_marked_as_read.success": "Tópicos marcados como lidos!", + "all-topics": "Todos os Tópicos", + "new-topics": "Novos Tópicos", + "watched-topics": "Topicos Acompanhados" } \ No newline at end of file diff --git a/public/language/pt_BR/user.json b/public/language/pt_BR/user.json index 21df201b80..f7e79b1f13 100644 --- a/public/language/pt_BR/user.json +++ b/public/language/pt_BR/user.json @@ -6,6 +6,7 @@ "postcount": "Número de Posts", "email": "Email", "confirm_email": "Confirmar Email", + "account_info": "Informação da Conta", "ban_account": "Banir Conta", "ban_account_confirm": "Você realmente quer banir esse usuario?", "unban_account": "Desbanir Conta", @@ -88,14 +89,21 @@ "topics_per_page": "Tópicos por Página", "posts_per_page": "Posts por Página", "notification_sounds": "Tocar um som quando você receber uma notificação.", + "notifications_and_sounds": "Notificações & Sons", + "incoming-message-sound": "Som de recebimento de mensagem", + "outgoing-message-sound": "Som de envio de mensagem", + "notification-sound": "Som de notificação", + "no-sound": "Sem som", "browsing": "Configurações de Navegação", "open_links_in_new_tab": "Abrir links externos em nova aba", "enable_topic_searching": "Habilitar Pesquisa dentro de Tópico", "topic_search_help": "Se habilitado, a pesquisa dentro do tópico irá substituir a pesquisa padrão do seu navegador. Assim, você poderá pesquisar pelo tópico inteiro, e não apenas pelo o que está sendo exibido na tela.", + "delay_image_loading": "Aguardar para Carregar Imagens", + "image_load_delay_help": "Se habilitado, imagens em tópicos não serão carregadas até que eles sejam rolados à visão", "scroll_to_my_post": "Após postar uma réplica, mostre o novo post", - "follow_topics_you_reply_to": "Seguir tópicos que você responde", - "follow_topics_you_create": "Seguir tópicos que você cria", - "grouptitle": "Escolha o título do grupo que você deseja exibir", + "follow_topics_you_reply_to": "Assistir os tópicos que você responde", + "follow_topics_you_create": "Assistir aos tópicos que você cria", + "grouptitle": "Título do Grupo", "no-group-title": "Sem título de grupo", "select-skin": "Escolha uma Skin", "select-homepage": "Selecione uma página inicial", @@ -105,5 +113,17 @@ "custom_route_help": "Entre com o nome da rota aqui, sem nenhuma barra anterior (por exemplo, \"recente\", ou \"popular\")", "sso.title": "Logar por outros Serviços", "sso.associated": "Associado com", - "sso.not-associated": "Clique aqui para associar com" + "sso.not-associated": "Clique aqui para associar com", + "info.latest-flags": "Últimas Sinalizações", + "info.no-flags": "Nenhum Post Sinalizado Encontrado", + "info.ban-history": "Histórico de Banimentos Recentes", + "info.no-ban-history": "Este usuário nunca foi banido", + "info.banned-until": "Banido até %1", + "info.banned-permanently": "Banido permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Sem motivo escolhido.", + "info.username-history": "Histórico do Nome de Usuário", + "info.email-history": "Histórico do Email", + "info.moderation-note": "Nota da Moderação", + "info.moderation-note.success": "Nota da moderação salva" } \ No newline at end of file diff --git a/public/language/pt_BR/users.json b/public/language/pt_BR/users.json index 0cd0b190d5..62aa2b060e 100644 --- a/public/language/pt_BR/users.json +++ b/public/language/pt_BR/users.json @@ -2,6 +2,7 @@ "latest_users": "Últimos Usuários", "top_posters": "Principais Participantes", "most_reputation": "Maior Reputação", + "most_flags": "Mais Sinalizações", "search": "Pesquisar", "enter_username": "Digite um nome de usuário para pesquisar", "load_more": "Carregar Mais", diff --git a/public/language/ro/category.json b/public/language/ro/category.json index e525ac8878..a090e3ac07 100644 --- a/public/language/ro/category.json +++ b/public/language/ro/category.json @@ -1,16 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", + "category": "Categorie", + "subcategories": "Subcategorii", "new_topic_button": "Subiect Nou", - "guest-login-post": "Log in to post", + "guest-login-post": "Conecteaza-te pentru a posta", "no_topics": "<strong>Nu există nici un subiect de discuție în această categorie.</strong><br />De ce nu încerci să postezi tu unul?", "browsing": "navighează", "no_replies": "Nu a răspuns nimeni", - "no_new_posts": "No new posts.", + "no_new_posts": "Nici o postare nouă", "share_this_category": "Distribuie această categorie", - "watch": "Watch", + "watch": "Urmărește", "ignore": "Ignoră", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", - "watched-categories": "Watched categories" + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Categorii urmărite" } \ No newline at end of file diff --git a/public/language/ro/email.json b/public/language/ro/email.json index 5b802555de..e07033fd97 100644 --- a/public/language/ro/email.json +++ b/public/language/ro/email.json @@ -1,19 +1,19 @@ { "password-reset-requested": "Cererea de resetare a parolei a fost efectuată - %1!", "welcome-to": "Salutare lui %1", - "invite": "Invitation from %1", + "invite": "Invitație de la %1", "greeting_no_name": "Salut", "greeting_with_name": "Salut %1", "welcome.text1": "îți mulțumim că te-ai Înregistrat cu %1!", "welcome.text2": "Pentru a-ți activa cu success contul trebuie să verificăm adresa de email pe care ai folosit-o la înregistrare.", "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.cta": "Apasă aici pentru a confirma adresa ta de email", - "invitation.text1": "%1 has invited you to join %2", + "invitation.text1": "%1 te-a invitat să te alături %2", "invitation.ctr": "Click here to create your account.", "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", "reset.text2": "Pentru a continua cu resetarea parolei, te rugăm sa apeși pe următorul link:", "reset.cta": "Apasă aici pentru a-ți reseta parola", - "reset.notify.subject": "Password successfully changed", + "reset.notify.subject": "Parola a fost schimbată cu succes", "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.", "digest.notifications": "You have unread notifications from %1:", @@ -21,9 +21,10 @@ "digest.cta": "Apasă aici pentru a vizita %1", "digest.unsub.info": "This digest was sent to you due to your subscription settings.", "digest.no_topics": "There have been no active topics in the past %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "zi", + "digest.week": "saptămână", + "digest.month": "lună", + "digest.subject": "Rezumat pentru %1", "notif.chat.subject": "Ai primit un mesaj de la %1", "notif.chat.cta": "Apasă aici pentru a continua conversația", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index 56750a42a0..96802562e2 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -14,20 +14,25 @@ "invalid-password": "Parolă Invalidă", "invalid-username-or-password": "Te rugăm să specifici atât un nume de utilizator cât si o parolă", "invalid-search-term": "Cuvânt de căutare invalid", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Numele de utilizator este deja folosit", "email-taken": "Adresa de email este deja folostă", "email-not-confirmed": "Adresa ta de email nu a fost inca confirmata, click aici ca sa o confirmi.", "email-not-confirmed-chat": "Nu vei putea trimite mesaje daca email-ul tau nu e confirmat, click aici sa il confirmi.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Ca sa accesezi forumul trebuie sa iti confirmi email-ul, click aici ca sa intri in mail.", "email-confirm-failed": "Mail-ul tau nu a putut fi confirmat, te rog incearca mai tarziu.", "confirm-email-already-sent": "Email-ul de confirmare ti-a fost trimis, asteapta te rog %1 minut(e) ca sa trimiti inca unul.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Numele de utilizator este prea scurt", "username-too-long": "Numele de utilizator este prea lung", "password-too-long": "Parola prea lunga.", "user-banned": "Utilizator banat", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Imi pare rau dar trebuie sa astepti %1 secunda(e) pentru a posta prima oara.", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Categoria nu exista.", "no-topic": "Topicul nu exista.", "no-post": "Post-ul nu exista.", @@ -38,10 +43,26 @@ "category-disabled": "Categorie dezactivată", "topic-locked": "Subiect Închis", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Nu poți bana alți administratori!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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": "Numele grupului este prea scurt", + "group-name-too-long": "Group name too long", "group-already-exists": "Grupul deja există", "group-name-change-not-allowed": "Schimbarea numelui grupului este interzisă", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistemul de reputație este dezactivat.", "downvoting-disabled": "Votarea negativă este dezactivată", "not-enough-reputation-to-downvote": "Nu ai destulă reputație pentru a vota negativ acest post.", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/ro/global.json b/public/language/ro/global.json index 501638ec86..8f39ff6550 100644 --- a/public/language/ro/global.json +++ b/public/language/ro/global.json @@ -3,12 +3,14 @@ "search": "Căutare", "buttons.close": "Închide", "403.title": "Acces Interzis", - "403.message": "You seem to have stumbled upon a page that you do not have access to.", - "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", + "403.message": "Se pare că ai ajuns pe o pagină la care nu ai acces", + "403.login": "Poate ar trebui să te <a href='%1/login'>autentifici</a>?", "404.title": "Nu a fost găsit", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Eroare internă.", + "500.title": "Internal Error.", "500.message": "Oops! Se pare că ceva a mers greșit!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Înregistrare", "login": "Autentificare", "please_log_in": "Autentifică-te", @@ -17,6 +19,7 @@ "welcome_back": "Bine ai revenit", "you_have_successfully_logged_in": "Te-ai conectat cu succes", "save_changes": "Salvează Modificări", + "save": "Save", "close": "Închide", "pagination": "Paginație", "pagination.out_of": "%1 din %2", @@ -49,8 +52,10 @@ "users": "Utilizatori", "topics": "Subiecte", "posts": "Mesaje", - "best": "Best", + "best": "Cel mai bun", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Vizualizări", "reputation": "Reputație", @@ -65,7 +70,7 @@ "posted_in_ago_by": "postat în %1 %2 de %3", "user_posted_ago": "%1 a postat %2", "guest_posted_ago": "Vizitator a postat %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "ultima editare de %1", "norecentposts": "Nici un mesaj recent", "norecenttopics": "Nici un subiect recent", "recentposts": "Mesaje Recente", @@ -85,10 +90,13 @@ "unfollow": "Nu mai urmări", "delete_all": "Şterge Tot", "map": "Hartă", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "sessions": "Ședința de login", + "ip_address": "Adresa IP", + "enter_page_number": "Introdu numărul paginei", + "upload_file": "Încărcați fișierul", + "upload": "Încărcați", + "allowed-file-types": "Tipuri de fișiere permise sunt %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ro/groups.json b/public/language/ro/groups.json index 60719c2bf9..6bc6baa2c9 100644 --- a/public/language/ro/groups.json +++ b/public/language/ro/groups.json @@ -1,54 +1,56 @@ { "groups": "Grupuri", "view_group": "Vezi Grup", - "owner": "Group Owner", - "new_group": "Create New Group", - "no_groups_found": "There are no groups to see", - "pending.accept": "Accept", - "pending.reject": "Reject", - "pending.accept_all": "Accept All", - "pending.reject_all": "Reject All", - "pending.none": "There are no pending members at this time", - "invited.none": "There are no invited members at this time", - "invited.uninvite": "Rescind Invitation", + "owner": "Propietar de group", + "new_group": "Crează un grup nou", + "no_groups_found": "Nu sunt grupuri de văzut", + "pending.accept": "Acceptă", + "pending.reject": "Respinge", + "pending.accept_all": "Acceptă toate", + "pending.reject_all": "Respinge toate", + "pending.none": "Momentan nu există membrii în așteptare", + "invited.none": "Momentan nu există membrii invitați", + "invited.uninvite": "Anulează invitația", "invited.search": "Search for a user to invite to this group", - "invited.notification_title": "You have been invited to join <strong>%1</strong>", + "invited.notification_title": "Ai fost invitat să te alături <strong>%1<strong>", "request.notification_title": "Group Membership Request from <strong>%1</strong>", "request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>", - "cover-save": "Save", - "cover-saving": "Saving", + "cover-save": "Salvează", + "cover-saving": "Salvez", "details.title": "Detalii Grup", "details.members": "Listă Membrii", - "details.pending": "Pending Members", - "details.invited": "Invited Members", + "details.pending": "Membrii în așteptare", + "details.invited": "Membrii invitați", "details.has_no_posts": "Membrii acestui grup nu au facut nici o postare.", "details.latest_posts": "Ultimele Mesaje", - "details.private": "Private", + "details.private": "Privat", "details.disableJoinRequests": "Disable join requests", "details.grant": "Grant/Rescind Ownership", "details.kick": "Kick", - "details.owner_options": "Group Administration", - "details.group_name": "Group Name", - "details.member_count": "Member Count", - "details.creation_date": "Creation Date", - "details.description": "Description", - "details.badge_preview": "Badge Preview", - "details.change_icon": "Change Icon", - "details.change_colour": "Change Colour", - "details.badge_text": "Badge Text", - "details.userTitleEnabled": "Show Badge", + "details.owner_options": "Administrarea grupului", + "details.group_name": "Numele grupului", + "details.member_count": "Număr de membrii", + "details.creation_date": "Data creării", + "details.description": "Descriere", + "details.badge_preview": "Previzualizarea insignei", + "details.change_icon": "Schimbă icoana", + "details.change_colour": "Schimbă culoarea", + "details.badge_text": "Textul insignei", + "details.userTitleEnabled": "Arată insigna", "details.private_help": "If enabled, joining of groups requires approval from a group owner", - "details.hidden": "Hidden", + "details.hidden": "Ascuns", "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", - "details.delete_group": "Delete Group", + "details.delete_group": "Șterge grupul", "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", - "event.updated": "Group details have been updated", - "event.deleted": "The group \"%1\" has been deleted", - "membership.accept-invitation": "Accept Invitation", - "membership.invitation-pending": "Invitation Pending", - "membership.join-group": "Join Group", - "membership.leave-group": "Leave Group", - "membership.reject": "Reject", - "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "event.updated": "Detaliile grupului au fost actualizate", + "event.deleted": "Grupul %1\" a fost șters", + "membership.accept-invitation": "Acceptă invitația", + "membership.invitation-pending": "Invitație in așteptare", + "membership.join-group": "Alăture-te grupului", + "membership.leave-group": "Părăsește grupul", + "membership.reject": "Respinge", + "new-group.group_name": "Numele grupului:", + "upload-group-cover": "Încarcă coperta de grup", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/ro/login.json b/public/language/ro/login.json index 479e3a83be..1f3e72b093 100644 --- a/public/language/ro/login.json +++ b/public/language/ro/login.json @@ -5,7 +5,8 @@ "remember_me": "Autentifică-mă automat la fiecare vizită", "forgot_password": "Ai uitat parola?", "alternative_logins": "Autentificare Alternativă", - "failed_login_attempt": "Încercare de autentificare eșuată, te rugăm să încerci dinou.", + "failed_login_attempt": "Login nereușit", "login_successful": "Te-ai autentificat cu succes!", - "dont_have_account": "Nu ai un cont?" + "dont_have_account": "Nu ai un cont?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json index f1f0bdf9d6..eda44c3a00 100644 --- a/public/language/ro/modules.json +++ b/public/language/ro/modules.json @@ -5,7 +5,7 @@ "chat.no_active": "Nu ai nici o conversație activă", "chat.user_typing": "%1 scrie ...", "chat.user_has_messaged_you": "%1 ți-a trimis un mesaj.", - "chat.see_all": "See all chats", + "chat.see_all": "Vezi toate conversațiile", "chat.mark_all_read": "Mark all chats read", "chat.no-messages": "Selectează un recipient pentru a vedea istoria mesajelor chat", "chat.no-users-in-room": "No users in this room", @@ -20,7 +20,7 @@ "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "chat.roomname": "Chat Room %1", "chat.add-users-to-room": "Add users to room", - "composer.compose": "Compose", + "composer.compose": "Scrie", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", "composer.user_said_in": "%1 a spus în %2:", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json index 9684a7d9cc..ea8a0d2b06 100644 --- a/public/language/ro/notifications.json +++ b/public/language/ro/notifications.json @@ -1,11 +1,11 @@ { "title": "Notificări", "no_notifs": "Nu ai nici o notificare recentă", - "see_all": "See all notifications", - "mark_all_read": "Mark all notifications read", + "see_all": "Vezi toate notificările", + "mark_all_read": "Marchează toate notificările citite", "back_to_home": "Înapoi la %1", "outgoing_link": "Link Extern", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "Părăsești acuma %1", "continue_to": "Continuă la %1", "return_to": "Întoarce-te la %1", "new_notification": "Notificare Nouă", @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> a semnalizat un mesaj în <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/ro/pages.json b/public/language/ro/pages.json index b90accee42..77930e3386 100644 --- a/public/language/ro/pages.json +++ b/public/language/ro/pages.json @@ -1,24 +1,26 @@ { "home": "Acasă", "unread": "Subiecte Necitite", - "popular-day": "Popular topics today", - "popular-week": "Popular topics this week", - "popular-month": "Popular topics this month", + "popular-day": "Subiecte populare azi", + "popular-week": "Subiecte populare în săptămâna asta", + "popular-month": "Subiecte populare în luna asta", "popular-alltime": "All time popular topics", "recent": "Subiecte Noi", "flagged-posts": "Flagged Posts", - "users/online": "Online Users", - "users/latest": "Latest Users", - "users/sort-posts": "Users with the most posts", + "users/online": "Utilizatori online", + "users/latest": "Ultimii membrii", + "users/sort-posts": "Membrii cu cele mai multe postări", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notificări", "tags": "Taguri", "tag": "Topics tagged under \"%1\"", - "register": "Register an account", + "register": "Înregistrează un cont nou", + "registration-complete": "Registration complete", "login": "Login to your account", - "reset": "Reset your account password", + "reset": "Resetează parola contului tău", "categories": "Categorii", "groups": "Grupuri", "group": "%1 group", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/ro/recent.json b/public/language/ro/recent.json index 5b657f39b1..45d788dfc3 100644 --- a/public/language/ro/recent.json +++ b/public/language/ro/recent.json @@ -6,14 +6,14 @@ "year": "An", "alltime": "Tot Timpul", "no_recent_topics": "Nu există subiecte recente.", - "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.", + "no_popular_topics": "Nu sunt subiecte populare.", + "there-is-a-new-topic": "Există un subiect nou.", + "there-is-a-new-topic-and-a-new-post": "Există un subiect nou si o postare nouă.", "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.", + "there-are-new-topics": "Există %1 postări noi.", + "there-are-new-topics-and-a-new-post": "Exista %1 subiect nou și o postare nouă", + "there-are-new-topics-and-new-posts": "Exista %1 subiecte noi și %2 postări noi", + "there-is-a-new-post": "Exista o postare nouă", + "there-are-new-posts": "Există %1 postări noi", "click-here-to-reload": "Click here to reload." } \ No newline at end of file diff --git a/public/language/ro/register.json b/public/language/ro/register.json index 99b423716d..60ad1513bf 100644 --- a/public/language/ro/register.json +++ b/public/language/ro/register.json @@ -1,5 +1,6 @@ { "register": "Înregistrare", + "cancel_registration": "Cancel Registration", "help.email": "Implicit, adresa ta de email va fi ascunsă.", "help.username_restrictions": "Un nume de utilizator între %1 și %2 caractere. Alți utilizatori te pot menționa cu @<span id='yourUsername'>utilizator</span>.", "help.minimum_password_length": "Lungimea parolei trebuie sa fie mai mare de %1 caractere.", @@ -15,5 +16,8 @@ "alternative_registration": "Înregistrare Alternativă", "terms_of_use": "Termeni de utilizare", "agree_to_terms_of_use": "Sunt de acord cu termenii de utilizare", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/ro/reset_password.json b/public/language/ro/reset_password.json index 15800e2b55..02afb16f0c 100644 --- a/public/language/ro/reset_password.json +++ b/public/language/ro/reset_password.json @@ -13,5 +13,5 @@ "invalid_email": "Adresă de email invalidă / Adresa de email nu există!", "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_expired": "Your password has expired, please choose a new password" + "password_expired": "Parola ta a expirat, te rugăm alege altă parolă" } \ No newline at end of file diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index 5503fa8c3d..e57199a2de 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -5,7 +5,7 @@ "no_topics_found": "Nu a fost găsit nici un subiect!", "no_posts_found": "Nu a fost găsit nici un mesaj!", "post_is_deleted": "Acest mesaj a fost șters!", - "topic_is_deleted": "This topic is deleted!", + "topic_is_deleted": "Acest subiect este șters!", "profile": "Profil", "posted_by": "Postat de %1", "posted_by_guest": "Postat de Vizitator", @@ -13,8 +13,8 @@ "notify_me": "Notică-mă de noi răspunsuri în acest subiect", "quote": "Citează", "reply": "Răspunde", - "reply-as-topic": "Reply as topic", - "guest-login-reply": "Log in to reply", + "reply-as-topic": "Răspunde ca subiect", + "guest-login-reply": "Login pentru a răspunde", "edit": "Editează", "delete": "Șterge", "purge": "Curăță", @@ -26,12 +26,30 @@ "tools": "Unelte", "flag": "Semnalizează", "locked": "Închis", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Semnalizează acest mesaj pentru moderare", "flag_success": "Acest mesaj a fost semnalizat pentru moderare.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Acest subiect a fost șters. Doar utilizatorii cu privilegii pentru moderarea subiectelor îl poate vedea.", "following_topic.message": "Vei primi notificări când cineva va posta un nou mesaj in acest subiect.", - "not_following_topic.message": "Nu vei mai primi notificări legate de acest subiect.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Te rugăm să te înregistrezi sau să te autentifici ca să te poți abona la acest subiect.", "markAsUnreadForAll.success": "Subiect marcat ca citit pentru toți.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Abonează-te la notificări legate de acest subiect", "unwatch.title": "Oprește urmărirea acestui subiect", "share_this_post": "Distribuie acest mesaj", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Unelte pentru subiecte", - "thread_tools.markAsUnreadForAll": "Marchează Necitit", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin Subiect", "thread_tools.unpin": "Unpin Subiect", "thread_tools.lock": "Închide Subiect", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Acest subiect va fi mutat în categoria", "fork_topic_instruction": "Apasă pe mesajele care vrei sa le bifurci", "fork_no_pids": "Nu a fost selectat nici un mesaj!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Introdu numele subiectului aici ...", diff --git a/public/language/ro/unread.json b/public/language/ro/unread.json index 5cff8c6546..a5b6c46e7e 100644 --- a/public/language/ro/unread.json +++ b/public/language/ro/unread.json @@ -5,6 +5,9 @@ "mark_as_read": "Marchează ca citit", "selected": "Selectate", "all": "Toate", - "all_categories": "All categories", - "topics_marked_as_read.success": "Subiectele au fost marcate ca citite!" + "all_categories": "Toate categoriile", + "topics_marked_as_read.success": "Subiectele au fost marcate ca citite!", + "all-topics": "Toate subiectele", + "new-topics": "Subiecte noi", + "watched-topics": "Subiecte urmărite" } \ No newline at end of file diff --git a/public/language/ro/uploads.json b/public/language/ro/uploads.json index 1622cb5693..aa633f773e 100644 --- a/public/language/ro/uploads.json +++ b/public/language/ro/uploads.json @@ -1,6 +1,6 @@ { "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", + "select-file-to-upload": "Selectează un fișier pentru încărcare!", "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "maximum-file-size": "Maxim %1 kB" } \ No newline at end of file diff --git a/public/language/ro/user.json b/public/language/ro/user.json index 8a61af9abf..d86c8ab301 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -6,13 +6,14 @@ "postcount": "Post Count", "email": "Adresă Email", "confirm_email": "Confirmă Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", "delete_account": "Șterge Cont", "delete_account_confirm": "Ești sigur ca vrei să îți ștergi contul? <br /><strong>Această acțiune este ireversibilă și nu o să mai fie posibil să îți recuperezi datele</strong><br /><br />Introdu numele tău de utilizator pentru a confirma că dorești să ștergi acest cont.", "delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", - "account-deleted": "Account deleted", + "account-deleted": "Cont șters", "fullname": "Nume Întreg", "website": "Pagină Web", "location": "Locație", @@ -88,14 +89,21 @@ "topics_per_page": "Subiecte pe pagină", "posts_per_page": "Mesaje pe pagină", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Setări navigare", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/ro/users.json b/public/language/ro/users.json index 8f2cf6914c..d3a4f09a9e 100644 --- a/public/language/ro/users.json +++ b/public/language/ro/users.json @@ -2,12 +2,13 @@ "latest_users": "Ultimii Utilizatori", "top_posters": "Top Utilizatori", "most_reputation": "Cei mai apreciați utilizatori", + "most_flags": "Most Flags", "search": "Căutare", "enter_username": "Introdu un nume de utilizator pentru a căuta", "load_more": "Încarcă mai multe", "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", "filter-by": "Filter By", - "online-only": "Online only", + "online-only": "Numai online", "invite": "Invită", "invitation-email-sent": "An invitation email has been sent to %1", "user_list": "Listă utilizatori", diff --git a/public/language/ru/category.json b/public/language/ru/category.json index 347af2bd0e..341b57f917 100644 --- a/public/language/ru/category.json +++ b/public/language/ru/category.json @@ -2,7 +2,7 @@ "category": "Категория", "subcategories": "Подкатегории", "new_topic_button": "Создать тему", - "guest-login-post": "Войдите, чтобы создавать сообщения", + "guest-login-post": "Войдите, чтобы ответить в теме", "no_topics": "<strong>В этой категории еще нет тем.</strong><br />Почему бы вам не создать первую?", "browsing": "просматривают", "no_replies": "Нет ответов", @@ -10,7 +10,11 @@ "share_this_category": "Поделиться этой категорией", "watch": "Следить", "ignore": "Игнорировать", - "watch.message": "Вы теперь следите за обновлениями из этой категории", - "ignore.message": "Вы теперь игнорируете обновления из этой категории", + "watching": "Отслеживать", + "ignoring": "Игнорировать", + "watching.description": "Показывать тему в непрочитанных", + "ignoring.description": "Не показывать тему в непрочитанных", + "watch.message": "Вы теперь следите за обновлениями из этой категории и всех подкатегорий", + "ignore.message": "Вы теперь игнорируете обновления из этой категории и всех подкатегорий", "watched-categories": "Наблюдаемые категории" } \ No newline at end of file diff --git a/public/language/ru/email.json b/public/language/ru/email.json index a4b378b897..f76b43d120 100644 --- a/public/language/ru/email.json +++ b/public/language/ru/email.json @@ -24,6 +24,7 @@ "digest.day": "день", "digest.week": "неделя", "digest.month": "месяц", + "digest.subject": "Дайджест для %1", "notif.chat.subject": "Новое сообщение от %1", "notif.chat.cta": "Нажмите для продолжения диалога", "notif.chat.unsub.info": "Вы получили это уведомление в соответствии с настройками подписок.", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index efaab28fa9..a5034824f0 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -14,20 +14,25 @@ "invalid-password": "Неверный пароль", "invalid-username-or-password": "Пожалуйста, укажите и имя пользователя и пароль", "invalid-search-term": "Неверный поисковой запрос", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "Нам не удалось Вас найти из-за просроченной сессии. Попробуйте еще раз.", + "invalid-pagination-value": "Неверное значение страницы. Значение должно быть в диапазоне от %1 до %2", "username-taken": "Имя пользователя занято", "email-taken": "Email занят", "email-not-confirmed": "Ваш email не подтвержден, нажмите для подтверждения.", "email-not-confirmed-chat": "Вы не можете оставлять сообщения, пока Ваш email не подтверждён. Нажмите на это сообщение чтобы получить письмо повторно.", + "email-not-confirmed-email-sent": "Ваш адрес электронной почты еще не был подтвержден, пожалуйста, проверьте Ваш почтовый ящик для подтверждения регистрации.", "no-email-to-confirm": "Этот форум требует подтверждения по E-mail. Нажмите здесь для ввода E-mail.", "email-confirm-failed": "Мы не можем подтвердить Ваш E-mail, попробуйте позже.", "confirm-email-already-sent": "Сообщение для подтверждения уже выслано на E-mail. Повторная отправка возможна через %1 мин.", + "sendmail-not-found": "Не можем найти sendmail, убедитесь что он установлен и управляется NodeBB.", "username-too-short": "Слишком короткое имя пользователя", "username-too-long": "Имя пользователя слишком длинное", "password-too-long": "Пароль слишком длинный", "user-banned": "Пользователь заблокирован", + "user-banned-reason": "Пользователь заблокирован (Причина: %1)", "user-too-new": "Вы можете написать свое первой сообщение через %1 сек.", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Извините, ваш IP адрес был забанен этим сообществом. Если вы считаете что это ошибка, пожалуйста, свяжитесь с администратором.", + "ban-expiry-missing": "Пожалуйста, укажите дату окончания этой блокировки", "no-category": "Категория не существует", "no-topic": "Тема не существует", "no-post": "Сообщение не существует", @@ -38,10 +43,26 @@ "category-disabled": "Категория отключена", "topic-locked": "Тема закрыта", "post-edit-duration-expired": "Сообщения можно редактировать только в течение %1 секунд(ы) после опубликования", + "post-edit-duration-expired-minutes": "Вы можете редактировать посты только в течение %1 минут(ы) после публикации.", + "post-edit-duration-expired-minutes-seconds": "Вы можете редактировать сообщение в течение %1 минут(ы) %2 секунд(ы) после публикации.", + "post-edit-duration-expired-hours": "Вы можете редактировать сообщения в течение %1 час(а) после публикации.", + "post-edit-duration-expired-hours-minutes": "Вы можете редактировать сообщения в течение %1 час(а) %2 минут(ы) после публикации.", + "post-edit-duration-expired-days": "Вы можете редактировать сообщения в течение %1 дня(ей) после публикации.", + "post-edit-duration-expired-days-hours": "Вы можете редактировать сообщения в течение %1 чдня(ей) и %2 час(а) после публикации.", + "post-delete-duration-expired": "Сообщения можно удалять только через %1 секунд(ы) после публикации", + "post-delete-duration-expired-minutes": "Сообщения можно удалять только через %1 минут(ы) после публикации", + "post-delete-duration-expired-minutes-seconds": "Сообщения можно удалять только через %1 минут(ы) 2% секунд(ы) после публикации", + "post-delete-duration-expired-hours": "Сообщения можно удалять только через %1 час(ы) после публикации", + "post-delete-duration-expired-hours-minutes": "Сообщения можно удалять только через %1 час(ы) 2% минут(ы) после публикации", + "post-delete-duration-expired-days": "Сообщения можно удалять только через %1 день(ы) после публикации", + "post-delete-duration-expired-days-hours": "Сообщения можно удалять только через %1 день(ы) 2% час(ы) после публикации", + "cant-delete-topic-has-reply": "Вы не можете удалить свою тему после того как в ней появились ответы", + "cant-delete-topic-has-replies": "Вы не можете удалить свою тему после того как в ней появилось %1 ответов", "content-too-short": "Слишком короткое сообщение. Минимум символов: %1.", "content-too-long": "Слишком длинное сообщение. Максимум символов: %1.", "title-too-short": "Слишком короткое сообщение. Минимум символов: %1.", "title-too-long": "Слишком длинный заголовок. Максимум символов: %1.", + "category-not-selected": "Категория не выбрана", "too-many-posts": "Вы можете создавать сообщение только один раз в %1 сек.", "too-many-posts-newbie": "Вы новый пользователь, поэтому можете создавать сообщение раз в %1 сек., пока не заработаете %2 п. репутации.", "tag-too-short": "Слишком короткий тэг. Минимум символов: %1.", @@ -52,13 +73,15 @@ "file-too-big": "Слишком большой файл. Максимальный размер: %1 Кбайт.", "guest-upload-disabled": "Загрузка для гостей была отключена", "already-favourited": "Вы уже добавили это сообщение в закладки", - "already-unfavourited": "You have already unbookmarked this post", + "already-unfavourited": "Вы уже убрали из закладок этот пост", "cant-ban-other-admins": "Вы не можете забанить других администраторов!", "cant-remove-last-admin": "Вы единственный администратор. Назначьте другого пользователя администратором, прежде чем складывать с себя полномочия админа", + "cant-delete-admin": "Удалить права администратора с этой учетной записи, прежде чем пытаться удалить её.", "invalid-image-type": "Неверный формат изображения. Поддерживаемые форматы: %1", "invalid-image-extension": "Недопустимое расширение файла", "invalid-file-type": "Неверный формат файла. Поддерживаемые форматы: %1", "group-name-too-short": "Название группы слишком короткое", + "group-name-too-long": "Название группы слишком длинное", "group-already-exists": "Группа уже существует", "group-name-change-not-allowed": "Изменение названия группы запрещено", "group-already-member": "Уже в этой группе", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "У вас нет доступа для редактирования этого сообщения", "cant-remove-last-user": "Вы не можете убрать последнего пользователя", "cant-delete-chat-message": "У вас нет доступа на удаление этого сообщения", + "already-voting-for-this-post": "Вы уже голосовали за это сообщение.", "reputation-system-disabled": "Система репутации отключена.", "downvoting-disabled": "Понижение оценки отключено", "not-enough-reputation-to-downvote": "У Вас недостаточно репутации для понижения оценки сообщения", @@ -99,5 +123,9 @@ "no-session-found": "Сессия входа не найдена!", "not-in-room": "Пользователь не в комнате", "no-users-in-room": "В этой комнате нет пользователей", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "Вы не можете удалить себя сами из группы.", + "no-users-selected": "Пользователь(и) не выбраны", + "invalid-home-page-route": "Неверный маршрут домашней страницы", + "invalid-session": "Несуществующая Сессия", + "invalid-session-text": "Похоже, что ваша сессия больше не активна, или не совпадает с сервером. Пожалуйста, обновите эту страницу." } \ No newline at end of file diff --git a/public/language/ru/global.json b/public/language/ru/global.json index 3c6a09c0b5..e0a280d999 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -9,6 +9,8 @@ "404.message": "Вы пытаетесь перейти на страницу, которой не существует. Вам стоит вернутся на <a href='%1/'>главную страницу</a>.", "500.title": "Внутренняя ошибка.", "500.message": "Упс! Похоже, что-то пошло не так!", + "400.title": "Неверный Запрос.", + "400.message": "Похоже, эта ссылка имеет неправильный формат, пожалуйста, проверьте и повторите попытку. Либо вернитесь на <а href='%1/'>главную страницу</a>.", "register": "Зарегистрироваться", "login": "Войти", "please_log_in": "Пожалуйста, войдите под своим аккаунтом", @@ -17,6 +19,7 @@ "welcome_back": "С возвращением", "you_have_successfully_logged_in": "Вы успешно вошли на форум", "save_changes": "Сохранить изменения", + "save": "Сохранить", "close": "Закрыть", "pagination": "Пагинация", "pagination.out_of": "%1 из %2", @@ -50,8 +53,10 @@ "topics": "Темы", "posts": "Сообщения", "best": "Лучшие", - "upvoted": "Upvoted", - "downvoted": "Downvoted", + "upvoters": "Кто лайкнул", + "upvoted": "Лайки", + "downvoters": "Кто дизлайкнул", + "downvoted": "Дизлайки", "views": "Просмотры", "reputation": "Репутация", "read_more": "читать дальше", @@ -60,12 +65,12 @@ "posted_ago_by": "сообщений %1 от %2", "posted_ago": "написал %1", "posted_in": "написал в %1", - "posted_in_by": "posted in %1 by %2", + "posted_in_by": "опубликовано в %1 %2", "posted_in_ago": "написал в %1 %2", "posted_in_ago_by": "%3 написал в %1 %2", "user_posted_ago": "%1 написал %2", "guest_posted_ago": "Гость написал %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "отредактировано %1", "norecentposts": "Нет новых сообщений", "norecenttopics": "Нету новых тем", "recentposts": "Свежие записи", @@ -90,5 +95,8 @@ "enter_page_number": "Введите номер страницы", "upload_file": "Загрузить файл", "upload": "Загрузить", - "allowed-file-types": "Разрешенные форматы файлов %1" + "allowed-file-types": "Разрешенные форматы файлов %1", + "unsaved-changes": "У вас есть несохраненные изменения. Вы уверены, что хотите уйти?", + "reconnecting-message": "Похоже, подключение к %1 было разорвано, подождите, пока мы пытаемся восстановить соединение.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/ru/groups.json b/public/language/ru/groups.json index eeeca0e8fb..c33cfb3d4c 100644 --- a/public/language/ru/groups.json +++ b/public/language/ru/groups.json @@ -41,7 +41,7 @@ "details.hidden": "Скрыто", "details.hidden_help": "Если включено, группа не будет показываться в списках, а пользователи должны приглашаться вручную", "details.delete_group": "Удалить группу", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Частные группы отключены на уровне системы, эта опция ничего не даст", "event.updated": "Настройки группы обновлены", "event.deleted": "Группа \"%1\" удалена", "membership.accept-invitation": "Принять приглашение", @@ -50,5 +50,7 @@ "membership.leave-group": "Покинуть", "membership.reject": "Отклонить", "new-group.group_name": "Название группы:", - "upload-group-cover": "Загрузить обложку группы" + "upload-group-cover": "Загрузить обложку группы", + "bulk-invite-instructions": "Введите через запятую имена пользователей, которых хотите пригласить в эту группу", + "bulk-invite": "Массовое приглашение" } \ No newline at end of file diff --git a/public/language/ru/login.json b/public/language/ru/login.json index b0a5793412..133dc011c1 100644 --- a/public/language/ru/login.json +++ b/public/language/ru/login.json @@ -5,7 +5,8 @@ "remember_me": "Запомнить меня", "forgot_password": "Забыли пароль?", "alternative_logins": "Войти через", - "failed_login_attempt": "Не удалось войти, попробуйте еще раз.", + "failed_login_attempt": "Некорректный логин", "login_successful": "Вы успешно вошли!", - "dont_have_account": "Нет акканута?" + "dont_have_account": "Нет акканута?", + "logged-out-due-to-inactivity": "Вы вышли из панели управления администратора из-за бездействия" } \ No newline at end of file diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index 07ecb488c8..1fe3ce4526 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -6,7 +6,7 @@ "chat.user_typing": "%1 печатает ...", "chat.user_has_messaged_you": "%1 отправил вам сообщение.", "chat.see_all": "Посмотреть все чаты", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Отметить все чаты как прочитанные", "chat.no-messages": "Пожалуйста, выберите собеседника для просмотра истории сообщений", "chat.no-users-in-room": "В этой комнате нет пользователей", "chat.recent-chats": "Последние переписки", @@ -23,12 +23,22 @@ "composer.compose": "Редактор", "composer.show_preview": "Показать предпросмотр", "composer.hide_preview": "Скрыть предпросмотр", - "composer.user_said_in": "%1 сказал %2:", - "composer.user_said": "%1 сказал:", + "composer.user_said_in": "%1 написал в %2:", + "composer.user_said": "%1 написал:", "composer.discard": "Вы уверены, что хотите отменить все изменения?", "composer.submit_and_lock": "Отправить и закрыть", "composer.toggle_dropdown": "Показать выпадающий список", "composer.uploading": "Загрузка %1", + "composer.formatting.bold": "Жирность", + "composer.formatting.italic": "Курсив", + "composer.formatting.list": "Список", + "composer.formatting.strikethrough": "Перечеркивание", + "composer.formatting.link": "Ссылка", + "composer.formatting.picture": "Изображение", + "composer.upload-picture": "Загрузить изображение", + "composer.upload-file": "Загрузить файл", + "composer.zen_mode": "Полноэкранный режим", + "composer.select_category": "Выберите категорию", "bootbox.ok": "ОК", "bootbox.cancel": "Отмена", "bootbox.confirm": "Подтвердить", diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index 21c0a3b46c..0a289efe57 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> и %2 другие проголосовали за ваше сообщение в <strong>%3</strong>", "moved_your_post": "<strong>%1</strong> переместил Ваше сообщение в <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> переместил <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> пометил сообщение в <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> и <strong>%2</strong> пометили ваше сообщение в <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> и %2 другие пометили ваше сообщение <strong>%3</strong>", @@ -30,7 +27,7 @@ "user_started_following_you_dual": "<strong>%1</strong> и <strong>%2</strong> подписались на вас.", "user_started_following_you_multiple": "<strong>%1</strong> и %2 подписались на вас.", "new_register": "<strong>%1</strong> отправил запрос на регистрацию.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "В очереди <strong>%1</strong> заявок на регистрацию.", "email-confirmed": "Email подтвержден", "email-confirmed-message": "Спасибо за подтверждение Вашего Email-адреса. Ваш аккаунт активирован.", "email-confirm-error-message": "Ошибка проверки Email-адреса. Возможно, код неверен, либо у него истек срок действия.", diff --git a/public/language/ru/pages.json b/public/language/ru/pages.json index 962dba881c..40493151ad 100644 --- a/public/language/ru/pages.json +++ b/public/language/ru/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Пользователи по кол-ву сообщений", "users/sort-reputation": "Пользователи по кол-ву репутации", "users/banned": "Заблокированные пользователи", + "users/most-flags": "Пользователи на которых больше всех жалуются", "users/search": "Поиск пользователей", "notifications": "Уведомления", "tags": "Теги", "tag": "Темы с тегом \"%1\"", "register": "Зарегистрироваться", + "registration-complete": "Регистрация завершена", "login": "Войти", "reset": "Забыл пароль?", "categories": "Категории", @@ -28,19 +30,20 @@ "account/edit/password": "Редактирование пароля \"%1\"", "account/edit/username": "Редактирование имени пользователя \"%1\"", "account/edit/email": "Редактирование email \"%1\"", + "account/info": "Информация об аккаунте", "account/following": "Наблюдают %1", "account/followers": "Кто наблюдает %1", "account/posts": "Сообщение от %1", "account/topics": "Тема создана %1", "account/groups": "%1 Групп", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1 постов в закладках", "account/settings": "Настройки пользователя", "account/watched": "Тему смотрели %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", + "account/upvoted": "Рейтинг поста поднят %1", + "account/downvoted": "Рейтинг поста снижен %1", "account/best": "Лучшие сообщения написанные %1", "confirm": "Email подтвержден", "maintenance.text": "%1 в настоящее время на обслуживании. Пожалуйста, возвращайтесь позже.", "maintenance.messageIntro": "Администратор оставил сообщение:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "throttled.text": "%1 в настоящее время недоступен из-за высокой нагрузки. Пожалуйста, приходите в другой раз." } \ No newline at end of file diff --git a/public/language/ru/register.json b/public/language/ru/register.json index 6d01b59e1c..06afecac8d 100644 --- a/public/language/ru/register.json +++ b/public/language/ru/register.json @@ -1,5 +1,6 @@ { "register": "Регистрация", + "cancel_registration": "Отменить Регистрацию", "help.email": "По умолчанию, ваш email будет скрыт.", "help.username_restrictions": "Уникальное Имя между %1 и %2 символов. Другие пользователи смогут упоминать вас по @<span id='yourUsername'>Имени</span>.", "help.minimum_password_length": "Длина вашего пароля должна быть минимум %1 символов.", @@ -15,5 +16,8 @@ "alternative_registration": "Альтернативная регистрация", "terms_of_use": "Условия использования", "agree_to_terms_of_use": "Я согласен с условиями", - "registration-added-to-queue": "Ваша регистрация была добавлена в очередь на утверждение. Вы получите уведомление по электронной почте, когда она будет одобрена администратором." + "terms_of_use_error": "Вы должны согласиться с условиями использования", + "registration-added-to-queue": "Ваша регистрация была добавлена в очередь на утверждение. Вы получите уведомление по электронной почте, когда она будет одобрена администратором.", + "interstitial.intro": "Нам потребуется дополнительная информация, прежде чем мы сможем создать вашу учетную запись.", + "interstitial.errors-found": "Мы не смогли завершить регистрацию:" } \ No newline at end of file diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index 00ca832430..2223108c47 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -26,12 +26,30 @@ "tools": "Опции", "flag": "Отметить", "locked": "Закрыт", - "bookmark_instructions": "Нажмите здесь чтобы вернуться к последнему непрочитанному сообщению в теме.", + "pinned": "Прикреплена", + "moved": "Перемещена", + "bookmark_instructions": "Нажмите здесь, чтобы вернуться к последнему прочитанному сообщению в этой теме.", "flag_title": "Отметить сообщение для модерирования", "flag_success": "Это сообщение было помечено для модерации", + "flag_manage_title": "Отмеченные сообщения в %1", + "flag_manage_history": "История действий", + "flag_manage_no_history": "В отчете нет событий", + "flag_manage_assignee": "Правоприемник", + "flag_manage_state": "Состояние", + "flag_manage_state_open": "Новое/Открытое", + "flag_manage_state_wip": "Рассматривается", + "flag_manage_state_resolved": "Подтверждено", + "flag_manage_state_rejected": "Отклонено", + "flag_manage_notes": "Коллективные заметки", + "flag_manage_update": "Обновить статус жалобы", + "flag_manage_history_assignee": "Назначено %1", + "flag_manage_history_state": "Статус обновлен на %1", + "flag_manage_history_notes": "Статус жалобы обновлен", + "flag_manage_saved": "Подробности жалобы обновлены", "deleted_message": "Эта тема была удалена. Только пользователи с правами управления темами могут ее видеть.", "following_topic.message": "Теперь вы будете получать уведомления при обновлении этой темы.", - "not_following_topic.message": "Вы больше не будете получать уведомления из этой темы.", + "not_following_topic.message": "Вы увидите эту тему в списке непрочитанных тем, но Вы не будете получать уведомления, когда кто-то напишет сообщение в эту тему.", + "ignoring_topic.message": "Вы больше не будете видеть эту тему в списке непрочитанных тем. Вас уведомят, когда Вас упомянут или проголосуют за Ваш пост.", "login_to_subscribe": "Пожалуйста зарегистрируйтесь, или войдите под своим аккаунтом, чтобы подписаться на эту тему.", "markAsUnreadForAll.success": "Тема помечена как непрочитанная для всех.", "mark_unread": "Отметить как непрочитанное", @@ -41,8 +59,14 @@ "watch.title": "Сообщать мне об ответах в этой теме", "unwatch.title": "Не сообщать мне об ответах в этой теме", "share_this_post": "Поделиться этим Сообщением", + "watching": "Наблюдать", + "not-watching": "Не наблюдать", + "ignoring": "Игнорирование", + "watching.description": "Сообщить мне о новых ответах.<br/>Показать тему в непрочитанных.", + "not-watching.description": "Не уведомлять меня о новых ответах.<br/>Показать тему в непрочитанных, если категория мной не игнорируется.", + "ignoring.description": "Не уведомлять меня о новых ответах.<br/>Не отображать тему в непрочитанных.", "thread_tools.title": "Опции темы", - "thread_tools.markAsUnreadForAll": "Отметить как непрочитанные", + "thread_tools.markAsUnreadForAll": "Отметить как непрочитанное для всех", "thread_tools.pin": "Прикрепить тему", "thread_tools.unpin": "Открепить тему", "thread_tools.lock": "Закрыть тему", @@ -77,12 +101,13 @@ "topic_will_be_moved_to": "Эта тема будет перенесена в категорию", "fork_topic_instruction": "Отметьте сообщения для ответвления", "fork_no_pids": "Сообщения не отмечены!", + "fork_pid_count": "%1 сообщение(я) отмечены", "fork_success": "Готово! Нажмите для перехода в отделённую тему.", "delete_posts_instruction": "Отметьте сообщения, которые Вы хотите удалить/очистить", "composer.title_placeholder": "Введите название темы...", "composer.handle_placeholder": "Название", "composer.discard": "Отменить", - "composer.submit": "Подтвердить", + "composer.submit": "Отправить", "composer.replying_to": "Ответ %1", "composer.new_topic": "Создать тему", "composer.uploading": "загрузка...", @@ -101,12 +126,12 @@ "newest_to_oldest": "От новых к старым", "most_votes": "По голосам", "most_posts": "По количеству ответов", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.title": "Создать новую тему вместо этой?", + "stale.warning": "Тема, в которую Вы пишите, очень старая. Вы хотите создать новую тему?", "stale.create": "Создать новую тему", "stale.reply_anyway": "Всё равно ответить в этой теме", - "link_back": "Re: [%1](%2)", + "link_back": "Ответ: [%1](%2)", "spam": "Спам", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "offensive": "Оскорбление", + "custom-flag-reason": "Введите другую причину" } \ No newline at end of file diff --git a/public/language/ru/unread.json b/public/language/ru/unread.json index 7d51d6abbf..421d8d730c 100644 --- a/public/language/ru/unread.json +++ b/public/language/ru/unread.json @@ -6,5 +6,8 @@ "selected": "Выбранное", "all": "Все", "all_categories": "Все категории", - "topics_marked_as_read.success": "Темы помечены как прочитанные!" + "topics_marked_as_read.success": "Темы помечены как прочитанные!", + "all-topics": "Все темы", + "new-topics": "Новые темы", + "watched-topics": "Наблюдаемые темы" } \ No newline at end of file diff --git a/public/language/ru/uploads.json b/public/language/ru/uploads.json index 1622cb5693..dedc27456d 100644 --- a/public/language/ru/uploads.json +++ b/public/language/ru/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Загрузка файла...", + "select-file-to-upload": "Укажите файл для загрузки!", + "upload-success": "Файл успешно загружен!", + "maximum-file-size": "Максимум %1 kb" } \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 5e14dc8004..b0bcf9ebf5 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -6,6 +6,7 @@ "postcount": "Сообщений", "email": "Email", "confirm_email": "Подтвердить Email", + "account_info": "Информация об аккаунте", "ban_account": "Заблокировать учетную запись", "ban_account_confirm": "Вы действительно хотите заблокировать этого пользователя?", "unban_account": "Разблокировать учетную запись", @@ -23,7 +24,7 @@ "profile_views": "Просмотров профиля", "reputation": "Репутация", "favourites": "Закладки", - "watched": "Просмотров", + "watched": "Наблюдаемые", "followers": "Читателей", "following": "Читаемых", "aboutme": "Обо мне", @@ -79,31 +80,50 @@ "has_no_posts": "Этот пользователь еще ничего не написал.", "has_no_topics": "Этот пользователь еще не размещал какие-либо темы.", "has_no_watched_topics": "Этот пользователь еще не просматривал какие-либо темы.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Пользователь не поднимал рейтинг ни одному посту.", + "has_no_downvoted_posts": "Пользователь не понижал рейтинг ни одному посту.", + "has_no_voted_posts": "Пользователь не голосовал за сообщения.", "email_hidden": "Email скрыт", "hidden": "скрыто", "paginate_description": "Разбивать по страницам топики и посты вместо того чтобы выводить бесконечным списком", "topics_per_page": "Тем на Странице", "posts_per_page": "Постов на Странице", "notification_sounds": "Воспроизводить звук во время получения уведомления", + "notifications_and_sounds": "Уведомления и звуки", + "incoming-message-sound": "Звук входящего сообщения", + "outgoing-message-sound": "Звук исходящего сообщения", + "notification-sound": "Звук уведомления", + "no-sound": "Без звука", "browsing": "Настройки просмотра", "open_links_in_new_tab": "Открывать внешние ссылки в новых вкладках", "enable_topic_searching": "Активировать поиск внутри тем", "topic_search_help": "Если опция включена, поиск в теме будет осуществляться за счёт собственного поиска, который позволит искать во всей теме, а не только в загруженных сообщениях", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Следить за темами в которых вы отвечаете", - "follow_topics_you_create": "Следить за темами которые вы создаёте", - "grouptitle": "Выберите бейдж группы для отображения", + "delay_image_loading": "Ожидайте загрузки изображения", + "image_load_delay_help": "Если включить, изображения в темах не будут загружаться, пока они не появятся в поле зрения.", + "scroll_to_my_post": "После отправки ответа, прокрутить страницу до него.", + "follow_topics_you_reply_to": "Следить за темами на которые Вы ответили", + "follow_topics_you_create": "Следить за темами на которые Вы создали", + "grouptitle": "Заголовок группы", "no-group-title": "Не показывать бейдж", "select-skin": "Выбрать скин", "select-homepage": "Укажите главную страницу", "homepage": "Главная страница", "homepage_description": "Укажите страницу, которую хотите использовать как главную страницу форума или 'None', что бы использовать страницу по умолчанию.", - "custom_route": "Custom Homepage Route", - "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", + "custom_route": "Другой маршрут главной страницы", + "custom_route_help": "Введите название маршрута здесь, без слешей (например, \\\"recent\\\", или \\\"popular\\\")", "sso.title": "Сервис единого входа", "sso.associated": "Связан с", - "sso.not-associated": "Нажмите здесь, что бы связать себя с" + "sso.not-associated": "Нажмите здесь, что бы связать себя с", + "info.latest-flags": "Новые отмеченные сообщения", + "info.no-flags": "Отмеченных сообщений не найдено", + "info.ban-history": "Недавно Заблокированы", + "info.no-ban-history": "Этот пользователь никогда не был заблокирован", + "info.banned-until": "Заблокирован до %1", + "info.banned-permanently": "Заблокирован навсегда", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Без объяснения причин.", + "info.username-history": "История имен", + "info.email-history": "История емейлов", + "info.moderation-note": "Примечание модератора", + "info.moderation-note.success": "Примечание модератора сохранено" } \ No newline at end of file diff --git a/public/language/ru/users.json b/public/language/ru/users.json index 40c180685a..36dcf0ead7 100644 --- a/public/language/ru/users.json +++ b/public/language/ru/users.json @@ -2,6 +2,7 @@ "latest_users": "Новые пользователи", "top_posters": "Самые активные", "most_reputation": "Лучшая репутация", + "most_flags": "Больше всего отмеченных сообщений", "search": "Поиск", "enter_username": "Введите имя пользователя для поиска", "load_more": "Загрузить еще", diff --git a/public/language/rw/category.json b/public/language/rw/category.json index 5bfd9286eb..f9b137bdbf 100644 --- a/public/language/rw/category.json +++ b/public/language/rw/category.json @@ -10,7 +10,11 @@ "share_this_category": "Sangiza iki cyiciro", "watch": "Kurikirana", "ignore": "Ihorere", - "watch.message": "Uzajya ubu ukurikirana ibishya byongewe muri iki cyiciro", - "ignore.message": "Ubu urekeye aho kuzajya ubona ibishya byongewe muri iki cyiciro", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Ibyiciro Bikurikirwa" } \ No newline at end of file diff --git a/public/language/rw/email.json b/public/language/rw/email.json index 626f11dec4..d26954b90b 100644 --- a/public/language/rw/email.json +++ b/public/language/rw/email.json @@ -24,6 +24,7 @@ "digest.day": "umunsi", "digest.week": "icyumweru", "digest.month": "ukwezi", + "digest.subject": "Digest for %1", "notif.chat.subject": "Ubutumwa bwo mu gikari bwaturutse kuri %1", "notif.chat.cta": "Kanda hano kugirango ukomeze", "notif.chat.unsub.info": "Iri tangazo rijyanye n'ubutumwa bwo mu gikari waryohererejwe kubera ko wabihisemo mu byo uzajya umenyeshwa", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index a86168d3ef..b423d96d19 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ijambobanga Ntiryemewe", "invalid-username-or-password": "Tanga izina ukoresha n'ijambobanga", "invalid-search-term": "Icyashatswe nticyemewe", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Izina ryarafashwe mbere", "email-taken": "Email yarafashwe mbere", "email-not-confirmed": "Email yawe ntabwo iremezwa. Kanda hano kugirango wemeze email yawe.", "email-not-confirmed-chat": "Ntabwo uremererwa kuganirira mu gikari kuko email yawe itari yemezwa. Kanda hano kugirango wemeze email yawe. ", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Uru rubuga rusaba ko wemeza ko utunze email. Kanda hano kugirango utange email yawe", "email-confirm-failed": "Ntabwo email yawe yabashije kwemezwa. Ongera ugerageze mu bundi buryo. ", "confirm-email-already-sent": "Email yo kwemeza yamaze koherezwa. Tegereza iminota (umunota) %1 mbere yo kohereza indi. ", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Izina ni rigufi cyane", "username-too-long": "Izina ni rirerire cyane", "password-too-long": "Password too long", "user-banned": "Umuntu wirukanwe", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Wihangena kuko usabwa gutegereza amasegonda (isegonda) %1 mbere yo gushyiraho ikintu cyawe cya mbere", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Icyiciro kitabaho", "no-topic": "Ikiganiro kitabaho", "no-post": "Icyashyizweho kitabaho", @@ -38,10 +43,26 @@ "category-disabled": "Icyiciro cyabujijwe", "topic-locked": "Ikiganiro Cyafungiranywe", "post-edit-duration-expired": "Wemerewe gusa kugira icyo uhindura ku byo washyizeho nyuma y'amasegonda (isegonda) %1 nyuma yo kubishyiraho", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Gerageza ushyireho ikintu kirekireho. Icyo ushyiraho kigomba kuba kigizwe nibura n'inyuguti (cyangwa ibimenyetso) zigera kuri %1.", "content-too-long": "Gerageza ushyireho ibintu bigufiyaho. Icyo ushyiraho kigomba kuba kigizwe n'inyuguti (cyangwa ibimenyetso) zirenga %1. ", "title-too-short": "Gerageza ushyireho umutwe muremureho. Umutwe ugomba kuba ugizwe n'inyuguti (cyangwa ibimenyetso) zigera kuri %1. ", "title-too-long": "Gerageza ushyireho umutwe mugufiyaho. Umutwe ugomba kuba ugizwe n'inyuguti (cyangwa ibimenyetso) zitarenga %1. ", + "category-not-selected": "Category not selected.", "too-many-posts": "Wemerewe kugira icyo ushyiraho rimwe mu masegonda (isegonda) %1. Ba utegerejeho gato kugirango wongere", "too-many-posts-newbie": "Nk'umuntu mushya, wemerewe gushyiraho ikintu rimwe mu masegonda (isegonda) %1 kugeza igihe ugize amanota agera kuri %2. Ba utegerejeho gato kugirango wongere", "tag-too-short": "Gerageza ukoreshe akamenyetso kagizwe n'inyuguti (cyangwa ibimenyetso) nibura zigera kuri %1", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Ntabwo wakwirukana abandi bayobozi!", "cant-remove-last-admin": "Ni wowe muyobozi wenyine. Ongeramo undi muntu nk'umuyobozi mbere y'uko wikura ku buyobozi", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Ubwoko bw'ifoto wahisemo ntibwemewe. Hemewe gusa: %1", "invalid-image-extension": "Impera itemewe igaragaza foruma y'ifoto", "invalid-file-type": "Ubwoko bw'ifayilo ntibwemewe. Hemewe gusa: %1", "group-name-too-short": "Izina ry'itsinda ni rigufi cyane", + "group-name-too-long": "Group name too long", "group-already-exists": "Itsinda ryitwa gutya risanzweho", "group-name-change-not-allowed": "Guhindura izina ry'itsinda ntibyemewe", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Ibijyanye n'itangwa ry'amanota ntibyemerewe. ", "downvoting-disabled": "Kwambura amanota ntibyemerewe", "not-enough-reputation-to-downvote": "Ntabwo ufite amanota ahagije ngo ube wakwemererwa kugira uwo wambura amanota", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "Nta muntu uri muri iki gikari", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/rw/global.json b/public/language/rw/global.json index aecc3f305e..44876dcad7 100644 --- a/public/language/rw/global.json +++ b/public/language/rw/global.json @@ -7,8 +7,10 @@ "403.login": "Wenda ahari ukeneye kugerageza <a href='%1/login'>kwinjiramo</a>", "404.title": "Ntacyabonetse", "404.message": "Biragaragara ko wageze kuri paji itariho ikintu. Subira <a href='%1/'>Imbere</a>.", - "500.title": "Hari ikibazo cya tekinike imbere. ", + "500.title": "Internal Error.", "500.message": "Ye baba we! Ntibikunze!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Iyandikishe", "login": "Injiramo", "please_log_in": "Injiramo", @@ -17,6 +19,7 @@ "welcome_back": "Urakaza Neza Urisanga", "you_have_successfully_logged_in": "Winjiyemo nta ngorane", "save_changes": "Bika ibyamaze gukorwa", + "save": "Save", "close": "Funga", "pagination": "Umubare wa Paji", "pagination.out_of": "%1 muri %2", @@ -50,7 +53,9 @@ "topics": "Ibiganiro", "posts": "Ibyashyizweho", "best": "Byiza", + "upvoters": "Upvoters", "upvoted": "Byakunzwe", + "downvoters": "Downvoters", "downvoted": "Byagawe", "views": "Byarebwe", "reputation": "Amanota", @@ -90,5 +95,8 @@ "enter_page_number": "Shyiramo nimero ya paji", "upload_file": "Pakira ifayilo", "upload": "Pakira", - "allowed-file-types": "Ubwoko bw'amafayilo bwemewe ni %1" + "allowed-file-types": "Ubwoko bw'amafayilo bwemewe ni %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/rw/groups.json b/public/language/rw/groups.json index 12ec0b7193..b653c89599 100644 --- a/public/language/rw/groups.json +++ b/public/language/rw/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Va mu Itsinda", "membership.reject": "Hakanira", "new-group.group_name": "Izina ry'Itsinda:", - "upload-group-cover": "Shyiraho ifoto yo hejuru iranga itsinda" + "upload-group-cover": "Shyiraho ifoto yo hejuru iranga itsinda", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/rw/login.json b/public/language/rw/login.json index 72ae32e57c..f0a269d9f4 100644 --- a/public/language/rw/login.json +++ b/public/language/rw/login.json @@ -5,7 +5,8 @@ "remember_me": "Wibukwe?", "forgot_password": "Wibagiwe ijambobanga?", "alternative_logins": "Ukundi Wakwinjiramo", - "failed_login_attempt": "Ntiwinjiyemo. Ongera ugerageze. ", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Winjiyemo nta ngorane!", - "dont_have_account": "Nta konte ufite?" + "dont_have_account": "Nta konte ufite?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json index e0aff84e80..e73f38df8c 100644 --- a/public/language/rw/modules.json +++ b/public/language/rw/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Shyiraho kandi Unafungirane", "composer.toggle_dropdown": "Hindura Icyerekezo", "composer.uploading": "Ugupakira %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "Sawa", "bootbox.cancel": "Isubire", "bootbox.confirm": "Emeza", diff --git a/public/language/rw/notifications.json b/public/language/rw/notifications.json index 60fa3be43d..26e3343ad1 100644 --- a/public/language/rw/notifications.json +++ b/public/language/rw/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> yatambikanye ikintu muri <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/rw/pages.json b/public/language/rw/pages.json index b59cebf627..b62046c0f1 100644 --- a/public/language/rw/pages.json +++ b/public/language/rw/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Abantu bashyizeho byinshi", "users/sort-reputation": "Abantu bafite amanota menshi", "users/banned": "Abantu Bakumiriwe", + "users/most-flags": "Most flagged users", "users/search": "Gushaka Abantu", "notifications": "Amatangazo", "tags": "Ibimenyetso", "tag": "Ibiganiro bifite ibimenyetso bya \"%1\"", "register": "Fungura Konte", + "registration-complete": "Registration complete", "login": "Injira muri konte yawe", "reset": "Tangiza bundi bushya konte yawe", "categories": "Ibyiciro", @@ -28,12 +30,13 @@ "account/edit/password": "Uguhindura ijambobanga rya \"%1\"", "account/edit/username": "Uguhindura izina rya \"%1\"", "account/edit/email": "Uguhindura email ya \"%1\"", + "account/info": "Account Info", "account/following": "Abantu %1 akurikira", "account/followers": "Abantu bakurikira %1", "account/posts": "Ibyashyizweho na %1", "account/topics": "Ibiganiro byatangijwe na %1", "account/groups": "Amatsinda ya %1", - "account/favourites": "Ibyazigamwe na %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Itunganya", "account/watched": "Ibiganiro bikurikirwa na %1", "account/upvoted": "Ibiganiro byakunzwe na %1", diff --git a/public/language/rw/register.json b/public/language/rw/register.json index 332517de87..aaf97ccaa4 100644 --- a/public/language/rw/register.json +++ b/public/language/rw/register.json @@ -1,5 +1,6 @@ { "register": "Iyandike", + "cancel_registration": "Cancel Registration", "help.email": "Ubusanzwe, email yawe ntabwo iba ibonwa na bose", "help.username_restrictions": "Izina rigomba kuba ryihariye kuri uru rubuga kandi rikaba rifite uburebure bw'inyuguti buva kuri %1 kugera kuri %2. Iryo zina ni ryo abantu bazajya bifashisha nka @<span id='yourUsername'>username</span> mu gihe bakoresheje izina ryawe mu byo banditse. ", "help.minimum_password_length": "Umubare w'inyuguti n'ibimenyetso bigize ijambobanga ryawe ugomba kuba nibura %1.", @@ -15,5 +16,8 @@ "alternative_registration": "Ukundi Wakwiyandika", "terms_of_use": "Amategeko n'Amabwiriza", "agree_to_terms_of_use": "Nzakurikiza Amategeko n'Amabwiriza", - "registration-added-to-queue": "Icyifuzo cy'iyandikwa ryawe cyakiriwe ariko gitegereje isuzuma. Uzabimenyeshwa biciye muri email niba ubuyobozi bwakwemereye kwandikwa. " + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Icyifuzo cy'iyandikwa ryawe cyakiriwe ariko gitegereje isuzuma. Uzabimenyeshwa biciye muri email niba ubuyobozi bwakwemereye kwandikwa. ", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json index b3ba723ec7..e8d7adaeac 100644 --- a/public/language/rw/topic.json +++ b/public/language/rw/topic.json @@ -26,12 +26,30 @@ "tools": "Ibikoresho", "flag": "Tambikana", "locked": "Birafungiranye", - "bookmark_instructions": "Kanda hano kugirango usubire ahari ibitarasomwe biheruka muri iki kiganiro.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Bimenyeshe ubuyobozi", "flag_success": "Bimaze kumenyeshwa ubuyobozi ngo bikurikiranwe. ", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Iki kiganiro cyamaze gukurwaho. Abantu babifitiye uburenganzira ni bo bonyine bashobora kukibona. ", "following_topic.message": "Ntabwo uzongera kubimenyeshwa nihagira umuntu ugira icyo yandika kuri iki kiganiro. ", - "not_following_topic.message": "Ntabwo uzongera kujya umenyeshwa ku bibera muri iki kiganiro. ", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Ba umunyamuryango cyangwa winjiremo niba ushaka kwiyandikisha kuri iki kiganiro. ", "markAsUnreadForAll.success": "Ikiganiro kigizwe nk'icyasomwe na bose", "mark_unread": "Garagaza nk'ibyasomwe", @@ -41,8 +59,14 @@ "watch.title": "Ujye umenyeshwa ibyongerwaho bishya kuri iki kiganiro", "unwatch.title": "Rekera aho gucunga iki kiganiro", "share_this_post": "Sangiza Ibi", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Ibikoresho by'Ikiganiro", - "thread_tools.markAsUnreadForAll": "Bigaragaze nk'Ibyasomwe", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Zamura Ikiganiro", "thread_tools.unpin": "Manura Ikiganiro", "thread_tools.lock": "Fungirana Ikiganiro", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Iki kiganiro kirimurirwa mu cyiciro", "fork_topic_instruction": "Kanda ku byashizweho ushaka kugabanyaho", "fork_no_pids": "Nta kintu wahisemo!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Umaze kugabanyaho ku kiganiro! Kanda hano ugezwe ku kiganiro cyavutse. ", "delete_posts_instruction": "Kanda ku bintu ushaka guhisha/gusiba", "composer.title_placeholder": "Shyira umutwe w'ikiganiro cyawe aha...", diff --git a/public/language/rw/unread.json b/public/language/rw/unread.json index 075fdc7792..07c6e6c04c 100644 --- a/public/language/rw/unread.json +++ b/public/language/rw/unread.json @@ -6,5 +6,8 @@ "selected": "Ibyatoranyijwe", "all": "Byose", "all_categories": "Ibyiciro Byose", - "topics_marked_as_read.success": "Ibiganiro byamaze kugaragazwa nk'ibyasomwe!" + "topics_marked_as_read.success": "Ibiganiro byamaze kugaragazwa nk'ibyasomwe!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/rw/user.json b/public/language/rw/user.json index 9ca89d0160..73c0527cf4 100644 --- a/public/language/rw/user.json +++ b/public/language/rw/user.json @@ -6,6 +6,7 @@ "postcount": "Ingano y'ibyo Yashyizeho", "email": "Email", "confirm_email": "Emeza Email", + "account_info": "Account Info", "ban_account": "Irukana", "ban_account_confirm": "Wiringiye neza ko ushaka kwirukana uyu muntu?", "unban_account": "Garura iyi Konte", @@ -88,14 +89,21 @@ "topics_per_page": "Ibiganiro kuri Buri Paji", "posts_per_page": "Ibyashyizweho kuri Buri Paji", "notification_sounds": "Hajye humvikana ijwi rikumenyesha ko haje itangazo rishya", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Gutunganya Uburyo Usoma", "open_links_in_new_tab": "Fungurira imirongo ijya hanze mu idirishya rishya", "enable_topic_searching": "Emerera Ugushakira mu Kiganiro", "topic_search_help": "Nibyemerwa, ugushakira mu kiganiro bizajya biba ari byo bikorwa maze bitume umuntu abasha gushakira mu kiganiro hose aho gushakira kuri paji igaragarira amaso, imbere yawe gusa", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "Nyuma yo gushyiraho igisubizo, hagaragare icyashyizweho gishya", - "follow_topics_you_reply_to": "Kurikira ibiganiro ushyiraho ibisubizo", - "follow_topics_you_create": "Kurikira ibiganiro uba watangije", - "grouptitle": "Hitamo umutwe w'itsinda ushaka ko uzajya ugaragara", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "Nta mutwe w'itsinda", "select-skin": "Hitamo Uruhu", "select-homepage": "Hitamo Paji y'Imbere", @@ -105,5 +113,17 @@ "custom_route_help": "Shyiramo izina ry'inzira, utiriwe ushyiraho akarongo (ni ukuvuga ni nko kwandika gusa \"ibiheruka\" cyangwa \"ibikunzwe\")", "sso.title": "Kwinjiramo ukoreshe serivisi za SSO", "sso.associated": "Bisanishijwe na", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/rw/users.json b/public/language/rw/users.json index 16993f0592..dc92a1f54b 100644 --- a/public/language/rw/users.json +++ b/public/language/rw/users.json @@ -2,6 +2,7 @@ "latest_users": "Abantu Bashya", "top_posters": "Abashyizeho Byinshi", "most_reputation": "Abafite Amanota Menshi", + "most_flags": "Most Flags", "search": "Shaka", "enter_username": "Shyiramo izina ryo gushaka", "load_more": "Zana Ibindi", diff --git a/public/language/sc/category.json b/public/language/sc/category.json index f9c527ad17..324b42de64 100644 --- a/public/language/sc/category.json +++ b/public/language/sc/category.json @@ -10,7 +10,11 @@ "share_this_category": "Share this category", "watch": "Watch", "ignore": "Ignore", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/sc/email.json b/public/language/sc/email.json index 1b8d512945..691e6309a2 100644 --- a/public/language/sc/email.json +++ b/public/language/sc/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "New chat message received from %1", "notif.chat.cta": "Click here to continue the conversation", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index 0709e823b6..076a5c0345 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -14,20 +14,25 @@ "invalid-password": "Invalid Password", "invalid-username-or-password": "Please specify both a username and password", "invalid-search-term": "Invalid search term", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Username taken", "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username too short", "username-too-long": "Username too long", "password-too-long": "Password too long", "user-banned": "User banned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", "no-post": "Post does not exist", @@ -38,10 +43,26 @@ "category-disabled": "Category disabled", "topic-locked": "Topic Locked", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-name-too-long": "Group name too long", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/sc/global.json b/public/language/sc/global.json index 586d9340bf..815a01096c 100644 --- a/public/language/sc/global.json +++ b/public/language/sc/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "No Agatadu", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Faddina interna.", + "500.title": "Internal Error.", "500.message": "Oops! Paret chi carchi cosa est andada male!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registra·ti", "login": "Intra", "please_log_in": "Pro praghere Intra", @@ -17,6 +19,7 @@ "welcome_back": "Welcome Back", "you_have_successfully_logged_in": "Ses intradu", "save_changes": "Alloga Acontzos", + "save": "Save", "close": "Serra", "pagination": "Paginatzione", "pagination.out_of": "%1 out of %2", @@ -50,7 +53,9 @@ "topics": "Topics", "posts": "Arresonos", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Bìsitas", "reputation": "Reputation", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/sc/groups.json b/public/language/sc/groups.json index 3c4f6ce638..8af283d60e 100644 --- a/public/language/sc/groups.json +++ b/public/language/sc/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/sc/login.json b/public/language/sc/login.json index 83a84e0ad1..7249b15820 100644 --- a/public/language/sc/login.json +++ b/public/language/sc/login.json @@ -5,7 +5,8 @@ "remember_me": "Regorda·mi?", "forgot_password": "Password Iscarèssida?", "alternative_logins": "Intradas Alternativas", - "failed_login_attempt": "Intrada isballiada, pro praghere torra a provare.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Ses intradu!", - "dont_have_account": "Don't have an account?" + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json index 89b779d53e..ed17a97b6a 100644 --- a/public/language/sc/modules.json +++ b/public/language/sc/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json index 5f7fd03816..d92a4638cc 100644 --- a/public/language/sc/notifications.json +++ b/public/language/sc/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/sc/pages.json b/public/language/sc/pages.json index a2e82aa90f..65766c8f40 100644 --- a/public/language/sc/pages.json +++ b/public/language/sc/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notìficas", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/sc/register.json b/public/language/sc/register.json index 7b4bb97cd7..1588a83ad5 100644 --- a/public/language/sc/register.json +++ b/public/language/sc/register.json @@ -1,5 +1,6 @@ { "register": "Registra·ti", + "cancel_registration": "Cancel Registration", "help.email": "S'email tua est cuada pro su pùblicu in manera predefinida.", "help.username_restrictions": "Unu nùmene de impitadore ùnicu intre %1 e %2 caràtere. Is àteros t'ant a pòdere mentovare cun @<span id='yourUsername'>nùmeneimpitadore</span>.", "help.minimum_password_length": "Sa password depet èssere a su mancu de %1 caràteres.", @@ -15,5 +16,8 @@ "alternative_registration": "Registratziones Alternativas", "terms_of_use": "Tèrmines de Impreu", "agree_to_terms_of_use": "So de acòrdiu cun is Tèrmines de Impreu", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index 1f58d8582d..11c3eeec55 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -26,12 +26,30 @@ "tools": "Ainas", "flag": "Signala", "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Signala custu arresonu pro sa moderatzione", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "As a retzire notìficas si calincunu pùblica in custa arresonada.", - "not_following_topic.message": "No as a retzire prus notìficas pro custa arresonada.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Pro praghere registra·ti o intra pro sutascrìere custa arresonada.", "markAsUnreadForAll.success": "Arresonada marcada comente de lèghere pro totus.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Cumpartzi custu Arresonu", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Signa comente De Lèghere", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pone in evidèntzia s'Arresonda", "thread_tools.unpin": "Boga dae s'Evidèntzia s'Arresonasa", "thread_tools.lock": "Bloca Arresonada", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Custa arresonada at a èssere mòvida in sa creze", "fork_topic_instruction": "Sèbera is arresonos chi boles partzire", "fork_no_pids": "Perunu arresonu seberadu!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Pone su tìtulu de s'arresonada inoghe...", diff --git a/public/language/sc/unread.json b/public/language/sc/unread.json index 692bb12f14..3039d0fa6e 100644 --- a/public/language/sc/unread.json +++ b/public/language/sc/unread.json @@ -6,5 +6,8 @@ "selected": "Selected", "all": "All", "all_categories": "All categories", - "topics_marked_as_read.success": "Topics marked as read!" + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/sc/user.json b/public/language/sc/user.json index 2b30121863..68591dc76b 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -6,6 +6,7 @@ "postcount": "Post Count", "email": "Email", "confirm_email": "Confirm Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Arresonadas pro Pàgina", "posts_per_page": "Arresonos pro Pàgina", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/sc/users.json b/public/language/sc/users.json index 93913a20a3..7c1de58d1a 100644 --- a/public/language/sc/users.json +++ b/public/language/sc/users.json @@ -2,6 +2,7 @@ "latest_users": "Ùrtimos Impitadores", "top_posters": "Prus Ativos", "most_reputation": "Prus Famados", + "most_flags": "Most Flags", "search": "Chirca", "enter_username": "Pone unu nùmene de impitadore de chircare", "load_more": "Càrriga de prus", diff --git a/public/language/sk/category.json b/public/language/sk/category.json index a700fc155c..b1e017f33f 100644 --- a/public/language/sk/category.json +++ b/public/language/sk/category.json @@ -10,7 +10,11 @@ "share_this_category": "zdielaj túto kategóriu", "watch": "Watch", "ignore": "Ignoruj", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/sk/email.json b/public/language/sk/email.json index 5fb7201bf4..8ad65fe965 100644 --- a/public/language/sk/email.json +++ b/public/language/sk/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "Máte novú správu od %1", "notif.chat.cta": "Kliknite sem pre pokračovanie v konverzácii", "notif.chat.unsub.info": "Toto upozornenie o správach ste prijali na základe Vašich nastavení účtu.", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 4d99825f9e..1d34a2c7f0 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -14,20 +14,25 @@ "invalid-password": "Nesprávne heslo", "invalid-username-or-password": "Please specify both a username and password", "invalid-search-term": "Invalid search term", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "Užívateľske meno je obsadené", "email-taken": "Email je obsadený", "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, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", "email-confirm-failed": "We could not confirm your email, please try again later.", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "Username too short", "username-too-long": "Username too long", "password-too-long": "Password too long", "user-banned": "Užívateľ je zakázaný", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Category does not exist", "no-topic": "Topic does not exist", "no-post": "Post does not exist", @@ -38,10 +43,26 @@ "category-disabled": "Kategória je znefunkčená.", "topic-locked": "Uzamknutá téma", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "Nemožte zakázať druhých adminov.", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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": "Názov skupiny je príliš krátky", + "group-name-too-long": "Group name too long", "group-already-exists": "Skupina už existuje", "group-name-change-not-allowed": "Nepovolená zmena mena skupiny", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/sk/global.json b/public/language/sk/global.json index 652bb0e62a..3efe047ecf 100644 --- a/public/language/sk/global.json +++ b/public/language/sk/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "Stránka nenájdená", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "Neznámá chyba", + "500.title": "Internal Error.", "500.message": "Jejda, vyzerá, že sa niečo pokazilo.", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registrovať", "login": "Prihlásiť sa", "please_log_in": "Prosím, prihláste sa", @@ -17,6 +19,7 @@ "welcome_back": "Welcome Back", "you_have_successfully_logged_in": "Úspešne si sa prihlásil", "save_changes": "Uložiť zmeny", + "save": "Save", "close": "Zrušiť", "pagination": "Stránkovanie", "pagination.out_of": "%1 z %2", @@ -50,7 +53,9 @@ "topics": "Tém", "posts": "Príspevkov", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Zhliadnutí", "reputation": "Reputácia", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/sk/groups.json b/public/language/sk/groups.json index ee2368b97e..766742e852 100644 --- a/public/language/sk/groups.json +++ b/public/language/sk/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/sk/login.json b/public/language/sk/login.json index bc2f675ff2..feb1f4dfe5 100644 --- a/public/language/sk/login.json +++ b/public/language/sk/login.json @@ -5,7 +5,8 @@ "remember_me": "Zapamätať si ma?", "forgot_password": "Zabudol si heslo?", "alternative_logins": "Ďalšie spôsoby prihlásenia", - "failed_login_attempt": "Prihlásenie sa nepodarilo, skús to znovu.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Prihlásenie prebehlo úspešne!", - "dont_have_account": "Nemáš účet?" + "dont_have_account": "Nemáš účet?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index b9be2fb6c3..fda24c5754 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json index 160a4d9e86..6092fb256b 100644 --- a/public/language/sk/notifications.json +++ b/public/language/sk/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/sk/pages.json b/public/language/sk/pages.json index f5d5c49231..9addda3be3 100644 --- a/public/language/sk/pages.json +++ b/public/language/sk/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Notifikácie", "tags": "Tagy", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/sk/register.json b/public/language/sk/register.json index f3d6e5db2e..38d97f6fcc 100644 --- a/public/language/sk/register.json +++ b/public/language/sk/register.json @@ -1,5 +1,6 @@ { "register": "Registrácia", + "cancel_registration": "Cancel Registration", "help.email": "Váš email nebude bez vašeho povolenia zverejnený.", "help.username_restrictions": "Jedinečné užívateľské meno dlhé %1 až %2 znakov. Ostatní užívatelia Vás môžu spomenúť ako @<span id='yourUsername'>užívateľské-meno</span>.", "help.minimum_password_length": "Dĺžka vašeho hesla musí byť aspoň %1 znakov.", @@ -15,5 +16,8 @@ "alternative_registration": "Iný spôsob registrácie", "terms_of_use": "Podmienky používania", "agree_to_terms_of_use": "Súhlasím s podmienkami používania", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index 39f4ca03d8..a5c642a501 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -26,12 +26,30 @@ "tools": "Nástroje", "flag": "Označiť", "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Označiť príspevok pre moderáciu", "flag_success": "Tento príspevok bol označený na úpravu. ", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "Budete teraz príjimať notifikácie, ked niekto prispeje do témy.", - "not_following_topic.message": "Nebudete už dostávať notifikácie z tejto Témy", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Prosím Zaregistrujte sa alebo sa Prihláste, aby ste mohli odoberať túto Tému", "markAsUnreadForAll.success": "Téma označená ako neprečítaná pre všetkých.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Buďte informovaní o nových odpovediach k tejto téme", "unwatch.title": "Stop watching this topic", "share_this_post": "Zdielaj tento príspevok", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Topic Tools", - "thread_tools.markAsUnreadForAll": "Označ ako neprečítané", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Zviditeľniť tému", "thread_tools.unpin": "Odstrániť zviditeľnenie témy", "thread_tools.lock": "Zamknúť tému", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Táto téma bude presunutá do kategórie", "fork_topic_instruction": "Vyber príspevky, ktoré chceš oddeliť", "fork_no_pids": "Žiadne príspevky neboli vybrané!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Vlož nadpis témy sem...", diff --git a/public/language/sk/unread.json b/public/language/sk/unread.json index cd254ebd4a..eed25e3c74 100644 --- a/public/language/sk/unread.json +++ b/public/language/sk/unread.json @@ -6,5 +6,8 @@ "selected": "Vybrané", "all": "Všetko", "all_categories": "All categories", - "topics_marked_as_read.success": "Témy označiť ako prečítané!" + "topics_marked_as_read.success": "Témy označiť ako prečítané!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/sk/user.json b/public/language/sk/user.json index 6baaf337ce..c519271dd3 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -6,6 +6,7 @@ "postcount": "Počet príspevkov", "email": "Email", "confirm_email": "Confirm Email", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Témy na stranu", "posts_per_page": "Príspevkov na stranu", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Hľadaj v nadstaveniach", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/sk/users.json b/public/language/sk/users.json index 73b24c1744..1c6c9f717e 100644 --- a/public/language/sk/users.json +++ b/public/language/sk/users.json @@ -2,6 +2,7 @@ "latest_users": "Najnovší užívatelia", "top_posters": "Najaktívnejší", "most_reputation": "Najváženejší", + "most_flags": "Most Flags", "search": "Vyhľadávať", "enter_username": "Zadaj užívateľské meno k hľadaniu", "load_more": "Načítať dalšie", diff --git a/public/language/sl/category.json b/public/language/sl/category.json index eee12f8c27..301b6cfbdc 100644 --- a/public/language/sl/category.json +++ b/public/language/sl/category.json @@ -1,16 +1,20 @@ { "category": "Kategorija", - "subcategories": "Podkategorija", + "subcategories": "Podkategorije", "new_topic_button": "Nova tema", - "guest-login-post": "Za objavljanje moraš biti prijavljen", - "no_topics": "<strong>V tej kategoriji ni tem.</strong><br />Boš odprl novo temo?", + "guest-login-post": "Prijava", + "no_topics": "<strong>V tej kategoriji ni tem.</strong>", "browsing": "brskanje", "no_replies": "Nihče ni odgovoril", "no_new_posts": "Ni novih objav.", "share_this_category": "Deli to kategorijo", "watch": "Spremljaj", - "ignore": "Ne spremljaj", - "watch.message": "Trenutno spremljaš nove objave v tej kategoriji", - "ignore.message": "Ne spremljaš novih objav v tej kategoriji", - "watched-categories": "Watched categories" + "ignore": "Prezri", + "watching": "Spremljano", + "ignoring": "Prezrto", + "watching.description": "Pokaži teme v Neprebrano", + "ignoring.description": "Ne pokaži tem v Neprebrano", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Spremljane kategorije" } \ No newline at end of file diff --git a/public/language/sl/email.json b/public/language/sl/email.json index 5007647a0f..36911ab96d 100644 --- a/public/language/sl/email.json +++ b/public/language/sl/email.json @@ -1,35 +1,36 @@ { - "password-reset-requested": "Zahtevana ponastavitev gesla - %1!", - "welcome-to": "Pozdravljeni na %1", + "password-reset-requested": "Zahtevana je bila ponastavitev gesla - %1!", + "welcome-to": "Dobrodošli %1", "invite": "Povabilo od %1", "greeting_no_name": "Živjo", "greeting_with_name": "Živjo %1", - "welcome.text1": "Hvala, ker ste se registrirali na %1!", - "welcome.text2": "Za popolno aktivacijo vašega računa morate potrditi elektronski naslov s katerim ste se registrirali.", - "welcome.text3": "Administrator je odobril prošnjo za registracijo. Sedaj se lahko registrirate z vašim uporabniškim imenom in geslom.", - "welcome.cta": "Kliknite za potrditev vašega elektronskega naslova", - "invitation.text1": "%1 te je povabil, da se pridružiš %2", - "invitation.ctr": "Klikni tu za kreiranje vašega računa.", + "welcome.text1": "Hvala, ker ste se registrirali na forumu: %1!", + "welcome.text2": "Za popolno aktivacijo vašega računa moramo preveriti lastništvo e-poštnega naslova s katerim ste se registrirali.", + "welcome.text3": "Srbnik je sprejel vašo registracijo. Sedaj se lahko prijavite z vašim uporabniškim imenom in geslom.", + "welcome.cta": "Kliknite tu za potrditev vašega e-poštnega naslova", + "invitation.text1": "%1 te je povabil, da se pridružiš forumu: %2", + "invitation.ctr": "Klikni tu in ustvari svoj račun.", "reset.text1": "Dobili smo zahtevo za ponastavitev vašega gesla. Če niste zahtevali ponastavitve gesla, prosimo prezrite to sporočilo.", "reset.text2": "Za nadaljevanje ponastavitve gesla prosimo kliknite na sledečo povezavo:", "reset.cta": "Kliknite tu za ponastavitev gesla", - "reset.notify.subject": "Geslo uspešno spremenjeno", - "reset.notify.text1": "Obveščamo vas, da je bilo na %1 uspešno spremenjeno vaše geslo.", - "reset.notify.text2": "Če niste dali te zahteve, prosimo nemudoma kontaktirajte administratorja.", - "digest.notifications": "Imate eno neprebrano obvestilo na %1:", - "digest.latest_topics": "Zadnja tema na %1", - "digest.cta": "Kliknite tu za obisk %1", - "digest.unsub.info": "Ta izvleček je bil poslan zaradi vaših nastavitev obvestil.", - "digest.no_topics": "V preteklosti ni bilo aktivnih tem na %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", - "notif.chat.subject": "Novo sporočilo prejeto na %1", + "reset.notify.subject": "Geslo je bilo uspešno spremenjeno", + "reset.notify.text1": "Obveščamo vas, da je bilo na forumu: %1 uspešno spremenjeno vaše geslo.", + "reset.notify.text2": "Če niste dali te zahteve, prosimo nemudoma obvestite skrbnika.", + "digest.notifications": "Imate neprebrana obvestila od %1:", + "digest.latest_topics": "Zadnja teme na forumu: %1", + "digest.cta": "Kliknite tu za obisk foruma: %1", + "digest.unsub.info": "Ta izvleček vam je bil poslan zaradi nastavitev vaše naročnine.", + "digest.no_topics": "%1, ki je pretekel ni imel aktivnih tem.", + "digest.day": "Dan", + "digest.week": "Teden", + "digest.month": "Mesec", + "digest.subject": "Povzetek za %1", + "notif.chat.subject": "Novo sporočilo klepeta prejeto na %1", "notif.chat.cta": "Kliknite tu za nadaljevanje pogovora", - "notif.chat.unsub.info": "Obvestilu o pogovoru je bilo poslano zaradi vaših nastavitev obvestil.", - "notif.post.cta": "Kliknite tu za celotno temo.", - "notif.post.unsub.info": "Obvestilo o odgovoru je bilo poslano zaradi vaših nastavitev obvestil.", - "test.text1": "To je testno elektronsko sporočilo za preverjanje prejemnika, če je pravilno nastavil NodeBB.", - "unsub.cta": "Kliknite tu za spremembo nastavitev.", + "notif.chat.unsub.info": "Obvestilo o klepetu vam je bilo poslano zaradi nastavitev vaše naročnine.", + "notif.post.cta": "Kliknite tu, če želite prebrati celotno temo.", + "notif.post.unsub.info": "Obvestilo o objavi je bilo poslano zaradi nastavitev vaše naročnine.", + "test.text1": "To je testno elektronsko sporočilo, služi preverjanju pravilnosti nastavite podsistema za pošiljanje NodeBB poštnih sporočil.", + "unsub.cta": "Klikni tu za spremembo nastavitev.", "closing": "Hvala!" } \ No newline at end of file diff --git a/public/language/sl/error.json b/public/language/sl/error.json index a0453bc488..edd3b4e0d6 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -1,91 +1,115 @@ { - "invalid-data": "Napačen podatek", + "invalid-data": "Napačni podatki", "not-logged-in": "Niste prijavljeni.", "account-locked": "Vaš račun je bil začasno zaklenjen.", - "search-requires-login": "Iskanje zahteva uporabniški račun - prosimo, da se prijavite oz. registrirate.", + "search-requires-login": "Iskanje zahteva uporabniški račun - prosimo, da se prijavite ali. registrirate.", "invalid-cid": "Napačen ID kategorije", "invalid-tid": "Napačen ID teme", "invalid-pid": "Napačen ID objave", "invalid-uid": "Napačen ID uporabnika", "invalid-username": "Napačno uporabniško ime", - "invalid-email": "Napačen e-mail naslov", + "invalid-email": "Napačen e-poštni naslov", "invalid-title": "Napačen naslov!", "invalid-user-data": "Napačni podatki o uporabniku", "invalid-password": "Napačno geslo", "invalid-username-or-password": "Prosimo vpišite uporabniško ime in geslo", - "invalid-search-term": "Napačna iskalna poizvedba", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "invalid-search-term": "Napačni iskalni izraz", + "csrf-invalid": "Prijava ni mogoča, verjetno zaradi potekle seje. Poskusite znova.", + "invalid-pagination-value": "Napačna vrednost za številčenje strani. Vrednost mora biti najmanj %1 in največ %2", "username-taken": "Uporabniško ime je že zasedeno", - "email-taken": "E-mail naslov je že zaseden", - "email-not-confirmed": "Vaš e-mail naslov še ni bil potrjen. Prosimo kliknite tu za potrditev vašega e-mail naslova.", - "email-not-confirmed-chat": "Ne morete klepetati dokler ne potrdite vašega e-mail naslova. Prosimo kliknite tu za potrditev vašega e-mail naslova.", - "no-email-to-confirm": "Ta forum zahteva potrjen e-mail naslov. Prosim kliknite tu za vnos e-mail naslova.", - "email-confirm-failed": "Nismo mogli potrditi vašega e-mail naslova. Prosimo poskusite ponovno.", - "confirm-email-already-sent": "Potrditveni e-mail naslov je že poslan. Prosimo počakajte %1 minut(o) za ponovno pošiljanje.", + "email-taken": "E-poštnio naslov je že zaseden", + "email-not-confirmed": "Vaš e-poštni naslov še ni bil potrjen. Prosimo kliknite tu za potrditev vašega e-poštnega naslova.", + "email-not-confirmed-chat": "Ne morete klepetati dokler ne potrdite vašega e-poštnega naslova. Prosimo kliknite tu za potrditev vašega e-poštnega naslova.", + "email-not-confirmed-email-sent": "Vaš e-poštni naslov še ni bil potrjen. Prosimo preverite vašo mapo prejeto.", + "no-email-to-confirm": "Ta forum zahteva potrjen e-poštni naslov. Prosim kliknite tu za vnos e-poštnega naslova.", + "email-confirm-failed": "Potrditi vašega e-poštnega naslova ni uspela. Prosimo poskusite ponovno.", + "confirm-email-already-sent": "Potrditveni e-sporočilo je že poslan. Prosimo počakajte %1 minut(o) za ponovno pošiljanje.", + "sendmail-not-found": "Ne najdem izvršljive datoteke za pošiljanje e-pošte. Prepričajte se da je ta nameščena in izvršljiva preko uporabnika, ki izvaja NodeBB", "username-too-short": "Uporabniško ime je prekratko", "username-too-long": "Uporabniško ime je predolgo", - "password-too-long": "Password too long", - "user-banned": "Uporabnik je blokiran", - "user-too-new": "Oprostite, počakajte %1 sekund pred vašo prvo objavo", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "password-too-long": "Geslo je predolgo", + "user-banned": "Uporabnik je izločen", + "user-banned-reason": "Uporabnik je izločen (Vzrok: %1)", + "user-too-new": " Pred vašo prvo objavo počakajte %1 sekund(o).", + "blacklisted-ip": "Vaš IP naslov je izločen. Vprašajte skrbnika za več informacij.", + "ban-expiry-missing": "Vnesite končni datum za to izločitev.", "no-category": "Kategorija ne obstaja", "no-topic": "Tema ne obstaja", "no-post": "Objava ne obstaja", "no-group": "Skupina ne obstaja", "no-user": "Uporabnik ne obstaja", "no-teaser": "Predogled ne obstaja", - "no-privileges": "Nimate dovolj pravic.", + "no-privileges": "Nimate dovolj pravic za to dejanje", "category-disabled": "Kategorija onemogočena", - "topic-locked": "Tema zaklenjena", - "post-edit-duration-expired": "Urejanje objave je dovoljeno le %1 sekund po prvi objavi.", + "topic-locked": "Tema je zaklenjena", + "post-edit-duration-expired": "Urejanje objave je dovoljeno le %1 sekund(o) po objavi.", + "post-edit-duration-expired-minutes": "Urejanje objave je dovoljeno le %1 minut(o) po objavi.", + "post-edit-duration-expired-minutes-seconds": "Urejanje objave je dovoljeno le %1 minut(o) in %2 sekund(o) po objavi.", + "post-edit-duration-expired-hours": "Urejanje objave je dovoljeno le %1 ur(o) po objavi.", + "post-edit-duration-expired-hours-minutes": "Urejanje objave je dovoljeno le %1 ur(o) in %2 minut(o) po objavi.", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "Teme, ki ima odgovor ni mogoče izbrisati.", + "cant-delete-topic-has-replies": "Število odgovorov, ko teme ni mogoče izbrisati: %1", "content-too-short": "Prosimo napišite daljšo objavo. Objave morajo vsebovati vsaj %1 znak(ov).", "content-too-long": "Prosimo napišite krajšo objavo. Objave ne smejo vsebovati več kot %1 znak(ov).", "title-too-short": "Prosimo vnesite daljši naslov. Naslovi morajo vsebovati vsaj %1 znak(ov).", "title-too-long": "Prosimo napišite krajši naslov. Naslovi ne smejo vsebovati več kot %1 znak(ov).", + "category-not-selected": "Category not selected.", "too-many-posts": "Objavljate lahko na vsake %1 sekund - prosimo počakajte pred novo objavo.", - "too-many-posts-newbie": "Kot nov uporabnik lahko objavljate le vsake %1 sekund, dokler ne dosežete vsaj %2 ugleda - prosimo počakajte pred novo objavo.", + "too-many-posts-newbie": "Kot nov uporabnik lahko objavljate le vsakih %1 sekund(o/i/e), dokler ne dosežete vsaj %2 ugled(a/e/ov) - prosimo počakajte pred novo objavo.", "tag-too-short": "Prosimo vnesite daljšo oznako. Oznake morajo vsebovati vsaj %1 znak(ov).", "tag-too-long": "Prosimo vnesite krajšo oznako. Oznake ne smejo vsebovati več kot %1 znak(ov).", "not-enough-tags": "Ni dovolj oznak. Tema mora vsebovati vsaj %1 znak(ov)", "too-many-tags": "Preveč oznak. Tema ne sme imeti več kot %1 znak(ov)", "still-uploading": "Prosimo počakajte, da se prenosi končajo.", "file-too-big": "Največja dovoljena velikost datoteke je %1 kB - prosimo naložite manjšo datoteko", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", - "cant-ban-other-admins": "Ne morete blokirati drugih administratorjev!", - "cant-remove-last-admin": "Ste edini administrator. Dodajte novega administratorja preden boste odstranili sebe.", + "guest-upload-disabled": "Gostom je prenašanje onemogočeno", + "already-favourited": "To objavo ste že zaznamovali", + "already-unfavourited": "To objavo ste že nezaznamovali ", + "cant-ban-other-admins": "Ne morete izločiti drugih skrbnikov!", + "cant-remove-last-admin": "Ste edini skrbnik. Preden se boste odstranili, dodajte novega skrbnika.", + "cant-delete-admin": "Pred brisanjem tega računa morate odstraniti skrbniške pravice", "invalid-image-type": "Nedovoljen format slike. Dovoljeni formati so: %1", - "invalid-image-extension": "Nedovoljena končnica slike", - "invalid-file-type": "Nedovoljen format datoteke. Dovoljeni formati so: %1", + "invalid-image-extension": "Nedovoljena pripona slike", + "invalid-file-type": "Nedovoljena vrsta datoteke. Dovoljene vrste so: %1", "group-name-too-short": "Ime skupine je prekratko", - "group-already-exists": "Skupina še obstaja", + "group-name-too-long": "Ime skupine je predolgo", + "group-already-exists": "Skupina že obstaja", "group-name-change-not-allowed": "Sprememba imena skupine ni dovoljena", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", + "group-already-member": "Že član te skupine", + "group-not-member": "Ni član te skupine", "group-needs-owner": "Ta skupina potrebuje vsaj enega skrbnika", "group-already-invited": "Ta uporabnik je že bil povabljen", "group-already-requested": "Vaša prošnja za članstvo je že bila sprejeta.", "post-already-deleted": "Ta objava je že bila izbrisana", - "post-already-restored": "Ta objava je že bila razveljavljena", + "post-already-restored": "Ta objava je že bila obnovljena", "topic-already-deleted": "Ta tema je že bila izbrisana", - "topic-already-restored": "Ta tema je že bila razveljavljena", + "topic-already-restored": "Ta tema je že bila obnovljena", "cant-purge-main-post": "Ne morete odstraniti prve objave, prosimo izbrišite temo.", "topic-thumbnails-are-disabled": "Sličice teme so onemogočene.", "invalid-file": "Nedovoljena datoteka", - "uploads-are-disabled": "Nalaganje je onemogočeno", + "uploads-are-disabled": "Prenosi so onemogočeni", "signature-too-long": "Vaš podpis ne sme biti daljši od %1 znak(ov).", "about-me-too-long": "Rubrika \"O meni\" ne sme biti daljša od %1 znak(ov).", "cant-chat-with-yourself": "Ne morete klepetati s seboj!", - "chat-restricted": "Uporabnik je omejil klepetanje. Za možnost klepetanja vas mora uporabnik slediti", - "chat-disabled": "Chat system disabled", + "chat-restricted": "Uporabnik je omejil klepetanje. Za možnost klepetanja vas mora uporabnik spremljati", + "chat-disabled": "Klepet je onemogočen", "too-many-messages": "Poslali ste veliko število sporočil, prosimo počakajte nekaj časa.", - "invalid-chat-message": "Invalid chat message", - "chat-message-too-long": "Chat message is too long", - "cant-edit-chat-message": "You are not allowed to edit this message", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", - "reputation-system-disabled": "Možnost ugleda je onemogočena.", + "invalid-chat-message": "Neveljavno sporočilo klepeta", + "chat-message-too-long": "Sporočilo klepeta je predolgo", + "cant-edit-chat-message": "Nimate dovoljenja za urejanje tega sporočila", + "cant-remove-last-user": "Zadnjega uporabnika ne morete odstraniti", + "cant-delete-chat-message": "NImate dovoljenja za izbris tega sporočila", + "already-voting-for-this-post": "Za to objavo ste že volili", + "reputation-system-disabled": "Sistem za ugled je onemogočen.", "downvoting-disabled": "Negativno glasovanje je onemogočeno", "not-enough-reputation-to-downvote": "Nimate dovolj ugleda za negativno glasovanje", "not-enough-reputation-to-flag": "Nimate dovolj ugleda za prijavo te objave", @@ -93,11 +117,15 @@ "reload-failed": "NodeBB je zaznal težavo pri osveževanju: ", "registration-error": "Napaka pri registraciji", "parse-error": "Nekaj je šlo narobe pri pridobivanju odgovora s strežnika", - "wrong-login-type-email": "Uporabite svoj e-mail naslov za prijavo", + "wrong-login-type-email": "Uporabite svoj e-poštni naslov za prijavo", "wrong-login-type-username": "Uporabite svoje uporabniško ime za prijavo", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "invite-maximum-met": "Povabili ste največje dovoljeno število ljudi (%1 od %2)", + "no-session-found": "Prijavne seje ni mogoče najti!", + "not-in-room": "Uporabnik ni v sobi", + "no-users-in-room": "V tej sobi ni uporabnikov", + "cant-kick-self": "Sami sebe ne morete umakniti iz skupine", + "no-users-selected": "Noben uporabnik ni izbran", + "invalid-home-page-route": "Napačna pot do domače strani", + "invalid-session": "Seje se ne ujemajo", + "invalid-session-text": "Kaže da vaša prijavna seja ni več aktiva. Prosim osvežite to stran." } \ No newline at end of file diff --git a/public/language/sl/global.json b/public/language/sl/global.json index c8ba8fe3d2..ef0910e47b 100644 --- a/public/language/sl/global.json +++ b/public/language/sl/global.json @@ -5,90 +5,98 @@ "403.title": "Dostop zavrnjen", "403.message": "Kot kaže ste naleteli na stran, za katero nimate dovoljenja.", "403.login": "Morda bi se raje <a href='%1/login'>prijavili</a>?", - "404.title": "Ni mogoče najti", + "404.title": "Ni bilo mogoče najti", "404.message": "Kot kaže ste naleteli na stran, ki ne obstaja. Vrnite se na <a href='%1/'>začetno stran</a>.", - "500.title": "Notranja napaka.", + "500.title": "Internal Error.", "500.message": "Ups! Nekaj je šlo narobe!", + "400.title": "Napačna zahteva", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registracija", "login": "Prijava", - "please_log_in": "Prosimo prijavite se", + "please_log_in": "Prijavite se", "logout": "Odjava", - "posting_restriction_info": "Objavljanje je trenutno omogočeno le ga člane, kliknite tu za prijavo.", - "welcome_back": "Pozdravljeni nazaj", + "posting_restriction_info": "Objavljanje je trenutno omogočeno le za registrirane člane, kliknite tu za prijavo.", + "welcome_back": "Dobrodošli nazaj", "you_have_successfully_logged_in": "Uspešno ste se prijavili", "save_changes": "Shrani spremembe", + "save": "Save", "close": "Zapri", "pagination": "Oštevilčenje strani", "pagination.out_of": "%1 od %2", "pagination.enter_index": "Vnesi številko", - "header.admin": "Administrator", + "header.admin": "Skrbnik", "header.categories": "Kategorije", "header.recent": "Nedavno", "header.unread": "Neprebrano", "header.tags": "Oznake", - "header.popular": "Popularno", + "header.popular": "Priljubljeno", "header.users": "Uporabniki", "header.groups": "Skupine", "header.chats": "Klepet", "header.notifications": "Obvestila", "header.search": "Iskanje", "header.profile": "Profil", - "header.navigation": "Navigacija", - "notifications.loading": "Nalagam obvestila", - "chats.loading": "Nalagam klepet", + "header.navigation": "Krmarjenje", + "notifications.loading": "Nalaganje obvestil", + "chats.loading": "Nalaganje klepetov", "motd.welcome": "Pozdravljeni v NodeBB, pogovorna platforma prihodnosti.", "previouspage": "Prejšnja stran", "nextpage": "Naslednja stran", "alert.success": "Uspešno", "alert.error": "Napaka", - "alert.banned": "Blokiran", - "alert.banned.message": "Vaš uporabniški račun je blokiran, sedaj boste odjavljeni.", + "alert.banned": "Izločen", + "alert.banned.message": "Vaš uporabniški račun je izločen, sedaj boste odjavljeni.", "alert.unfollow": "Ne sledite več %1!", - "alert.follow": "Sledite %1!", + "alert.follow": "Sedaj spremljate %1!", "online": "Dosegljiv", "users": "Uporabniki", "topics": "Teme", "posts": "Objave", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", - "views": "Ogledi", + "best": "Najboljše", + "upvoters": "Glasovalcev za", + "upvoted": "Glasov za", + "downvoters": "Glasovalcev proti", + "downvoted": "Glasov proti", + "views": "Ogledov", "reputation": "Ugled", "read_more": "preberi več", "more": "Več", "posted_ago_by_guest": "objavil %1 kot Gost", - "posted_ago_by": "objavil %1 kot %2", + "posted_ago_by": "objavljeno %1 od %2", "posted_ago": "objavil %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", + "posted_in": "objavljeno v %1", + "posted_in_by": "objavljeno v %1 od %2", "posted_in_ago": "objavljeno v %1 %2", - "posted_in_ago_by": "objavil v %1 %2 kot %3", + "posted_in_ago_by": "objavljeno v %1 %2 od %3", "user_posted_ago": "%1 objavil %2", "guest_posted_ago": "Gost je objavil %1", - "last_edited_by": "last edited by %1", + "last_edited_by": "Zadnji urejal %1", "norecentposts": "Ni nedavnih objav", "norecenttopics": "Ni nedavnih tem", - "recentposts": "Zadnje objave", + "recentposts": "Nedavne objave", "recentips": "Nedavni IP-ji ob prijavah", "away": "Odsoten", "dnd": "Ne moti", "invisible": "Neviden", "offline": "Nedosegljiv", - "email": "E-mail", + "email": "E-pošta", "language": "Jezik", "guest": "Gost", "guests": "Gosti", - "updated.title": "Forum posodobljen", - "updated.message": "Forum je bil posodobljen na zadnjo verzijo. Kliknite tu za osvežitev strani.", + "updated.title": "Forum je posodobljen", + "updated.message": "Forum je bil pravkar posodobljen na zadnjo različico. Kliknite tu za osvežitev strani.", "privacy": "Zasebnost", - "follow": "Sledi", - "unfollow": "Prenehaj slediti", + "follow": "Spremljaj", + "unfollow": "Prekliči spremljanje", "delete_all": "Izbriši vse", - "map": "Map", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "map": "Zemljevid", + "sessions": "Prijavnih sej", + "ip_address": "Naslov IP", + "enter_page_number": "Vnesi številko strani", + "upload_file": "Prenesi datoteko", + "upload": "Prenos", + "allowed-file-types": "Dovoljene vrste datotek so %1", + "unsaved-changes": "Imate neshranjene spremembe. A res želite stran?", + "reconnecting-message": "Kot kaže je bila povezava do %1 prekinjena. Počakajte, ponovno poskušamo vzpostaviti povezavo.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/sl/groups.json b/public/language/sl/groups.json index bb5af04f68..5a62abee98 100644 --- a/public/language/sl/groups.json +++ b/public/language/sl/groups.json @@ -1,7 +1,7 @@ { "groups": "Skupine", "view_group": "Poglej skupino", - "owner": "Avtor skupine", + "owner": "Lastnik skupine", "new_group": "Ustvari novo skupino", "no_groups_found": "Ni skupin", "pending.accept": "Potrdi", @@ -24,8 +24,8 @@ "details.has_no_posts": "Člani skupine še niso ustvarili objave.", "details.latest_posts": "Zadnje objave", "details.private": "Zasebno", - "details.disableJoinRequests": "Disable join requests", - "details.grant": "Odobri/prekliči avtorstvo", + "details.disableJoinRequests": "Onemogoči zahteve po pridružitvi", + "details.grant": "Dodeli/Prekliči lastništvo", "details.kick": "Odstrani člana iz skupine", "details.owner_options": "Administratorji skupine", "details.group_name": "Ime skupine", @@ -37,11 +37,11 @@ "details.change_colour": "Zamenjaj barvo", "details.badge_text": "Ime značke", "details.userTitleEnabled": "Pokaži značko", - "details.private_help": "Za pridružitev boste morda potrebovali dovoljenje ustanovitelja skupine", + "details.private_help": "Če je omogočeno bo pridružitev skupini zahtevala odobritev lastnika skupine", "details.hidden": "Skrito", "details.hidden_help": "Skupina je verjetno skrita pred uporabniki, zato se lahko vanjo pridružijo zgolj tisti s povabilom", "details.delete_group": "Izbriši skupino", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Zasebne skupine so onemogočene na sistemskem nivoju, ta možnost tako nima učinka", "event.updated": "Podatki o skupini so bili posodobljeni", "event.deleted": "Skupina %1 je bila izbrisana", "membership.accept-invitation": "Sprejmi povabilo", @@ -50,5 +50,7 @@ "membership.leave-group": "Zapusti skupino", "membership.reject": "Zavrni", "new-group.group_name": "Ime skupine:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/sl/login.json b/public/language/sl/login.json index 6955d274bd..ff4ca4b5e6 100644 --- a/public/language/sl/login.json +++ b/public/language/sl/login.json @@ -1,11 +1,12 @@ { - "username-email": "Uporabniško ime / E-poštni naslov", + "username-email": "Uporabniško ime / E-pošta", "username": "Uporabniško ime", - "email": "E-poštni naslov", + "email": "E-pošta", "remember_me": "Spomni me?", - "forgot_password": "Pozabljeno geslo?", - "alternative_logins": "Prijava z drugim računom", - "failed_login_attempt": "Napačna prijava, prosim poskusi ponovno.", - "login_successful": "Prijava je uspešna.", - "dont_have_account": "Ali še nimaš uporabniškega računa?" + "forgot_password": "Ste pozabili geslo?", + "alternative_logins": "Alternativne prijave", + "failed_login_attempt": "Prijava ni uspela", + "login_successful": "Uspešno ste se prijavili.", + "dont_have_account": "Ali še nimaš uporabniškega računa?", + "logged-out-due-to-inactivity": "Zaradi neaktivnsoti ste odjavljeni iz ACP - Skrbniške nadzorne plošče" } \ No newline at end of file diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json index 911a9f1588..d1557150bb 100644 --- a/public/language/sl/modules.json +++ b/public/language/sl/modules.json @@ -6,33 +6,43 @@ "chat.user_typing": "%1 piše sporočilo...", "chat.user_has_messaged_you": "%1 ti je napisal/a sporočilo.", "chat.see_all": "Poglej vse klepete", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Označi vse klepete kot prebrane", "chat.no-messages": "Za pogled zgodovine klepeta izberi prejemnika", - "chat.no-users-in-room": "No users in this room", + "chat.no-users-in-room": "V tej sobi ni uporabnikov", "chat.recent-chats": "Zadnji klepeti", - "chat.contacts": "Kontakti", + "chat.contacts": "Stiki", "chat.message-history": "Zgodovina klepeta", "chat.pop-out": "Klepet v novem oknu", "chat.maximize": "Povečaj", "chat.seven_days": "7 dni", "chat.thirty_days": "30 dni", "chat.three_months": "3 mesece", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", - "composer.compose": "Ustvari", + "chat.delete_message_confirm": "Ali ste prepričani, da želite izbrisati to sporočilo?", + "chat.roomname": "Soba za klepet %1", + "chat.add-users-to-room": "Dodaj uporabnike v sobo", + "composer.compose": "Sestavljanje", "composer.show_preview": "Pokaži predogled", "composer.hide_preview": "Skrij predogled", "composer.user_said_in": "%1 je napisal/a v %2:", "composer.user_said": "%1 je napisal/a:", - "composer.discard": "Ste prepričani, da želite opustiti to objavo?", + "composer.discard": "Ste prepričani, da želite zavreči to objavo?", "composer.submit_and_lock": "Pošlji in zakleni", "composer.toggle_dropdown": "Preklopi spustni meni", - "composer.uploading": "Uploading %1", - "bootbox.ok": "Vredu", + "composer.uploading": "Prenašanje %1", + "composer.formatting.bold": "Krepko", + "composer.formatting.italic": "Ležeče", + "composer.formatting.list": "Seznam", + "composer.formatting.strikethrough": "Prečrtano", + "composer.formatting.link": "Povezava", + "composer.formatting.picture": "Slika", + "composer.upload-picture": "Prenesi sliko\t", + "composer.upload-file": "Prenesi datoteko", + "composer.zen_mode": "Zen način", + "composer.select_category": "Select a category", + "bootbox.ok": "V redu", "bootbox.cancel": "Prekliči", "bootbox.confirm": "Potrdi", - "cover.dragging_title": "Cover Photo Positioning", - "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", - "cover.saved": "Cover photo image and position saved" + "cover.dragging_title": "Določanje položaja naslovne fotografije ", + "cover.dragging_message": "Povleci sliko na željen položaj in klikni \"Shrani\" ", + "cover.saved": "Naslovna fotografija in položaj shranjena" } \ No newline at end of file diff --git a/public/language/sl/notifications.json b/public/language/sl/notifications.json index 37711ea13d..218f17fca9 100644 --- a/public/language/sl/notifications.json +++ b/public/language/sl/notifications.json @@ -5,34 +5,31 @@ "mark_all_read": "Označi vsa obvestila kot prebrana", "back_to_home": "Nazaj na %1", "outgoing_link": "Odhodna povezava", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "Sedaj zapuščate %1", "continue_to": "Nadaljujte na %1", "return_to": "Vrnite se na %1", "new_notification": "Novo obvestilo", "you_have_unread_notifications": "Imate neprebrana obvestila.", "new_message_from": "Novo obvestilo od <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> je glasoval za vašo objavo v <strong>%2</strong>.", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", - "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", - "user_flagged_post_in": "<strong>%1</strong>je prijavil vašo objavo v <strong>%2</strong>", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", - "user_posted_to": "<strong>%1</strong> je odgovoril na: <strong>%2</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", + "upvoted_your_post_in_dual": "<strong>%1</strong> in <strong>%2</strong> sta glasovala za vašo objavo v <strong>%3</strong>.", + "upvoted_your_post_in_multiple": "<strong>%1</strong> in %2 drugih je glasovalo za vašo objavo v <strong>%3</strong>.", + "moved_your_post": "<strong>%1</strong> je premaknil vašo objavo v <strong>%2</strong>", + "moved_your_topic": "<strong>%1</strong> je premaknil <strong>%2</strong>", + "user_flagged_post_in": "<strong>%1</strong>je označil vašo objavo v <strong>%2</strong>", + "user_flagged_post_in_dual": "<strong>%1</strong> in <strong>%2</strong> sta vašo objavo označila v <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 drugih je označilo vašo objavo v <strong>%3</strong>", + "user_posted_to": "<strong>%1</strong> je objavil odgovor na: <strong>%2</strong>", + "user_posted_to_dual": "<strong>%1</strong> in <strong>%2</strong> sta objavila odgovor na: <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> in %2 drugi so objavili odgovor na: <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> je odprl novo temo: <strong>%2</strong>", - "user_started_following_you": "<strong>%1</strong> te sledi.", - "user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", - "new_register": "<strong>%1</strong> je poslal prošnjo za registracijo.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", - "email-confirmed": "E-mail naslov potrjen", + "user_started_following_you": "<strong>%1</strong> ti je začel slediti.", + "user_started_following_you_dual": "<strong>%1</strong> in <strong>%2</strong> sta ti začela slediti.", + "user_started_following_you_multiple": "<strong>%1</strong> in %2 drugih ti je začelo slediti.", + "new_register": "<strong>%1</strong> je poslal zahtevo za registracijo.", + "new_register_multiple": "Število registracijskih zahtev, ki čakajo pregled: <strong>%1</strong>", + "email-confirmed": "E-poštni naslov potrjen", "email-confirmed-message": "Hvala ker ste potrdili svoj naslov. Račun je sedaj aktiviran.", - "email-confirm-error-message": "Prišlo je do napake pri preverjanju vašega e-mail naslova. Morda je bila koda napačna ali pa je potekla.", - "email-confirm-sent": "Potrditveni e-mail je poslan." + "email-confirm-error-message": "Prišlo je do napake pri preverjanju vašega e-poštnega naslova. Morda je bila koda napačna ali pa je potekla.", + "email-confirm-sent": "Potrditveno e-sporočilo je poslano." } \ No newline at end of file diff --git a/public/language/sl/pages.json b/public/language/sl/pages.json index 3c206aadf4..e844293767 100644 --- a/public/language/sl/pages.json +++ b/public/language/sl/pages.json @@ -1,46 +1,49 @@ { "home": "Domov", "unread": "Neprebrane teme", - "popular-day": "Današnje popularne teme", - "popular-week": "Popularne teme tega tedna", - "popular-month": "Popularne teme tega meseca", - "popular-alltime": "Najbolj popularne teme vseh časov", + "popular-day": "Priljubljene teme danes", + "popular-week": "Priljubljene teme v tem tednu", + "popular-month": "Priljubljene teme v tem mesecu", + "popular-alltime": "Priljubljene teme neglede na čas", "recent": "Zadnje teme", - "flagged-posts": "Flagged Posts", - "users/online": "Uporabnikov na strani", + "flagged-posts": "Označene teme", + "users/online": "Dosegljivi uporabniki", "users/latest": "Zadnji uporabniki", "users/sort-posts": "Uporabniki z največ objavami", - "users/sort-reputation": "Uporabniki najbolj vredni zaupanja", - "users/banned": "Banned Users", + "users/sort-reputation": "Uporabniki z največjim ugledom", + "users/banned": "Izločeni uporabniki", + "users/most-flags": "Največkrat označeni uporabniki", "users/search": "Iskanje uporabnikov", "notifications": "Obvestila", "tags": "Oznake", - "tag": "Teme z oznakami \"%1\"", + "tag": "Teme označene kot \"%1\"", "register": "Registriraj svoj račun", + "registration-complete": "Registration complete", "login": "Prijavi se v svoj račun", - "reset": "Ponastavi geslo", + "reset": "Ponastavi geslo svojega računa", "categories": "Kategorije", "groups": "Skupine", "group": "Skupina %1", "chats": "Klepet", "chat": "Klepet z %1", "account/edit": "Urejanje \"%1\"", - "account/edit/password": "Editing password of \"%1\"", - "account/edit/username": "Editing username of \"%1\"", - "account/edit/email": "Editing email of \"%1\"", + "account/edit/password": "Urejanje gesla za \"%1\"", + "account/edit/username": "Urejanje uporabniškega imena za \"%1\"", + "account/edit/email": "Urejanje e-pošte za \"%1\"", + "account/info": "Podatki o računu", "account/following": "Ljudje, ki jih sledi %1", "account/followers": "Ljudje, ki sledijo %1", "account/posts": "Objave uporabnika %1", - "account/topics": "Ustvarjene teme uporabnika %1", - "account/groups": "Teme uporabnika %1", - "account/favourites": "%1's Bookmarked Posts", + "account/topics": "Ustvarjene teme od uporabnika %1", + "account/groups": "Skupine od %1", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Uporabniške nastavitve", - "account/watched": "Teme, ki jih spremlja uporabnik %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", - "maintenance.text": "%1 je trenutno v prenovi. Prosimo pridite nazaj kasneje.", - "maintenance.messageIntro": "Administrator obvešča:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "account/watched": "Teme, ki jih spremlja %1", + "account/upvoted": "%1 objav z glasovi za", + "account/downvoted": "%1 objav z glasovi proti", + "account/best": "Najboljše objave od %1", + "confirm": "E-pošta potrjena", + "maintenance.text": "%1 je v vzdrževanju.", + "maintenance.messageIntro": "Dodatno vam je skrbnik pustil tole sporočilo:", + "throttled.text": "Storitev %1 trenutno je zaradi obremenitve nedosegljiva. Vrnite se pozneje." } \ No newline at end of file diff --git a/public/language/sl/recent.json b/public/language/sl/recent.json index 924b7e7b6c..fcb58dde23 100644 --- a/public/language/sl/recent.json +++ b/public/language/sl/recent.json @@ -13,7 +13,7 @@ "there-are-new-topics": "%1 novih tem.", "there-are-new-topics-and-a-new-post": "%1 novih tem in nova objava.", "there-are-new-topics-and-new-posts": "%1 novih tem in %2 novih objav", - "there-is-a-new-post": "Nova objava.", + "there-is-a-new-post": "Objavljena je nova objava.", "there-are-new-posts": "%1 novih objav.", "click-here-to-reload": "Kliknite tu za osvežitev." } \ No newline at end of file diff --git a/public/language/sl/register.json b/public/language/sl/register.json index e2dcbfb0f6..4c70709d09 100644 --- a/public/language/sl/register.json +++ b/public/language/sl/register.json @@ -1,8 +1,9 @@ { "register": "Registracija", - "help.email": "Tvoj e-poštni naslov bo skrit po privzetih nastavitvah.", - "help.username_restrictions": "Unikatno uporabniško ime med %1 in %2 znakov. Drugi vas lahko omenijo v objavi z @<span id='yourUsername'>Uporabnik</span>.", - "help.minimum_password_length": "Tvoje geslo mora vsebovati najmanj %1 znakov.", + "cancel_registration": "Preklic registracije", + "help.email": "Privzeto bo vaš e-poštni naslov skrit pred javnostjo.", + "help.username_restrictions": "Enolično uporabniško ime med %1 in %2 znaki. Drugi vas lahko omenijo v objavi z @<span id='yourUsername'>uporabnik</span>.", + "help.minimum_password_length": "Vaše geslo mora vsebovati najmanj %1 znakov.", "email_address": "E-poštni naslov", "email_address_placeholder": "Vnesi e-mail naslov", "username": "Uporabniško ime", @@ -15,5 +16,8 @@ "alternative_registration": "Alternativna registracija", "terms_of_use": "Pogoji uporabe", "agree_to_terms_of_use": "Strinjam se s pogoji uporabe", - "registration-added-to-queue": "Registracija uporabniškega profila je v postopku. Ob potrditvi s strani administratorja boste prejeli sporočilo v vaš e-poštni predal." + "terms_of_use_error": "S pogoji uporabe se morate strinjati", + "registration-added-to-queue": "Registracija uporabniškega profila je v postopku. Ob potrditvi s strani skrbnika boste prejeli sporočilo v vaš e-poštni predal.", + "interstitial.intro": "Preden ustvarimo vaš račun zahtevamo nekaj dodatnih informacij.", + "interstitial.errors-found": "Vaše registracije ne moremo dokončati:" } \ No newline at end of file diff --git a/public/language/sl/tags.json b/public/language/sl/tags.json index 4fbbab14d8..50e7944fde 100644 --- a/public/language/sl/tags.json +++ b/public/language/sl/tags.json @@ -1,7 +1,7 @@ { "no_tag_topics": "Ni novih tem s to oznako.", "tags": "Oznake", - "enter_tags_here": "Tu vpišite oznake, med %1 in %2 znaki.", + "enter_tags_here": "Tu vpišite oznake. Dovoljeno število znakov: najmanj %1 in največ %2.", "enter_tags_here_short": "Vpišite oznake...", - "no_tags": "Ni še oznak." + "no_tags": "Oznak še ni." } \ No newline at end of file diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json index 503f7ee250..cdab1a601e 100644 --- a/public/language/sl/topic.json +++ b/public/language/sl/topic.json @@ -13,7 +13,7 @@ "notify_me": "Bodi obveščen o novih odgovorih na to temo", "quote": "Citiraj", "reply": "Odgovori", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "Odgovori s temo", "guest-login-reply": "Prijavi se za odgovor", "edit": "Uredi", "delete": "Izbriši", @@ -24,37 +24,61 @@ "link": "Povezava", "share": "Deli", "tools": "Orodja", - "flag": "Prijavi", + "flag": "Označi", "locked": "Zaklenjeno", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", - "flag_title": "Prijavi to objavo v pregled administratorju", - "flag_success": "Ta objava je bila prijavljena v pregled administratorju.", - "deleted_message": "Ta tema je bila izbrisana. Le uporabniki s pravicami teme jo lahko vidijo.", - "following_topic.message": "Sedaj boste dobili obvestila, ko bo nekdo objavil v to temo.", - "not_following_topic.message": "Ne boste več prejemali obvestil s te teme.", - "login_to_subscribe": "Prosimo prijavite ali registrirajte se za naročanje o tej temi.", + "pinned": "Pripeto", + "moved": "Premaknjeno", + "bookmark_instructions": "Klikni tukaj za vrnitev na zadnje prebrano objavo v tej niti", + "flag_title": "Označi to objavo za vodenje", + "flag_success": "Ta objava je bila označena za vodenje.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", + "deleted_message": "Ta tema je bila izbrisana. Le uporabniki s pravicami upravljanja tem jo lahko vidijo.", + "following_topic.message": "Če nekdo objavi v to temo, boste od sedaj dobivali obvestila. ", + "not_following_topic.message": "To temo boste videli v seznamu neprebranih tem. Ne boste pa dobili obvestila, če bo nekdo objavil v tej temi. ", + "ignoring_topic.message": "To temo ne boste videli v seznamu neprebranih tem. Prav tako ne boste dobili obvestila, če bo nekdo objavil v tej temi. ", + "login_to_subscribe": "Če se želite naročiti na to temo se morate najprej prijaviti oziroma opraviti registracijo.", "markAsUnreadForAll.success": "Tema označena kot neprebrana za vse.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "Označi kot neprebrano", + "mark_unread.success": "Tema označena kot neprebrana.", "watch": "Spremljaj", "unwatch": "Ne spremljaj", "watch.title": "Bodi obveščen o novih odgovorih v tej temi", "unwatch.title": "Prenehaj spremljati to temo", "share_this_post": "Deli to objavo", + "watching": "Spremljano", + "not-watching": "Ni spremljano", + "ignoring": "Prezri", + "watching.description": "Obvesti me o novih odgovorih.<br/>Teme prikaži v Neprebrano.", + "not-watching.description": "Ne obvesti me o novih odgovorih.<br/>Teme prikaži v Neprebrano le če kategorija ni prezrta.", + "ignoring.description": "Ne obvesti me o novih odgovorih.<br/>Teme ne prikaži v Neprebrano.", "thread_tools.title": "Orodja teme", - "thread_tools.markAsUnreadForAll": "Označi kot neprebrano", - "thread_tools.pin": "Prilepi temo", - "thread_tools.unpin": "Odlepi temo", + "thread_tools.markAsUnreadForAll": "Označi vse kot neprebrano", + "thread_tools.pin": "Pripni temo", + "thread_tools.unpin": "Odpni temo", "thread_tools.lock": "Zakleni temo", "thread_tools.unlock": "Odkleni temo", - "thread_tools.move": "Prestavi temo", - "thread_tools.move_all": "Prestavi vse", + "thread_tools.move": "Premakni temo", + "thread_tools.move_all": "Premakni vse", "thread_tools.fork": "Razcepi temo", "thread_tools.delete": "Izbriši temo", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "Izbriši objave", "thread_tools.delete_confirm": "Ste prepričani, da želite izbrisati to temo?", - "thread_tools.restore": "Razveljavi temo", - "thread_tools.restore_confirm": "Ste prepričani, da želite razveljaviti to temo?", + "thread_tools.restore": "Obnovi temo", + "thread_tools.restore_confirm": "Ste prepričani, da želite obnoviti to temo?", "thread_tools.purge": "Očisti temo", "thread_tools.purge_confirm": "Ste prepričani, da želite očistiti to temo?", "topic_move_success": "Ta tema je bila uspešno prestavljena v %1", @@ -65,9 +89,9 @@ "disabled_categories_note": "Onemogočene kategorije so obarvane sivo", "confirm_move": "Premakni", "confirm_fork": "Razcepi", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Zaznamek", + "favourites": "Zaznamki", + "favourites.has_no_favourites": "Zaznamovali še niste nobenih objav.", "loading_more_posts": "Nalagam več objav", "move_topic": "Premakni temo", "move_topics": "Premakni teme", @@ -77,11 +101,12 @@ "topic_will_be_moved_to": "Ta tema bo premaknjena v to kategorijo", "fork_topic_instruction": "Klikni na objavo, ki o želiš odcepiti", "fork_no_pids": "Ni izbranih objav!", + "fork_pid_count": "Izbranih objav: %1 ", "fork_success": "Uspešno ste razcepili temo! Klikni tu za ogled te teme.", - "delete_posts_instruction": "Click the posts you want to delete/purge", + "delete_posts_instruction": "Kliknite na teme, ki jih želite izbrisati/očistiti ", "composer.title_placeholder": "Vpiši naslov teme...", "composer.handle_placeholder": "Ime", - "composer.discard": "Zavrži", + "composer.discard": "Zavrzi", "composer.submit": "Pošlji", "composer.replying_to": "Odgovor na %1", "composer.new_topic": "Nova tema", @@ -92,21 +117,21 @@ "composer.thumb_file_label": "Ali naložite datoteko", "composer.thumb_remove": "Počisti polja", "composer.drag_and_drop_images": "Primite in spustite slike tukaj", - "more_users_and_guests": "%1 uporabnik(ov) in %2 Gost(ov)", - "more_users": "%1 uporabnik(ov)", + "more_users_and_guests": "%1 uporabnik(a/i/ov) in %2 Gost(a/i/ov)", + "more_users": "%1 uporabnik(a/i/ov)", "more_guests": "%1 Gost(ov)", - "users_and_others": "%1 in %2 drugih", + "users_and_others": "%1 in %2 drugi(h)", "sort_by": "Razvrsti po", "oldest_to_newest": "Od starejšega do novejšega", "newest_to_oldest": "Od novejšega do starejšega", "most_votes": "Največ glasov", "most_posts": "Največ objav", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", - "link_back": "Re: [%1](%2)", - "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "stale.title": "Raje ustvari novo temo?", + "stale.warning": "Tema na katero odgovarjaš je precej stara. A ne bi raje ustvaril novo temo namesto te, z sklicem na to v tvojem odgovoru?", + "stale.create": "Ustvari novo temo", + "stale.reply_anyway": "Vseeno odgovori na to temo", + "link_back": "Odg: [%1](%2)", + "spam": "Neželeno", + "offensive": "Žaljivo", + "custom-flag-reason": "Vstavi vzrok označevanja" } \ No newline at end of file diff --git a/public/language/sl/unread.json b/public/language/sl/unread.json index 73f87fdfe1..623463a299 100644 --- a/public/language/sl/unread.json +++ b/public/language/sl/unread.json @@ -6,5 +6,8 @@ "selected": "Izbrano", "all": "Vse", "all_categories": "Vse kategorije", - "topics_marked_as_read.success": "Teme označene kot prebrane!" + "topics_marked_as_read.success": "Teme označene kot prebrane!", + "all-topics": "Vse teme", + "new-topics": "Nove teme", + "watched-topics": "Spremljane teme" } \ No newline at end of file diff --git a/public/language/sl/uploads.json b/public/language/sl/uploads.json index 1622cb5693..85ee20d0c2 100644 --- a/public/language/sl/uploads.json +++ b/public/language/sl/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Prenašanje datoteke ...", + "select-file-to-upload": "Izberete datoteko, ki jo želite prenesti!", + "upload-success": "Datoteka je bila uspešno prenesena!", + "maximum-file-size": "Največ %1 kb " } \ No newline at end of file diff --git a/public/language/sl/user.json b/public/language/sl/user.json index 2f13808e59..a7d13e5511 100644 --- a/public/language/sl/user.json +++ b/public/language/sl/user.json @@ -1,18 +1,19 @@ { - "banned": "Blokirani", + "banned": "Izločen", "offline": "Odjavljeni", "username": "Uporabniško ime", "joindate": "Datum pridružitve", "postcount": "Število objav", "email": "E-pošta", "confirm_email": "Potrdi e-poštni naslov", - "ban_account": "Blokiraj račun", - "ban_account_confirm": "Ali želiš blokirati uporabnika?", - "unban_account": "Odblokiraj račun", + "account_info": "Podatki računa", + "ban_account": "Izločen račun", + "ban_account_confirm": "Ali želiš izločiti uporabnika?", + "unban_account": "Ponovno vključi račun", "delete_account": "Izbriši račun", "delete_account_confirm": "Ali želiš izbrisati račun? <br /><strong>S potrditvijo bodo izbrisani vsi podatki, ki jih ne bo več možno obnoviti.</strong><br /><br />Vpiši svoje uporabniško ime za dokončanje procesa.", "delete_this_account_confirm": "Ali želiš izbrisati račun? <br /><strong>S potrditvijo bodo izbrisani vsi podatki, ki jih ne bo več možno obnoviti.</strong><br /><br />", - "account-deleted": "Account deleted", + "account-deleted": "Račun je izbrisan", "fullname": "Ime in priimek", "website": "Spletna stran", "location": "Lokacija", @@ -22,25 +23,25 @@ "profile": "Profil", "profile_views": "Ogledi", "reputation": "Naziv", - "favourites": "Bookmarks", - "watched": "Zgodovina ogledov", - "followers": "Sledilci", - "following": "Sledim", + "favourites": "Zaznamki", + "watched": "Spremljano", + "followers": "Spremljevalci", + "following": "Spremljano", "aboutme": "O meni", "signature": "Podpis", "birthday": "Rojstni datum", "chat": "Klepet", - "chat_with": "Chat with %1", + "chat_with": "Klepet z %1", "follow": "Spremljaj", "unfollow": "Ne spremljaj", "more": "Več", - "profile_update_success": "Prosil je bil uspešno posodobljen.", + "profile_update_success": "Profil je bil uspešno posodobljen.", "change_picture": "Spremeni sliko", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Spremeni uporabniško ime", + "change_email": "Spremeni e-poštni naslov", "edit": "Uredi", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Uredi profil", + "default_picture": "Privzeta ikona", "uploaded_picture": "Naloži fotografijo", "upload_new_picture": "Naloži novo fotografijo", "upload_new_picture_from_url": "Naloži novo fotografijo s spletnega naslova", @@ -54,14 +55,14 @@ "change_password_success": "Geslo je bilo posodobljeno!", "confirm_password": "Potrdi geslo", "password": "Geslo", - "username_taken_workaround": "Predlagano uporabniško ime je že zasedeno, zato predlagamo <strong>%1</strong>", - "password_same_as_username": "Your password is the same as your username, please select another password.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "username_taken_workaround": "Predlagano uporabniško ime je že zasedeno, zato smo ga rahlo spremenili. Sedaj vas poznamo kot <strong>%1</strong>", + "password_same_as_username": "Vaše geslo je enako kot vaše uporabniško ime, prosim izberite drugačno geslo.", + "password_same_as_email": "Vaše geslo je enako kot vaše e-poštni naslov, prosim izberite drugačno geslo.", "upload_picture": "Naloži fotografijo", "upload_a_picture": "Naloži fotografijo", - "remove_uploaded_picture": "Remove Uploaded Picture", - "upload_cover_picture": "Upload cover picture", - "settings": "Nastavitve.", + "remove_uploaded_picture": "Odstrani preneseno sliko ", + "upload_cover_picture": "Prenesi fotografijo naslovnice", + "settings": "Nastavitve", "show_email": "Pokaži moj e-poštni naslov.", "show_fullname": "Pokaži moj ime in priimek.", "restrict_chats": "Dovoli klepet samo z osebami, ki jim sledim.", @@ -74,36 +75,55 @@ "send_chat_notifications": "Pošlji mi e-pošto, če se pojavi novo sporočilo v klepetu, medtem ko sem neprijavljen", "send_post_notifications": "Pošlji mi e-pošto, če se pojavijo nove objave v temi, kateri sledim", "settings-require-reload": "Nekatere spremembe potrebujejo osvežitev strani. Pritisni tukaj za ponovno nalaganje.", - "has_no_follower": "Uporabnik nihče ne sledi :(", + "has_no_follower": "Uporabniku nihče ne sledi :(", "follows_no_one": "Uporabnik nikomur ne sledi :(", - "has_no_posts": "Uporabnik še ni ustvaril nobene objave.", - "has_no_topics": "Uporabnik še ni odprl nobene teme.", + "has_no_posts": "Uporabnik še ni nič objavil.", + "has_no_topics": "Uporabnik še ni objavil nobene teme.", "has_no_watched_topics": "Uporabnik še ne spremlja nobene teme.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", + "has_no_upvoted_posts": "Uporabnik še ni glasoval za nobeno objavo.", + "has_no_downvoted_posts": "Uporabnik še ni glasoval proti nobeni objavi.", + "has_no_voted_posts": "Uporabnik nima glasovanih objav", "email_hidden": "Skrit e-poštni naslov", "hidden": "skrit", "paginate_description": "Uporabi oštevilčenje strani namesto neskončnega drsenja", "topics_per_page": "Število tem na stran", "posts_per_page": "Število objav na stran", "notification_sounds": "Zvočno me opozori, ko prejmem obvestilo", + "notifications_and_sounds": "Obvestila in zvoki", + "incoming-message-sound": "Zvok za prejeto sporočilo", + "outgoing-message-sound": "Zvok za poslano sporočilo", + "notification-sound": "Zvok obvestila", + "no-sound": "Ni zvoka", "browsing": "Preglej nastavitve", "open_links_in_new_tab": "Zunanje povezave odpri v novem zavihku", "enable_topic_searching": "Omogoči iskanje znotraj teme", "topic_search_help": "Če omogočite, bo iskanje prepisalo brskalnikove prevzete nastavitve in vam omogočilo iskanje skozi celotno temo.", + "delay_image_loading": "Zakasnitev pri nalaganju slike", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Spremljaj teme v katerih si sodeloval", - "follow_topics_you_create": "Spremljaj teme, ki si jih ustvaril/a", - "grouptitle": "Izberi ime skupine za prikaz", + "follow_topics_you_reply_to": "Spremljanj teme, na katere si odgovoril", + "follow_topics_you_create": "Spremljanj teme, ki si jih ustvaril", + "grouptitle": "Naslov skupine", "no-group-title": "Skupina nima imena", - "select-skin": "Izberi obliko", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", + "select-skin": "Izberi preobleko", + "select-homepage": "Izberi domačo stran", + "homepage": "Domača stran", "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", "custom_route": "Custom Homepage Route", "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", - "sso.title": "Single Sign-on Services", - "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.title": "Storitev enotne prijave ", + "sso.associated": "Povezan z", + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/sl/users.json b/public/language/sl/users.json index 4441c34e62..8a7e488a24 100644 --- a/public/language/sl/users.json +++ b/public/language/sl/users.json @@ -2,6 +2,7 @@ "latest_users": "Zadnji uporabniki", "top_posters": "Najboljši uporabniki", "most_reputation": "Največ ugleda", + "most_flags": "Most Flags", "search": "Išči", "enter_username": "Za iskanje vpiši uporabniško ime", "load_more": "Naloži več", @@ -10,11 +11,11 @@ "online-only": "Samo dosegljivi", "invite": "Povabi", "invitation-email-sent": "Povabilo je bilo poslano na e-mail naslov %1", - "user_list": "Lista uporabnikov", + "user_list": "Sezam uporabnikov", "recent_topics": "Zadnje teme", "popular_topics": "Popularne teme", "unread_topics": "Neprebrane teme", "categories": "Kategorije", "tags": "Oznake", - "no-users-found": "No users found!" + "no-users-found": "Ni mogoče najti uporabnikov" } \ No newline at end of file diff --git a/public/language/sr/category.json b/public/language/sr/category.json index 574ab2a529..29d26b51db 100644 --- a/public/language/sr/category.json +++ b/public/language/sr/category.json @@ -10,7 +10,11 @@ "share_this_category": "Podeli ovu kategoriju", "watch": "Прати", "ignore": "Игнориши", - "watch.message": "Сада пратите ажурирања ове категорије", - "ignore.message": "Сада игноришете ажурирања ове категорије", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Гледане категорије" } \ No newline at end of file diff --git a/public/language/sr/email.json b/public/language/sr/email.json index a9ef2f8954..885e6d2cbd 100644 --- a/public/language/sr/email.json +++ b/public/language/sr/email.json @@ -21,9 +21,10 @@ "digest.cta": "Кликните овде да посетите %1", "digest.unsub.info": "Овај резиме је послат вама због претплате.", "digest.no_topics": "Није било активних тема у последњих %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", + "digest.day": "Дан", + "digest.week": "Недеља", + "digest.month": "Месец", + "digest.subject": "Digest for %1", "notif.chat.subject": "Примљена је нова порука ћаскања од %1", "notif.chat.cta": "Кликните овде да наставите са разговором", "notif.chat.unsub.info": "Ова нотификација је послата вама због претплате коју имате.", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 25e4d1b130..e136a94f7f 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -14,20 +14,25 @@ "invalid-password": "Неисправна лозинка", "invalid-username-or-password": "Молимо наведите и корисничко име и лозинку", "invalid-search-term": "Неисправан упит за претрагу", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-pagination-value": "Неважећа вредност при обележавању страна, мора бити најмање %1 и највише %2 ", "username-taken": "Корисничко име је заузето", "email-taken": "Адреса е-поште је заусета", "email-not-confirmed": "Ваша адреса е-поште жоуш увек није оверена, кликните овде да би сте то учинили.", "email-not-confirmed-chat": "Није вам дозвољено да ћаскате док не оверите вашу е-пошту, клкните овде да то учините.", - "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", - "email-confirm-failed": "We could not confirm your email, please try again later.", - "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", - "username-too-short": "Username too short", - "username-too-long": "Username too long", - "password-too-long": "Password too long", - "user-banned": "User banned", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", + "no-email-to-confirm": "Форум захтева потврду е-поште, кликните овде да бисте отворили е-пошту", + "email-confirm-failed": "Потврда е-поште није успела, молимо вас да покушате касније.", + "confirm-email-already-sent": "Конфирмациони имејл је већ послат, молимо вас да сачекате %1 минут(а) да бисте послали други.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "username-too-short": "Корисничко име је прекратко", + "username-too-long": "Корисничко име је предуго", + "password-too-long": "Шифра је предугачка.", + "user-banned": "Члан банован", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "Категорија не постоји", "no-topic": "Тема не постоји", "no-post": "Порука не постоји", @@ -38,10 +43,26 @@ "category-disabled": "Category disabled", "topic-locked": "Тема је закључана", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "invalid-image-type": "Invalid image type. Allowed types are: %1", "invalid-image-extension": "Неважећи тип слике", "invalid-file-type": "Неважећи тип фајла. Дозвољени су: %1", "group-name-too-short": "Име групе је прекратко", + "group-name-too-long": "Group name too long", "group-already-exists": "Група већ постоји", "group-name-change-not-allowed": "Мењање имена групе није дозвољено", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/sr/global.json b/public/language/sr/global.json index 1891007d72..c57d8d4d4e 100644 --- a/public/language/sr/global.json +++ b/public/language/sr/global.json @@ -7,8 +7,10 @@ "403.login": "Можда би требало да се <a href='%1/login'>пријавите</a>?", "404.title": "Не постоји", "404.message": "Изгледа да сте наишли на страницу која не постоји. Вратите се на <a href='%1/'>почетну страницу.</a>.", - "500.title": "Унутрашња грешка.", + "500.title": "Internal Error.", "500.message": "Упс! Изгледа да нешто ије како треба!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Регистрација", "login": "Пријава", "please_log_in": "Молимо, пријавите се", @@ -17,6 +19,7 @@ "welcome_back": "Добродошли поново", "you_have_successfully_logged_in": "Успешно сте се пријавили", "save_changes": "Сними измене", + "save": "Save", "close": "Затвори", "pagination": "Обележавање страна", "pagination.out_of": "%1 од %2", @@ -50,7 +53,9 @@ "topics": "Теме", "posts": "Поруке", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "Прегледи", "reputation": "Репутација", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/sr/groups.json b/public/language/sr/groups.json index 91bd05e916..00127e0eac 100644 --- a/public/language/sr/groups.json +++ b/public/language/sr/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Напусти групу", "membership.reject": "Одбаци", "new-group.group_name": "Име групе:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/sr/login.json b/public/language/sr/login.json index 8fefdb9068..fae2c17159 100644 --- a/public/language/sr/login.json +++ b/public/language/sr/login.json @@ -5,7 +5,8 @@ "remember_me": "Памти ме?", "forgot_password": "Заборављена лозинка?", "alternative_logins": "Алтернативно пријављивање", - "failed_login_attempt": "Неуспело пријављивање, покушајте поново.", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "Успешно сте се пријавили!", - "dont_have_account": "Немате налог?" + "dont_have_account": "Немате налог?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json index 412f9fd207..adec279e99 100644 --- a/public/language/sr/modules.json +++ b/public/language/sr/modules.json @@ -6,7 +6,7 @@ "chat.user_typing": "%1 куца ...", "chat.user_has_messaged_you": "%1 вам посла поруку.", "chat.see_all": "Погледајте сва ћаскања", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Означи све разговоре као прочитане", "chat.no-messages": "Изаберите примаоца да бисте видели историју ћаскања", "chat.no-users-in-room": "Нема корисника у овој соби", "chat.recent-chats": "Недавна ћаскања", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Пошаљи и закључај", "composer.toggle_dropdown": "Подесите \"Dropdown\"", "composer.uploading": "Убацивање %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "ОК", "bootbox.cancel": "Откажи", "bootbox.confirm": "Потврди", diff --git a/public/language/sr/notifications.json b/public/language/sr/notifications.json index 52a2b2d949..e8f4a719c4 100644 --- a/public/language/sr/notifications.json +++ b/public/language/sr/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> и %2 осталих су гласали за вашу поруку у <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> је померио вашу поруку у <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> је померио <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> означи поруку у <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> и <strong>%2</strong> су означили поруку у <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> и осталих %2 су означили поруку у <strong>%3</strong>", diff --git a/public/language/sr/pages.json b/public/language/sr/pages.json index 0e7387583e..0210817e35 100644 --- a/public/language/sr/pages.json +++ b/public/language/sr/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "Обавештења", "tags": "Ознаке", "tag": "Теме са ознаком „%1“", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Категорије", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/sr/register.json b/public/language/sr/register.json index dbc57e60da..17da5279f4 100644 --- a/public/language/sr/register.json +++ b/public/language/sr/register.json @@ -1,5 +1,6 @@ { "register": "Регистрација", + "cancel_registration": "Cancel Registration", "help.email": "Ваша е-пошта ће подразумевано бити скривена од јавности.", "help.username_restrictions": "Јединствено корисничко име између %1 и %2 знакова. Остали вас могу спомињати путем @<span id=„ВашеКорисничкоИме“>username</span>.", "help.minimum_password_length": "Ваша лозинке мора имати најмање %1 знакова.", @@ -15,5 +16,8 @@ "alternative_registration": "Алтернативно регистровање", "terms_of_use": "Услови коришћења", "agree_to_terms_of_use": "Слажем се са условима коришћења", - "registration-added-to-queue": "Ваша регистрација је додата у ред одобравања. Добићете меијл када администратор прихвати вашу регистрацију." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Ваша регистрација је додата у ред одобравања. Добићете меијл када администратор прихвати вашу регистрацију.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index 37e042c18f..10c126da64 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -26,12 +26,30 @@ "tools": "Алатке", "flag": "Означи", "locked": "Закључан", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", - "not_following_topic.message": "You will no longer receive notifications from this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", "markAsUnreadForAll.success": "Topic marked as unread for all.", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "Алатке теме", - "thread_tools.markAsUnreadForAll": "Означи као непрочитано", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin Topic", "thread_tools.unpin": "Unpin Topic", "thread_tools.lock": "Закључај тему", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "This topic will be moved to the category", "fork_topic_instruction": "Click the posts you want to fork", "fork_no_pids": "No posts selected!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "Enter your topic title here...", diff --git a/public/language/sr/unread.json b/public/language/sr/unread.json index 3fe33d78e0..a811fd23d4 100644 --- a/public/language/sr/unread.json +++ b/public/language/sr/unread.json @@ -6,5 +6,8 @@ "selected": "Изабране", "all": "Све", "all_categories": "Све категорије", - "topics_marked_as_read.success": "Теме означене као прочитане!" + "topics_marked_as_read.success": "Теме означене као прочитане!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/sr/user.json b/public/language/sr/user.json index 66280af2d3..0c158620ac 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -6,6 +6,7 @@ "postcount": "Број порука", "email": "Е-пошта", "confirm_email": "Потврда е-поште", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "Topics per Page", "posts_per_page": "Posts per Page", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Browsing Settings", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "Enable In-Topic Searching", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/sr/users.json b/public/language/sr/users.json index 6308354310..fc774b20b2 100644 --- a/public/language/sr/users.json +++ b/public/language/sr/users.json @@ -2,6 +2,7 @@ "latest_users": "Последњи корисник", "top_posters": "Највише порука", "most_reputation": "Највећи углед", + "most_flags": "Most Flags", "search": "Претрага", "enter_username": "Унесите корисничко име за претрагу", "load_more": "Учитај више", diff --git a/public/language/sv/category.json b/public/language/sv/category.json index 12204804ed..42ebe430b9 100644 --- a/public/language/sv/category.json +++ b/public/language/sv/category.json @@ -6,11 +6,15 @@ "no_topics": "<strong>Det finns inga ämnen i denna kategori.</strong><br />Varför skapar inte du ett ämne?", "browsing": "läser", "no_replies": "Ingen har svarat", - "no_new_posts": "Inga nya inlägg", + "no_new_posts": "Inga nya inlägg.", "share_this_category": "Dela den här kategorin", "watch": "Bevaka", "ignore": "Ignorera", - "watch.message": "Du bevakar nu uppdateringar ifrån denna kategori", - "ignore.message": "Du ignorerar nu uppdateringar ifrån denna kategori", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Bevakade kategorier" } \ No newline at end of file diff --git a/public/language/sv/email.json b/public/language/sv/email.json index 7d10363d97..6780dc6f92 100644 --- a/public/language/sv/email.json +++ b/public/language/sv/email.json @@ -5,31 +5,32 @@ "greeting_no_name": "Hej", "greeting_with_name": "Hej %1", "welcome.text1": "Tack för att du registerar dig på %1!", - "welcome.text2": "För att slutföra aktiveringen av ditt konto, behöver vi verifiera att du har tillgång till den epostadress du registrerade dig med.", + "welcome.text2": "För att slutföra aktiveringen av ditt konto, behöver vi verifiera att du har tillgång till den e-postadress du registrerade dig med.", "welcome.text3": "En administrator har accepterat din registreringsansökan. Du kan logga in med ditt användarnamn och lösenord nu.", - "welcome.cta": "Klicka här för att bekräfta din epostadress ", + "welcome.cta": "Klicka här för att bekräfta din e-postadress ", "invitation.text1": "%1 har bjudit in dig till %2", "invitation.ctr": "Klicka här för att skapa ditt konto.", "reset.text1": "Vi fick en förfrågan om att återställa ditt lösenord, möjligen för att du har glömt det. Om detta inte är fallet, så kan du bortse från det här epostmeddelandet. ", "reset.text2": "För att fortsätta med återställning av lösenordet så kan du klicka på följande länk:", "reset.cta": "Klicka här för att återställa ditt lösenord", "reset.notify.subject": "Lösenordet ändrat", - "reset.notify.text1": "Vi vill uppmärksamma dig på att ditt lösenord ändrades den %1", - "reset.notify.text2": "Om du inte godkänt det här så vänligen kontakta en admin snarast. ", + "reset.notify.text1": "Vi vill uppmärksamma dig på att ditt lösenord ändrades den %1.", + "reset.notify.text2": "Om du inte godkänt det här så vänligen kontakta en administratör snarast. ", "digest.notifications": "Du har olästa notiser från %1:", "digest.latest_topics": "Senaste ämnen från %1", "digest.cta": "Klicka här för att besöka %1", "digest.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ", - "digest.no_topics": "Inga aktiva ämnen dom senaste %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", - "notif.chat.subject": "Nytt chatt-meddelande från %1", + "digest.no_topics": "Inga aktiva ämnen de senaste %1", + "digest.day": "dag", + "digest.week": "vecka", + "digest.month": "månad", + "digest.subject": "Sammanställt flöde för %1", + "notif.chat.subject": "Nytt meddelande från %1", "notif.chat.cta": "Klicka här för att fortsätta konversationen", - "notif.chat.unsub.info": "Denna chatt-notifikation skickades till dig på grund av dina inställningar för prenumerationer.", + "notif.chat.unsub.info": "Denna notifikation skickades till dig på grund av dina inställningar för prenumerationer.", "notif.post.cta": "Klicka här för att läsa hela ämnet", "notif.post.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ", - "test.text1": "\nDet här är ett textmeddelande som verifierar att eposten är korrekt installerat för din NodeBB. ", - "unsub.cta": "Klicka här för att ändra dom inställningarna", + "test.text1": "\nDet här är ett testmeddelande som verifierar att e-posten är korrekt installerad för din NodeBB. ", + "unsub.cta": "Klicka här för att ändra inställningarna", "closing": "Tack!" } \ No newline at end of file diff --git a/public/language/sv/error.json b/public/language/sv/error.json index efb83e5014..ca89517bd8 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -14,20 +14,25 @@ "invalid-password": "Ogiltigt lösenord", "invalid-username-or-password": "Specificera både användarnamn och lösenord", "invalid-search-term": "Ogiltig sökterm", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "Det gick inte att logga in dig, sannolikt på grund av en utgången session. Var god försök igen", + "invalid-pagination-value": "Ogiltigt värde för siduppdelning. Värdet måste vara mellan %1 och %2", "username-taken": "Användarnamn upptaget", "email-taken": "Epostadress upptagen", "email-not-confirmed": "Din epostadress är ännu inte bekräftad. Klicka här för att bekräfta din epostadress.", "email-not-confirmed-chat": "Du kan ej använda chatten förrän din epostadress har blivit bekräftad, var god klicka här för att bekräfta din epostadress.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Detta forum kräver bekräftning av epostadresser, var god klicka här för att fylla i en epostadress", "email-confirm-failed": "Vi kunde ej bekräfta din epostadress, var god försök igen senare.", "confirm-email-already-sent": "Bekräftningsbrev redan skickat, var god vänta %1 minut(er) innan du skickar ett nytt.", + "sendmail-not-found": "Kunde inte hitta Sendmail, vänligen se till att den är installerad och får köras av den användare som kör NodeBB.", "username-too-short": "Användarnamnet är för kort", "username-too-long": "Användarnamnet är för långt", - "password-too-long": "Password too long", + "password-too-long": "Lösenordet är för långt", "user-banned": "Användare bannlyst", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "När du är ny medlem måste du vänta %1 sekund(er) innan du gör ditt första inlägg", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Din IP-adress har blivit bannlyst från det här forumet. Om du tror att det beror på ett misstag, vad god kontakta en administratör. ", + "ban-expiry-missing": "Ange ett slutdatum för denna banning", "no-category": "Kategorin finns inte", "no-topic": "Ämnet finns inte", "no-post": "Inlägget finns inte", @@ -38,31 +43,49 @@ "category-disabled": "Kategorin inaktiverad", "topic-locked": "Ämnet låst", "post-edit-duration-expired": "Du kan endast ändra inlägg inom %1 sekund(er) efter att ha skickat det", + "post-edit-duration-expired-minutes": "Du kan endast ändra inlägg inom %1 minut(er) efter att ha skickat det", + "post-edit-duration-expired-minutes-seconds": "Du kan endast ändra inlägg inom %1 minut(er) %2 sekund(er) efter att ha skickat det", + "post-edit-duration-expired-hours": "Du kan endast ändra inlägg inom %1 timm(ar) efter att ha skickat det", + "post-edit-duration-expired-hours-minutes": "Du kan endast ändra inlägg inom %1 timm(ar) %2 minut(er) efter att ha skickat det", + "post-edit-duration-expired-days": "Du kan endast ändra inlägg inom %1 dag(ar) efter att ha skickat det", + "post-edit-duration-expired-days-hours": "Du kan endast ändra inlägg inom %1 dag(ar) %2 timm(ar) efter att ha skickat det", + "post-delete-duration-expired": "Du kan endast radera inlägg inom %1 sekund(er) efter att ha skickat det", + "post-delete-duration-expired-minutes": "Du kan endast radera inlägg inom %1 minut(er) efter att ha skickat det", + "post-delete-duration-expired-minutes-seconds": "Du kan endast radera inlägg inom %1 minut(er) %2 sekund(er) efter att ha skickat det", + "post-delete-duration-expired-hours": "Du kan endast radera inlägg inom %1 timm(ar) efter att ha skickat det", + "post-delete-duration-expired-hours-minutes": "Du kan endast radera inlägg inom %1 timmar(er) %2 minut(er) efter att ha skickat det", + "post-delete-duration-expired-days": "Du kan endast radera inlägg inom %1 dag(ar) efter att ha skickat det", + "post-delete-duration-expired-days-hours": "Du kan endast radera inlägg inom %1 dag(ar) %2 timm(ar) efter att ha skickat det", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Skriv ett längre inlägg. Inlägg måste innehålla minst %1 tecken.", "content-too-long": "Skriv ett kortare inlägg. Inlägg kan inte innehålla mer än %1 tecken.", "title-too-short": "Skriv en längre rubrik. Rubriker måste innehålla minst %1 tecken.", "title-too-long": "Skriv en kortare rubrik. Rubriker kan inte innehålla mer än %1 tecken.", + "category-not-selected": "Category not selected.", "too-many-posts": "Du måste vänta minst %1 sekund(er) mellan varje inlägg", "too-many-posts-newbie": "Som ny användare måste du vänta %1 sekund(er) mellan varje inlägg tills dess du har %2 förtroende", - "tag-too-short": "Fyll i ett längre märkord. Märkord måste vara minst %1 tecken långa", - "tag-too-long": "Fyll i ett kortare märkord. Märkord kan ej vara längre än %1 tecken långa", - "not-enough-tags": "Ej tillräckligt många märkord. Ämnen måste ha minst %1 märkord", - "too-many-tags": "För många märkord. Ämnen kan ej har mer än %1 märkord", + "tag-too-short": "Fyll i en längre tagg. Taggar måste vara minst %1 tecken långa", + "tag-too-long": "Fyll i en kortare tagg. Taggar kan ej vara längre än %1 tecken långa", + "not-enough-tags": "Otillräckligt antal taggar. Ämnen måste ha minst %1 taggar", + "too-many-tags": "För många taggar. Ämnen kan ej har mer än %1 tagg(ar)", "still-uploading": "Vänta medan uppladdningen slutförs.", - "file-too-big": "Den maximalt tillåtna filstorleken är %1 kB - ladda upp en mindre fil", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", - "cant-ban-other-admins": "Du kan inte bannlysa andra administratörer.", + "file-too-big": "Den maximalt tillåtna filstorleken är %1 kB - var god ladda upp en mindre fil", + "guest-upload-disabled": "Uppladdningar av oregistrerade användare har inaktiverats", + "already-favourited": "Du har redan lagt till bokmärke för det här inlägget", + "already-unfavourited": "Du har redan tagit bort bokmärket för det här inlägget", + "cant-ban-other-admins": "Du kan inte bannlysa andra administratörer!", "cant-remove-last-admin": "Du är den enda administratören. Lägg till en annan användare som administratör innan du tar bort dig själv.", + "cant-delete-admin": "Ta bort administratörsbehörighet från detta konto innan du försöker ta bort den.", "invalid-image-type": "Ogiltig bildtyp. Tillåtna typer är: % 1", "invalid-image-extension": "Ogiltigt bildformat", "invalid-file-type": "Ogiltig filtyp. Tillåtna typer är: % 1", "group-name-too-short": "Gruppnamnet är för kort", + "group-name-too-long": "Gruppnamnet är för långt", "group-already-exists": "Gruppen existerar redan", "group-name-change-not-allowed": "Gruppnamnet får inte ändras", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", + "group-already-member": "Redan i denna grupp", + "group-not-member": "Ej medlem av denna grupp", "group-needs-owner": "Gruppen kräver minst en ägare", "group-already-invited": "Användaren har redan bjudits in", "group-already-requested": "Din medlemsskapsförfrågan har redan skickats", @@ -70,34 +93,39 @@ "post-already-restored": "Inlägget är redan återställt", "topic-already-deleted": "Ämnet är redan raderat", "topic-already-restored": "Ämnet är redan återställt", - "cant-purge-main-post": "Huvudinlägg kan ej rensas, ta bort ämnet istället", + "cant-purge-main-post": "Huvudinlägg kan ej rensas bort, ta bort ämnet istället", "topic-thumbnails-are-disabled": "Miniatyrbilder för ämnen är inaktiverat", "invalid-file": "Ogiltig fil", "uploads-are-disabled": "Uppladdningar är inaktiverat", "signature-too-long": "Din signatur kan inte vara längre än %1 tecken.", - "about-me-too-long": "Din om mig kan inte vara längre än %1 tecken.", - "cant-chat-with-yourself": "Du kan inte chatta med dig själv.", - "chat-restricted": "Denna användaren har begränsat sina chatt-meddelanden. Användaren måste följa dig innan ni kan chatta med varann", - "chat-disabled": "Chat system disabled", + "about-me-too-long": "Din text om dig själv kan inte vara längre än %1 tecken.", + "cant-chat-with-yourself": "Du kan inte chatta med dig själv!", + "chat-restricted": "Denna användaren har begränsat sina meddelanden. Användaren måste följa dig innan ni kan chatta med varandra", + "chat-disabled": "Chatten är inaktiverad", "too-many-messages": "Du har skickat för många meddelanden, var god vänta", "invalid-chat-message": "Ogiltigt chattmeddelande", "chat-message-too-long": "Chattmeddelande är för långt", - "cant-edit-chat-message": "You are not allowed to edit this message", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "cant-edit-chat-message": "Du har inte rättigheter att redigera det här meddelandet", + "cant-remove-last-user": "Du kan inte ta bort den sista användaren", + "cant-delete-chat-message": "Du har inte rättigheter att radera det här meddelandet", + "already-voting-for-this-post": "Du har redan röstat på det här inlägget.", "reputation-system-disabled": "Ryktessystemet är inaktiverat.", "downvoting-disabled": "Nedröstning är inaktiverat", "not-enough-reputation-to-downvote": "Du har inte tillräckligt förtroende för att rösta ner det här meddelandet", "not-enough-reputation-to-flag": "Du har inte tillräckligt förtroende för att flagga det här inlägget.", "already-flagged": "Du har redan flaggat det här inlägget", - "reload-failed": "NodeBB stötte på problem med att ladda om: \"%1\". NodeBB kommer fortsätta servera den befintliga resurser till klienten, men du borde återställa det du gjorde alldeles innan du försökte ladda om.", + "reload-failed": "NodeBB stötte på problem med att ladda om: \"%1\". NodeBB kommer fortsätta servera befintliga resurser till klienten, men du borde återställa det du gjorde innan du försökte ladda om.", "registration-error": "Registreringsfel", "parse-error": "Något gick fel vid tolkning av svar från servern", - "wrong-login-type-email": "Använd din e-post adress för att logga in", + "wrong-login-type-email": "Använd din e-postadress för att logga in", "wrong-login-type-username": "Använd ditt användarnamn för att logga in", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "invite-maximum-met": "Du har bjudit in det maximala antalet användare (%1 av %2)", + "no-session-found": "Ingen login-session hittades!", + "not-in-room": "Användaren finns inte i rummet", + "no-users-in-room": "Inga användare i det här rummet", + "cant-kick-self": "Du kan inte sparka ut dig själv från gruppen", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/sv/global.json b/public/language/sv/global.json index a9cb801869..6a50800411 100644 --- a/public/language/sv/global.json +++ b/public/language/sv/global.json @@ -2,13 +2,15 @@ "home": "Hem", "search": "Sök", "buttons.close": "Stäng", - "403.title": "Tillgång Nekad", + "403.title": "Tillgång nekad", "403.message": "Du verkar ha ramlat in på en sida du ej har tillgång till.", "403.login": "Du kanske bör <a href='%1/login'>försöka logga in</a>?", "404.title": "Sidan saknas", "404.message": "Du verkar ha ramlat in på en sida som inte finns. Återgå till <a href='%1/'>första sidan</a>.", - "500.title": "Internt fel.", - "500.message": "Hoppsan! Verkar som att något gått snett!", + "500.title": "Internal Error.", + "500.message": "Hoppsan! Något verkar ha gått fel!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Registrera", "login": "Logga in", "please_log_in": "Var god logga in", @@ -17,6 +19,7 @@ "welcome_back": "Välkommen tillbaka ", "you_have_successfully_logged_in": "Inloggningen lyckades", "save_changes": "Spara ändringar", + "save": "Save", "close": "Stäng", "pagination": "Siduppdelning", "pagination.out_of": "%1 av %2", @@ -25,7 +28,7 @@ "header.categories": "Kategorier", "header.recent": "Senaste", "header.unread": "Olästa", - "header.tags": "Märkningar", + "header.tags": "Taggar", "header.popular": "Populära", "header.users": "Användare", "header.groups": "Grupper", @@ -33,15 +36,15 @@ "header.notifications": "Notiser", "header.search": "Sök", "header.profile": "Profil", - "header.navigation": "Navigation", - "notifications.loading": "Laddar Notiser", - "chats.loading": "Laddar Chattar", - "motd.welcome": "Välkommen till NodeBB, framtidens diskussions-plattform.", + "header.navigation": "Navigering", + "notifications.loading": "Laddar notiser", + "chats.loading": "Laddar chattar", + "motd.welcome": "Välkommen till NodeBB, framtidens diskussionsplattform.", "previouspage": "Föregående sida", "nextpage": "Nästa sida", - "alert.success": "success", + "alert.success": "Lyckat", "alert.error": "Fel", - "alert.banned": "Bannad", + "alert.banned": "Bannlyst", "alert.banned.message": "Du har blivit bannlyst och kommer nu att loggas ut. ", "alert.unfollow": "Du följer inte längre %1!", "alert.follow": "Du följer nu %1!", @@ -49,46 +52,51 @@ "users": "Användare", "topics": "Ämnen", "posts": "Inlägg", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", + "best": "Bästa", + "upvoters": "Upvoters", + "upvoted": "Uppröstad", + "downvoters": "Downvoters", + "downvoted": "Nedröstad", "views": "Visningar", "reputation": "Rykte", "read_more": "läs mer", "more": "Mer", "posted_ago_by_guest": "inskickad %1 av anonym", "posted_ago_by": "inskickad %1 av %2", - "posted_ago": "inskickad %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", + "posted_ago": "postat %1", + "posted_in": "postat i %1", + "posted_in_by": "postat i %1 av %2", "posted_in_ago": "inskickad i %1 %2", - "posted_in_ago_by": "inskickad i %1 %2 av %3", - "user_posted_ago": "%1 skickades in %2", - "guest_posted_ago": "Anonym skickade in %1", - "last_edited_by": "last edited by %1", + "posted_in_ago_by": "postat i %1 %2 av %3", + "user_posted_ago": "%1 postades %2", + "guest_posted_ago": "Anonym postade %1", + "last_edited_by": "Senaste redigerad av %1", "norecentposts": "Inga nya inlägg", "norecenttopics": "Inga nya ämnen", - "recentposts": "Senaste ämnena", + "recentposts": "Senaste inläggen", "recentips": "Nyligen inloggade IPn", "away": "Borta", - "dnd": "Do not disturb", + "dnd": "Stör inte", "invisible": "Osynlig", "offline": "Offline", - "email": "Epost", + "email": "E-post", "language": "Språk", "guest": "Anonym", "guests": "Anonyma", - "updated.title": "Forum uppdaterades", + "updated.title": "Forumet uppdaterades", "updated.message": "Det här forumet har nu uppdaterats till senaste versionen. Klicka här för att ladda om sidan.", "privacy": "Integritet", "follow": "Följ", "unfollow": "Sluta följ", - "delete_all": "Ta bort Alla", - "map": "Map", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "delete_all": "Ta bort alla", + "map": "Karta", + "sessions": "Login-sessioner", + "ip_address": "IP-adress", + "enter_page_number": "Skriv in sidnummer", + "upload_file": "Ladda upp en fil", + "upload": "Ladda upp", + "allowed-file-types": "Tillåtna filtyper är %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/sv/groups.json b/public/language/sv/groups.json index 2fa0d40823..4c895e83fd 100644 --- a/public/language/sv/groups.json +++ b/public/language/sv/groups.json @@ -7,30 +7,30 @@ "pending.accept": "Acceptera", "pending.reject": "Neka", "pending.accept_all": "Acceptera alla", - "pending.reject_all": "Neka alla", + "pending.reject_all": "Avvisa alla", "pending.none": "Det finns inga väntande medlemmar just nu", "invited.none": "Det finns inga inbjudna medlemmar just nu", "invited.uninvite": "Dra tillbaka inbjudan", "invited.search": "Sök efter en användare att lägga till i denna grupp", - "invited.notification_title": "You have been invited to join <strong>%1</strong>", - "request.notification_title": "Group Membership Request from <strong>%1</strong>", - "request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>", + "invited.notification_title": "Du har blivit inbjuden att bli medlem i <strong>%1</strong>", + "request.notification_title": "Förfrågan om gruppmedlemskap från <strong>%1</strong>", + "request.notification_text": "<strong>%1</strong> har skickat en förfrågan om medlemskap i <strong>%2</strong>", "cover-save": "Spara", "cover-saving": "Sparar", "details.title": "Detaljer för gruppen ", - "details.members": "Medlemmar", + "details.members": "Medlemslista", "details.pending": "Väntande medlemmar", "details.invited": "Inbjudna medlemmar", "details.has_no_posts": "Den här gruppens medlemmar har inte skrivit några inlägg.", "details.latest_posts": "Senaste inlägg", "details.private": "Privat", - "details.disableJoinRequests": "Disable join requests", - "details.grant": "Ge/Ta ifrån ägarskap", + "details.disableJoinRequests": "Inaktivera förfrågningar om att gå med", + "details.grant": "Tilldela/Dra tillbaka ägarskap", "details.kick": "Sparka ut", "details.owner_options": "Gruppadministration", "details.group_name": "Gruppnamn", "details.member_count": "Medlemsantal", - "details.creation_date": "Skapatdatum", + "details.creation_date": "Skapardatum", "details.description": "Beskrivning", "details.badge_preview": "Förhandsgranskning av märke", "details.change_icon": "Byt ikon", @@ -41,14 +41,16 @@ "details.hidden": "Dold", "details.hidden_help": "Om aktiverat kommer gruppen inte synas i grupplistan och användare måste bli inbjudna manuellt", "details.delete_group": "Ta bort grupp", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", - "event.updated": "Gruppdetaljerna har uppdaterats", + "details.private_system_help": "Privata grupper är ej tillgängligt. Den här inställningen har ingen effekt.", + "event.updated": "Gruppinformationen har uppdaterats", "event.deleted": "Gruppen \"%1\" har tagits bort", "membership.accept-invitation": "Acceptera inbjudan", "membership.invitation-pending": "Inbjudan väntar på svar", "membership.join-group": "Gå med i grupp", "membership.leave-group": "Lämna grupp", "membership.reject": "Neka", - "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "new-group.group_name": "Gruppnamn:", + "upload-group-cover": "Ladda upp omslagsbild för grupp", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/sv/login.json b/public/language/sv/login.json index 67478a02ca..5407078d3b 100644 --- a/public/language/sv/login.json +++ b/public/language/sv/login.json @@ -5,7 +5,8 @@ "remember_me": "Kom ihåg mig?", "forgot_password": "Glömt lösenord?", "alternative_logins": "Alternativa inloggningssätt", - "failed_login_attempt": "Inloggningen misslyckades, var god försök igen.", + "failed_login_attempt": "Misslyckad inloggning", "login_successful": "Du är nu inloggad!", - "dont_have_account": "Har du inget konto?" + "dont_have_account": "Har du inget konto?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json index ae9799ad2e..afe2311120 100644 --- a/public/language/sv/modules.json +++ b/public/language/sv/modules.json @@ -1,38 +1,48 @@ { "chat.chatting_with": "Chatta med <span id=\"chat-with-name\"></span>", - "chat.placeholder": "Skriv chatmeddelande här och tryck sen enter för att skicka ", + "chat.placeholder": "Skriv chattmeddelande här och tryck sen Enter för att skicka ", "chat.send": "Skicka", "chat.no_active": "Du har inte några aktiva chattar.", "chat.user_typing": "%1 skriver ...", "chat.user_has_messaged_you": "%1 har skickat ett medelande till dig.", - "chat.see_all": "See all chats", - "chat.mark_all_read": "Mark all chats read", - "chat.no-messages": "Välj mottagare för att visa historik för chatmeddelande", - "chat.no-users-in-room": "No users in this room", + "chat.see_all": "Se alla chattar", + "chat.mark_all_read": "Markera alla chattar som lästa", + "chat.no-messages": "Välj mottagare för att visa historik för chattmeddelande", + "chat.no-users-in-room": "Inga användare i detta rum", "chat.recent-chats": "Senaste chattarna", "chat.contacts": "Kontakter ", "chat.message-history": "Historik för meddelande", "chat.pop-out": "Utskjutande chatt", "chat.maximize": "Maximera", - "chat.seven_days": "7 Dagar", - "chat.thirty_days": "30 Dagar", - "chat.three_months": "3 Månader", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", + "chat.seven_days": "7 dagar", + "chat.thirty_days": "30 dagar", + "chat.three_months": "3 månader", + "chat.delete_message_confirm": "Är du säker på att du vill radera det här meddelandet?", + "chat.roomname": "Chattrum %1", + "chat.add-users-to-room": "Addera användare till rum", "composer.compose": "Komponera", "composer.show_preview": "Visa förhandsgranskning", "composer.hide_preview": "Dölj förhandsgranskning", "composer.user_said_in": "%1 sa i %2:", "composer.user_said": "%1 sa:", - "composer.discard": "Är du säker på att du vill förkasta det här inlägget?", + "composer.discard": "Är du säker på att du vill ta bort det här inlägget?", "composer.submit_and_lock": "Skicka och lås", "composer.toggle_dropdown": "Visa/Dölj dropdown", - "composer.uploading": "Uploading %1", + "composer.uploading": "Laddar upp %1", + "composer.formatting.bold": "Fet", + "composer.formatting.italic": "Kursiv", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Genomstrykning", + "composer.formatting.link": "Länk", + "composer.formatting.picture": "Bild", + "composer.upload-picture": "Ladda upp bild", + "composer.upload-file": "Ladda upp fil", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", - "bootbox.cancel": "Cancel", - "bootbox.confirm": "Confirm", - "cover.dragging_title": "Cover Photo Positioning", - "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", - "cover.saved": "Cover photo image and position saved" + "bootbox.cancel": "Avbryt", + "bootbox.confirm": "Bekräfta", + "cover.dragging_title": "Positionering av omslagsbild", + "cover.dragging_message": "Dra omslagsbilden till önskad position och tryck \"Spara\"", + "cover.saved": "Omslagsbilden sparad" } \ No newline at end of file diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json index 743451971a..53f94b6b3a 100644 --- a/public/language/sv/notifications.json +++ b/public/language/sv/notifications.json @@ -1,38 +1,35 @@ { "title": "Notiser", "no_notifs": "Du har inga nya notiser", - "see_all": "See all notifications", + "see_all": "Visa alla notiser", "mark_all_read": "Markera alla notiser som lästa", "back_to_home": "Tillbaka till %1", "outgoing_link": "Utgående länk", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "Du lämnar nu %1", "continue_to": "Fortsätt till %1", "return_to": "Återgå till %1", "new_notification": "Ny notis", "you_have_unread_notifications": "Du har olästa notiser.", "new_message_from": "Nytt medelande från <strong>%1</strong>", "upvoted_your_post_in": "<strong>%1</strong> har röstat upp ditt inlägg i <strong>%2</strong>", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", - "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", + "upvoted_your_post_in_dual": "<strong>%1</strong> och <strong>%2</strong> har röstat upp ditt inlägg i <strong>%3</strong>.", + "upvoted_your_post_in_multiple": "<strong>%1</strong> och %2 andra har röstat upp ditt inlägg i <strong>%3</strong>.", + "moved_your_post": "<strong>%1</strong> har flyttat ditt inlägg till <strong>%2</strong>", + "moved_your_topic": "<strong>%1</strong> har flyttat <strong>%2</strong>", "user_flagged_post_in": "<strong>%1</strong> flaggade ett inlägg i <strong>%2</strong>", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", + "user_flagged_post_in_dual": "<strong>%1</strong> och <strong>%2</strong> rapporterade ett inlägg i <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> och %2 andra rapporterade ett inlägg i <strong>%3</strong>", "user_posted_to": "<strong>%1</strong> har skrivit ett svar på: <strong>%2</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", + "user_posted_to_dual": "<strong>%1</strong> och <strong>%2</strong> har svarat på: <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> och %2 andra har svarat på: <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> har skapat ett nytt ämne: <strong>%2</strong>", "user_started_following_you": "<strong>%1</strong> började följa dig.", - "user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", + "user_started_following_you_dual": "<strong>%1</strong> och <strong>%2</strong> började följa dig.", + "user_started_following_you_multiple": "<strong>%1</strong> och %2 andra började följa dig.", "new_register": "<strong>%1</strong> skickade en registreringsförfrågan.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", - "email-confirmed": "Epost bekräftad", - "email-confirmed-message": "Tack för att du bekräftat din epostadress. Ditt konto är nu fullt ut aktiverat.", - "email-confirm-error-message": "Det uppstod ett fel med att bekräfta din epostadress. Kanske var koden ogiltig eller har gått ut.", - "email-confirm-sent": "Bekräftelseepost skickat." + "new_register_multiple": "Det finns <strong>%1</strong> förfrågningar om registrering som inväntar granskning.", + "email-confirmed": "E-post bekräftad", + "email-confirmed-message": "Tack för att du bekräftat din e-postadress. Ditt konto är nu fullt ut aktiverat.", + "email-confirm-error-message": "Det uppstod ett problem med bekräftelsen av din e-postadress. Kanske var koden felaktig eller ogiltig.", + "email-confirm-sent": "Bekräftelsemeddelande skickat." } \ No newline at end of file diff --git a/public/language/sv/pages.json b/public/language/sv/pages.json index 6953bf554e..b06f22e9b0 100644 --- a/public/language/sv/pages.json +++ b/public/language/sv/pages.json @@ -4,43 +4,46 @@ "popular-day": "Populära ämnen idag", "popular-week": "Populära ämnen den här veckan", "popular-month": "Populära ämnen denna månad", - "popular-alltime": "All time popular topics", + "popular-alltime": "Populäraste ämnena genom tiderna", "recent": "Senaste ämnena", - "flagged-posts": "Flagged Posts", + "flagged-posts": "Flaggade inlägg", "users/online": "Användare online", "users/latest": "Senaste Användare", "users/sort-posts": "Användare med flest inlägg", - "users/sort-reputation": "Users with the most reputation", - "users/banned": "Banned Users", + "users/sort-reputation": "Användare med bäst rykte", + "users/banned": "Bannlysta användare", + "users/most-flags": "Most flagged users", "users/search": "Användar Sök", "notifications": "Notiser", "tags": "Etiketter", "tag": "Ämnen märkta med \"%1\"", - "register": "Register an account", + "register": "Registrera ett konto", + "registration-complete": "Registration complete", "login": "Logga in på ditt konto", "reset": "Återställ lösenord", "categories": "Kategorier", "groups": "Grupper", - "group": "%1 group", - "chats": "Chats", - "chat": "Chatting with %1", - "account/edit": "Editing \"%1\"", - "account/edit/password": "Editing password of \"%1\"", - "account/edit/username": "Editing username of \"%1\"", - "account/edit/email": "Editing email of \"%1\"", - "account/following": "People %1 follows", - "account/followers": "People who follow %1", - "account/posts": "Posts made by %1", - "account/topics": "Topics created by %1", - "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "group": "%1 grupp", + "chats": "Chattar", + "chat": "Chattar med %1", + "account/edit": "Redigerar \"%1\"", + "account/edit/password": "Redigerar lösenord för \"%1\"", + "account/edit/username": "Redigerar användarnamn för \"%1\"", + "account/edit/email": "Redigerar e-postadress för \"%1\"", + "account/info": "Account Info", + "account/following": "Användare som %1 följer", + "account/followers": "Användare som följer %1", + "account/posts": "Inlägg skapade av %1", + "account/topics": "Ämnen skapade av %1 ", + "account/groups": "%1's grupper", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Avnändarinställningar", - "account/watched": "Topics watched by %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "account/watched": "Ämnen som bevakas av %1", + "account/upvoted": "Inlägg som röstats upp av %1", + "account/downvoted": "Inlägg som röstats ned av %1", + "account/best": "Bästa inläggen skapade av %1", + "confirm": "E-postadress bekräftad", "maintenance.text": "%1 genomgår underhåll just nu. Vänligen kom tillbaka lite senare.", - "maintenance.messageIntro": "Ytterligare så lämnade administratören detta meddelande:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "maintenance.messageIntro": "Utöver det så lämnade administratören följande meddelande:", + "throttled.text": "%1 ligger tillfälligt nere på grund av överbelastning. Var god återkom senare. " } \ No newline at end of file diff --git a/public/language/sv/recent.json b/public/language/sv/recent.json index 46d7531d3c..0e75be8355 100644 --- a/public/language/sv/recent.json +++ b/public/language/sv/recent.json @@ -6,12 +6,12 @@ "year": "År", "alltime": "Alltid", "no_recent_topics": "Det finns inga olästa ämnen.", - "no_popular_topics": "Det finns inga populära ämnen", - "there-is-a-new-topic": "Det finns ett nytt ämne", + "no_popular_topics": "Det finns inga populära ämnen.", + "there-is-a-new-topic": "Det finns ett nytt ämne.", "there-is-a-new-topic-and-a-new-post": "Det finns ett nytt ämne och ett nytt inlägg.", "there-is-a-new-topic-and-new-posts": "Det finns ett nytt ämne och %1 nya inlägg.", "there-are-new-topics": "Det finns %1 nya ämnen.", - "there-are-new-topics-and-a-new-post": "Det finns %1 nya ämnen och ett nytt inlägg..", + "there-are-new-topics-and-a-new-post": "Det finns %1 nya ämnen och ett nytt inlägg.", "there-are-new-topics-and-new-posts": "Det finns %1 nya ämnen och %2 nya inlägg.", "there-is-a-new-post": "Det finns ett nytt inlägg.", "there-are-new-posts": "Det finns %1 nya inlägg.", diff --git a/public/language/sv/register.json b/public/language/sv/register.json index 6f1aa3764b..f888963295 100644 --- a/public/language/sv/register.json +++ b/public/language/sv/register.json @@ -1,10 +1,11 @@ { "register": "Registrera", - "help.email": "Som standard, är din epost-adress dold för allmänheten.", + "cancel_registration": "Cancel Registration", + "help.email": "Som standard, är din e-postadress dold för allmänheten.", "help.username_restrictions": "Ett unikt användarnamn mellan %1 och %2 bokstäver. Andra kan nämna dig med @<span id='yourUsername'>användarnamn</span>.", "help.minimum_password_length": "Ditt lösenord måste vara minst %1 bokstäver.", - "email_address": "Epost-adress", - "email_address_placeholder": "Ange Epost-adress", + "email_address": "E-postadress", + "email_address_placeholder": "Ange E-postadress", "username": "Användarnamn", "username_placeholder": "Ange användarnamn", "password": "Lösenord", @@ -15,5 +16,8 @@ "alternative_registration": "Alternativ registrering", "terms_of_use": "Användarvillkor", "agree_to_terms_of_use": "Jag godkänner användarvillkoren", - "registration-added-to-queue": "Din registrering har lagts till i kön. Du kommer att få ett mail när den accepteras av en administratör." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Din registrering har lagts till i kön. Du kommer att få ett mail när den accepteras av en administratör.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/sv/reset_password.json b/public/language/sv/reset_password.json index 9f79207d43..a6ee1793e1 100644 --- a/public/language/sv/reset_password.json +++ b/public/language/sv/reset_password.json @@ -7,10 +7,10 @@ "wrong_reset_code.message": "Den mottagna återställningskoden var felaktig. Var god försök igen, eller <a href=\"/reset\">begär en ny återställningskod</a>.", "new_password": "Nytt lösenord", "repeat_password": "Bekräfta lösenord", - "enter_email": "Var god fyll i din <strong>epost-adress</strong> så får du snart en epost med instruktioner hur du återsätller ditt konto.", - "enter_email_address": "Skriv in epostadress", + "enter_email": "Var god fyll i din <strong>e-postadress</strong> så skickas ett e-postmeddelande med instruktioner hur du återställer ditt konto.", + "enter_email_address": "Skriv in e-postadress", "password_reset_sent": "Lösenordsåterställning skickad", - "invalid_email": "Felaktig epost / Epost finns inte!", + "invalid_email": "Felaktig e-post / E-post finns inte!", "password_too_short": "Lösenordet är för kort, var god välj ett annat lösenord.", "passwords_do_not_match": "De två lösenorden du har fyllt i matchar ej varandra.", "password_expired": "Ditt lösenord har gått ut, var god välj ett nytt lösenord." diff --git a/public/language/sv/search.json b/public/language/sv/search.json index eeb5a36feb..3682c8bef5 100644 --- a/public/language/sv/search.json +++ b/public/language/sv/search.json @@ -4,7 +4,7 @@ "advanced-search": "Avancerad sökning", "in": "i", "titles": "Ämnen", - "titles-posts": "Ämnen och Inlägg", + "titles-posts": "Ämnen och inlägg", "posted-by": "Skapad av", "in-categories": "I kategorier", "search-child-categories": "Sök i underkategorier", diff --git a/public/language/sv/tags.json b/public/language/sv/tags.json index b3fbeebd24..cc8a7e02dc 100644 --- a/public/language/sv/tags.json +++ b/public/language/sv/tags.json @@ -1,7 +1,7 @@ { - "no_tag_topics": "Det finns inga ämnen med detta märkord.", - "tags": "Märkord", - "enter_tags_here": "Fyll i märkord på mellan %1 och %2 tecken här.", + "no_tag_topics": "Det finns inga ämnen med denna tagg.", + "tags": "Taggar", + "enter_tags_here": "Fyll i taggar på %1 till %2 tecken här.", "enter_tags_here_short": "Ange taggar...", - "no_tags": "Det finns inga märkord ännu." + "no_tags": "Det finns inga taggar ännu." } \ No newline at end of file diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index 4fa03da4ee..9f2f5b0200 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -13,7 +13,7 @@ "notify_me": "Få notiser om nya svar i detta ämne", "quote": "Citera", "reply": "Svara", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "Svara som ämne", "guest-login-reply": "Logga in för att posta", "edit": "Ändra", "delete": "Ta bort", @@ -26,63 +26,88 @@ "tools": "Verktyg", "flag": "Rapportera", "locked": "Låst", - "bookmark_instructions": "Klicka här för att återvända till det sista olästa inlägget i den här tråden.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Klicka här för att återgå till senast lästa inlägg i detta ämne.", "flag_title": "Rapportera detta inlägg för granskning", "flag_success": "Det här inlägget har flaggats för moderering.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Det här ämnet har raderats. Endast användare med ämneshanterings-privilegier kan se det.", "following_topic.message": "Du kommer nu få notiser när någon gör inlägg i detta ämne.", - "not_following_topic.message": "Du kommer inte längre få notiser från detta ämne.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "Du kommer inte längre se detta ämne i listan olästa ämnen. Du kommer att meddelas när du nämns eller ditt inlägg är upp röstat.", "login_to_subscribe": "Var god registrera eller logga in för att kunna prenumerera på detta ämne.", "markAsUnreadForAll.success": "Ämne markerat som oläst av alla.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "Markera som oläst", + "mark_unread.success": "Ämne markerat som oläst.", "watch": "Bevaka", "unwatch": "Sluta bevaka", "watch.title": "Få notis om nya svar till det här ämnet", "unwatch.title": "Sluta bevaka detta ämne", "share_this_post": "Dela detta inlägg", + "watching": "Bevakar", + "not-watching": "Bevakar inte", + "ignoring": "Ignorerar", + "watching.description": "Meddela mig om nya svar. <br/> Visa ämne i oläst.", + "not-watching.description": "Meddela mig inte om nya svar. <br/> Visa ämne i oläst ifall kategorin är ignorerad.", + "ignoring.description": "Meddela mig inte om nya svar. <br/>Visa inte ämne i oläst.", "thread_tools.title": "Ämnesverktyg", - "thread_tools.markAsUnreadForAll": "Markera som oläst", - "thread_tools.pin": "Fäst ämne", + "thread_tools.markAsUnreadForAll": "Mark unread for all", + "thread_tools.pin": "Nåla fast ämne", "thread_tools.unpin": "Lösgör ämne", "thread_tools.lock": "Lås ämne", - "thread_tools.unlock": "Öppna upp ämne", + "thread_tools.unlock": "Lås upp ämne", "thread_tools.move": "Flytta ämne", - "thread_tools.move_all": "Flytta alla.", + "thread_tools.move_all": "Flytta alla", "thread_tools.fork": "Grena ämne", "thread_tools.delete": "Ta bort ämne", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "Radera inlägg", "thread_tools.delete_confirm": "Är du säker på att du vill ta bort det här ämnet?", "thread_tools.restore": "Återställ ämne", "thread_tools.restore_confirm": "Är du säker på att du vill återställa det här ämnet?", - "thread_tools.purge": "Rensa ämne", - "thread_tools.purge_confirm": "Är du säker att du vill rensa ut det här ämnet?", + "thread_tools.purge": "Rensa bort ämne", + "thread_tools.purge_confirm": "Är du säker att du vill rensa bort det här ämnet?", "topic_move_success": "Det här ämnet har flyttats till %1", "post_delete_confirm": "Är du säker på att du vill ta bort det här inlägget?", "post_restore_confirm": "Är du säker på att du vill återställa det här inlägget?", - "post_purge_confirm": "Är du säker att du vill rensa ut det här inlägget?", + "post_purge_confirm": "Är du säker att du vill rensa bort det här inlägget?", "load_categories": "Laddar kategorier", "disabled_categories_note": "Inaktiverade kategorier är utgråade", "confirm_move": "Flytta", "confirm_fork": "Grena", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Bokmärke", + "favourites": "Bokmärken", + "favourites.has_no_favourites": "Du har inte lagt till bokmärke på något inlägg än.", "loading_more_posts": "Laddar fler inlägg", "move_topic": "Flytta ämne", "move_topics": "Flytta ämnen", "move_post": "Flytta inlägg", "post_moved": "Inlägget flyttades.", "fork_topic": "Grena ämne", - "topic_will_be_moved_to": "Detta ämne kommer bli flytta till kategori", + "topic_will_be_moved_to": "Detta ämne kommer att flyttas till kategorin", "fork_topic_instruction": "Klicka på de inlägg du vill grena", "fork_no_pids": "Inga inlägg valda!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Ämnet har blivit förgrenat. Klicka här för att gå till det förgrenade ämnet.", - "delete_posts_instruction": "Click the posts you want to delete/purge", + "delete_posts_instruction": "Klicka på inläggen du vill radera/rensa bort", "composer.title_placeholder": "Skriv in ämnets titel här...", "composer.handle_placeholder": "Namn", "composer.discard": "Avbryt", - "composer.submit": "Skicka", + "composer.submit": "Posta inlägg", "composer.replying_to": "Svarar till %1", "composer.new_topic": "Nytt ämne", "composer.uploading": "laddar upp...", @@ -92,21 +117,21 @@ "composer.thumb_file_label": "Eller ladda upp en fil", "composer.thumb_remove": "Töm fält", "composer.drag_and_drop_images": "Dra och släpp bilder här", - "more_users_and_guests": "%1 fler användare() och %2 gäst(er)", - "more_users": "%1 fler användare()", + "more_users_and_guests": "%1 fler användare och %2 gäst(er)", + "more_users": "%1 fler användare", "more_guests": "1% fler gäst(er)", "users_and_others": "%1 och %2 andra", "sort_by": "Sortera på", "oldest_to_newest": "Äldst till nyaste", "newest_to_oldest": "Nyaste till äldst", - "most_votes": "Mest röster", - "most_posts": "Felst inlägg", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", + "most_votes": "Flest röster", + "most_posts": "Flest inlägg", + "stale.title": "Skapa nytt ämne istället?", + "stale.warning": "Ämnet du svarar på är ganska gammalt. Vill du skapa ett nytt ämne istället och inkludera en referens till det här ämnet i ditt inlägg?", + "stale.create": "Skapa nytt ämne", + "stale.reply_anyway": "Svara på ämnet ändå", "link_back": "Re: [%1](%2)", "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "offensive": "Kränkande", + "custom-flag-reason": "Ange skälet för rapporteringen" } \ No newline at end of file diff --git a/public/language/sv/unread.json b/public/language/sv/unread.json index 0ed0a80ae7..06ae0bbe92 100644 --- a/public/language/sv/unread.json +++ b/public/language/sv/unread.json @@ -2,9 +2,12 @@ "title": "Olästa", "no_unread_topics": "Det finns inga olästa ämnen.", "load_more": "Ladda fler", - "mark_as_read": "Markerad som läst", + "mark_as_read": "Markera som läst", "selected": "Vald", "all": "Alla", "all_categories": "Alla kategorier", - "topics_marked_as_read.success": "Ämnet markerat som läst." + "topics_marked_as_read.success": "Ämnet markerat som läst.", + "all-topics": "Alla ämnen", + "new-topics": "Nya ämnen", + "watched-topics": "Bevakade ämnen" } \ No newline at end of file diff --git a/public/language/sv/uploads.json b/public/language/sv/uploads.json index 1622cb5693..4546acd888 100644 --- a/public/language/sv/uploads.json +++ b/public/language/sv/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Laddar upp filen...", + "select-file-to-upload": "Välj en fil att ladda upp!", + "upload-success": "Filen laddades upp!", + "maximum-file-size": "Maximalt %1 kb" } \ No newline at end of file diff --git a/public/language/sv/user.json b/public/language/sv/user.json index ab893029e1..e039bb98eb 100644 --- a/public/language/sv/user.json +++ b/public/language/sv/user.json @@ -1,11 +1,12 @@ { - "banned": "Bannad", + "banned": "Bannlyst", "offline": "Offline", "username": "Användarnamn", "joindate": "Gick med", "postcount": "Antal inlägg", - "email": "Epost", - "confirm_email": "Bekräfta epostadress ", + "email": "E-post", + "confirm_email": "Bekräfta e-postadress ", + "account_info": "Account Info", "ban_account": "Bannlys konto", "ban_account_confirm": "Vill du verkligen bannlysa den här användaren?", "unban_account": "Ta bort bannlysning", @@ -22,7 +23,7 @@ "profile": "Profil", "profile_views": "Profil-visningar", "reputation": "Rykte", - "favourites": "Bookmarks", + "favourites": "Bokmärken", "watched": "Bevakad", "followers": "Följare", "following": "Följer", @@ -30,20 +31,20 @@ "signature": "Signatur", "birthday": "Födelsedag", "chat": "Chatta", - "chat_with": "Chat with %1", + "chat_with": "Chatta med %1", "follow": "Följ", "unfollow": "Sluta följ", "more": "Mer", "profile_update_success": "Profilen uppdaterades.", "change_picture": "Ändra bild", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Ändra användarnamn", + "change_email": "Ändra e-postadress", "edit": "Ändra", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "Redigera profil", + "default_picture": "Standard-ikon", "uploaded_picture": "Uppladdad bild", "upload_new_picture": "Ladda upp ny bild", - "upload_new_picture_from_url": "Ladda Upp Ny Bild Från Länk", + "upload_new_picture_from_url": "Ladda upp ny bild via länk", "current_password": "Nuvarande lösenord", "change_password": "Ändra lösenord", "change_password_error": "Ogiltigt lösenord.", @@ -56,54 +57,73 @@ "password": "Lösenord", "username_taken_workaround": "Användarnamnet är redan upptaget, så vi förändrade det lite. Du kallas nu för <strong>%1</strong>", "password_same_as_username": "Ditt lösenord är samma som ditt användarnamn, välj ett annat lösenord.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "Ditt lösenord är detsamma som din e-postadress. Var god välj ett annat lösenord.", "upload_picture": "Ladda upp bild", "upload_a_picture": "Ladda upp en bild", "remove_uploaded_picture": "Ta bort uppladdad bild", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Ladda upp omslagsbild", "settings": "Inställningar", - "show_email": "Visa min epost", - "show_fullname": "Visa Fullständigt Namn", + "show_email": "Visa min e-postadress", + "show_fullname": "Visa fullständigt namn", "restrict_chats": "Tillåt endast chatt-meddelanden från användare som jag följer", "digest_label": "Prenumerera på sammanställt flöde", - "digest_description": "Prenumerera på epostuppdateringar för det här forumet (notiser och ämnen) med en viss regelbundenhet", + "digest_description": "Prenumerera på e-postuppdateringar för det här forumet (notiser och ämnen) med en viss regelbundenhet", "digest_off": "Avslagen", - "digest_daily": "Daligen", + "digest_daily": "Dagligen", "digest_weekly": "Veckovis", "digest_monthly": "Månadsvis", - "send_chat_notifications": "Skicka ett epostmeddelande om nya chatt-meddelanden tas emot när jag inte är online.", - "send_post_notifications": "Skicka ett epost när svar kommit på ämnen jag prenumererar på till", + "send_chat_notifications": "Skicka ett e-postmeddelande om nya chatt-meddelanden tas emot när jag inte är online.", + "send_post_notifications": "Skicka ett e-postmeddelande när svar tillkommit på ämnen jag prenumererar på", "settings-require-reload": "Vissa inställningar som ändrades kräver att sidan laddas om. Klicka här för att ladda om sidan.", "has_no_follower": "Denna användare har inga följare :(", "follows_no_one": "Denna användare följer ingen :(", - "has_no_posts": "Användaren har inte skrivit några inlägg ännu", - "has_no_topics": "Användaren har inte skrivit några ämnen ännu", - "has_no_watched_topics": "Användaren har inte bevakat några ämnen ännu", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", - "email_hidden": "Epost dold", + "has_no_posts": "Användaren har inte skrivit några inlägg ännu.", + "has_no_topics": "Användaren har inte postat några ämnen ännu.", + "has_no_watched_topics": "Användaren har inte bevakat några ämnen ännu.", + "has_no_upvoted_posts": "Den här användaren har inte röstat upp några inlägg än.", + "has_no_downvoted_posts": "Den här användaren har inte röstat ned några inlägg än.", + "has_no_voted_posts": "Den här användaren har inga inlägg med röster", + "email_hidden": "E-post dold", "hidden": "dold", "paginate_description": "Gör så att ämnen och inlägg visas som sidor istället för oändlig skroll", "topics_per_page": "Ämnen per sida", "posts_per_page": "Inlägg per sida", "notification_sounds": "Spela ett ljud när du får en notis", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Inställning för bläddring", - "open_links_in_new_tab": "Öppna utgående länkar på ny flik", - "enable_topic_searching": "Aktivera Sökning Inom Ämne", + "open_links_in_new_tab": "Öppna utgående länkar i ny flik", + "enable_topic_searching": "Aktivera sökning inom ämne", "topic_search_help": "Om aktiverat kommer sökning inom ämne överskrida webbläsarens vanliga funktionen för sökning bland sidor och tillåta dig att söka genom hela ämnet istället för det som endast visas på skärmen.", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Följ ämnen som du svarat på", - "follow_topics_you_create": "Följ ämnen du skapat", - "grouptitle": "Välj tittel för gruppen så som du vill att den ska visas", + "delay_image_loading": "Fördröj inladdning av bilder", + "image_load_delay_help": "Aktivera för att hindra bilder ifrån att ladda in, innan de skrollats fram på skärmen. ", + "scroll_to_my_post": "Visa det nya inlägget när ett svar har postats", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Grupptitel", "no-group-title": "Ingen titel på gruppen", "select-skin": "Välj ett Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", - "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", - "sso.title": "Single Sign-on Services", - "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "select-homepage": "Välj en startsida", + "homepage": "Startsida", + "homepage_description": "Välj en sida som ska användas som forumets startsida eller 'Ingen' för att använda standardstartsidan.", + "custom_route": "Sökväg till egen startsida", + "custom_route_help": "Skriv in ett sökvägsnamn här, utan föregående slash. (tex \"recent\" eller \"popular\")", + "sso.title": "Single Sign-on-tjänster", + "sso.associated": "Associerad med", + "sso.not-associated": "Klicka här för att associera med", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/sv/users.json b/public/language/sv/users.json index 29f91b76c0..639b11ac14 100644 --- a/public/language/sv/users.json +++ b/public/language/sv/users.json @@ -2,6 +2,7 @@ "latest_users": "Senaste användarna", "top_posters": "Flest inlägg", "most_reputation": "Bäst rykte", + "most_flags": "Most Flags", "search": "Sök", "enter_username": "Ange ett användarnamn för att söka", "load_more": "Ladda fler", @@ -15,6 +16,6 @@ "popular_topics": "Populära ämnen", "unread_topics": "Olästa ämnen", "categories": "Kategorier", - "tags": "Märkord", - "no-users-found": "No users found!" + "tags": "Taggar", + "no-users-found": "Inga användare hittades!" } \ No newline at end of file diff --git a/public/language/th/category.json b/public/language/th/category.json index f8dcbeb3d8..1802bb7f1b 100644 --- a/public/language/th/category.json +++ b/public/language/th/category.json @@ -10,7 +10,11 @@ "share_this_category": "แชร์ Category นี้", "watch": "Watch", "ignore": "ไม่ต้องสนใจอีก", - "watch.message": "You are now watching updates from this category", - "ignore.message": "You are now ignoring updates from this category", + "watching": "Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread", + "ignoring.description": "Do not show topics in unread", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories" } \ No newline at end of file diff --git a/public/language/th/email.json b/public/language/th/email.json index 50a596b128..6881135565 100644 --- a/public/language/th/email.json +++ b/public/language/th/email.json @@ -24,6 +24,7 @@ "digest.day": "day", "digest.week": "week", "digest.month": "month", + "digest.subject": "Digest for %1", "notif.chat.subject": "New chat message received from %1", "notif.chat.cta": "กดตรงนี้เพื่อกลับไปยังบทสนทนา", "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", diff --git a/public/language/th/error.json b/public/language/th/error.json index 5fa5730b94..98b15e4d95 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -14,20 +14,25 @@ "invalid-password": "รหัสผ่านไม่ถูกต้อง", "invalid-username-or-password": "กรุณาระบุชื่อผู้ใช้และรหัสผ่าน", "invalid-search-term": "ข้อความค้นหาไม่ถูกต้อง", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", "username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว", "email-taken": "อีเมลนี้มีการใช้แล้ว", "email-not-confirmed": "ยังไม่มีการยืนยันอีเมลของคุณ, โปรดกดยืนยันอีเมลของคุณตรงนี้", "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", "no-email-to-confirm": "Forum นี้ต้องการการยืนยันอีเมล กรุณากดที่นี่เพื่อระบุอีเมล", "email-confirm-failed": "เราไม่สามารถยืนยันอีเมลของคุณ ณ ขณะนี้ กรุณาลองใหม่อีกครั้งภายหลัง", "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", "username-too-short": "ชื่อบัญชีผู้ใช้ สั้นเกินไป", "username-too-long": "ชื่อบัญชีผู้ใช้ ยาวเกินไป", "password-too-long": "Password too long", "user-banned": "User banned", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", "no-category": "ยังไม่มี Category นี้", "no-topic": "ยังไม่มี Topic นี้", "no-post": "ยังไม่มี Post นี้", @@ -38,10 +43,26 @@ "category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว", "topic-locked": "Topic Locked", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", @@ -55,10 +76,12 @@ "already-unfavourited": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", "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-name-too-long": "Group name too long", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "Already part of this group", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "You are not allowed to edit this message", "cant-remove-last-user": "You can't remove the last user", "cant-delete-chat-message": "You are not allowed to delete this message", + "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", "not-enough-reputation-to-downvote": "You do not have enough reputation to downvote this post", @@ -99,5 +123,9 @@ "no-session-found": "No login session found!", "not-in-room": "User not in room", "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/th/global.json b/public/language/th/global.json index 36acafc94d..cf889d3a28 100644 --- a/public/language/th/global.json +++ b/public/language/th/global.json @@ -7,8 +7,10 @@ "403.login": "Perhaps you should <a href='%1/login'>try logging in</a>?", "404.title": "ไม่พบ", "404.message": "You seem to have stumbled upon a page that does not exist. Return to the <a href='%1/'>home page</a>.", - "500.title": "มีข้อผิดพลาดภายในระบบ", + "500.title": "Internal Error.", "500.message": "อุ่ย! มีสิ่งที่ไม่ถูกต้องเกิดขึ้น!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "ลงทะเบียน", "login": "เข้าสู่ระบบ", "please_log_in": "กรุณาเข้าสู่ระบบ", @@ -17,6 +19,7 @@ "welcome_back": "ยินดีต้อนรับ", "you_have_successfully_logged_in": "คุณได้เข้าสู่ระบบแล้ว", "save_changes": "บันทึกการเปลี่ยนแปลง", + "save": "Save", "close": "ปิด", "pagination": "ให้เลขหน้า", "pagination.out_of": "%1 จาก %2", @@ -50,7 +53,9 @@ "topics": "Topics", "posts": "กระทู้", "best": "Best", + "upvoters": "Upvoters", "upvoted": "Upvoted", + "downvoters": "Downvoters", "downvoted": "Downvoted", "views": "ดู", "reputation": "Reputation", @@ -90,5 +95,8 @@ "enter_page_number": "Enter page number", "upload_file": "Upload file", "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/th/groups.json b/public/language/th/groups.json index ad202d2ceb..4fbb940168 100644 --- a/public/language/th/groups.json +++ b/public/language/th/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Leave Group", "membership.reject": "Reject", "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/th/login.json b/public/language/th/login.json index 5eba336a39..bb4729f5c6 100644 --- a/public/language/th/login.json +++ b/public/language/th/login.json @@ -5,7 +5,8 @@ "remember_me": "จำไว้ในระบบ?", "forgot_password": "ลืมรหัสผ่าน?", "alternative_logins": "เข้าสู่ระบบโดยทางอื่น", - "failed_login_attempt": "เข้าสู่ระบบล้มเหลว โปรดลองอีกครั้ง", + "failed_login_attempt": "Login Unsuccessful", "login_successful": "คุณเข้าสู่ระบบเรียบร้อยแล้ว", - "dont_have_account": "คุณยังไม่มีบัญชีเข้าระบบ?" + "dont_have_account": "คุณยังไม่มีบัญชีเข้าระบบ?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/th/modules.json b/public/language/th/modules.json index 15a7bb1fa5..8763167692 100644 --- a/public/language/th/modules.json +++ b/public/language/th/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Picture", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", "bootbox.confirm": "Confirm", diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json index dd81dfdb7e..3d1e6d9866 100644 --- a/public/language/th/notifications.json +++ b/public/language/th/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", diff --git a/public/language/th/pages.json b/public/language/th/pages.json index e017b8df5e..eb18cbfa0a 100644 --- a/public/language/th/pages.json +++ b/public/language/th/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Users with the most posts", "users/sort-reputation": "Users with the most reputation", "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", "users/search": "User Search", "notifications": "แจ้งเตือน", "tags": "Tags", "tag": "Topics tagged under \"%1\"", "register": "Register an account", + "registration-complete": "Registration complete", "login": "Login to your account", "reset": "Reset your account password", "categories": "Categories", @@ -28,12 +30,13 @@ "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/th/register.json b/public/language/th/register.json index 333898d689..191d023847 100644 --- a/public/language/th/register.json +++ b/public/language/th/register.json @@ -1,5 +1,6 @@ { "register": "ลงทะเบียน", + "cancel_registration": "Cancel Registration", "help.email": "ผู้ใช้อื่น ๆ จะไม่สามารถมองเห็นอีเมลของคุณโดยดีฟอลต์", "help.username_restrictions": "ชื่อผู้ใช้ที่ไม่ซ้ำกับผู้อื่น จะต้องมีความยาวระหว่าง% 1% และ %2 ตัวอักษร ผู้ใช้อื่นๆ สามารถพูดถึงคุณโดย @<span id='yourUsername'>ชื่อผู้ใช้</ span>", "help.minimum_password_length": "ความยาวรหัสผ่านของคุณต้องมีอย่างน้อย %1 ตัวอักษร", @@ -15,5 +16,8 @@ "alternative_registration": "เข้าสู่ระบบโดยทางอื่น", "terms_of_use": "เงื่อนไขการใช้งาน", "agree_to_terms_of_use": "ยอมรับข้อตกลงในการใช้งาน", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "interstitial.intro": "We require some additional information before we can create your account.", + "interstitial.errors-found": "We could not complete your registration:" } \ No newline at end of file diff --git a/public/language/th/topic.json b/public/language/th/topic.json index 19cecadf9d..047fc51d79 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -26,12 +26,30 @@ "tools": "เครื่องมือ", "flag": "ปักธง", "locked": "Locked", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "Pinned", + "moved": "Moved", + "bookmark_instructions": "Click here to return to the last read post in this thread.", "flag_title": "ปักธงโพสต์นี้เพื่อดำเนินการ", "flag_success": "This post has been flagged for moderation.", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Topic นี้ถูกลบไปแล้ว เฉพาะผู้ใช้งานที่มีสิทธิ์ในการจัดการ Topic เท่านั้นที่จะมีสิทธิ์ในการเข้าชม", "following_topic.message": "คุณจะได้รับการแจ้งเตือนเมื่อมีคนโพสต์ในกระทู้นี้", - "not_following_topic.message": "คุณจะไม่รับการแจ้งเตือนจากกระทู้นี้", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", "login_to_subscribe": "กรุณาลงทะเบียนหรือเข้าสู่ระบบเพื่อที่จะติดตามกระทู้นี้", "markAsUnreadForAll.success": "ทำเครื่องหมายว่ายังไม่ได้อ่านทั้งหมด", "mark_unread": "Mark unread", @@ -41,8 +59,14 @@ "watch.title": "ให้แจ้งเตือนเมื่อมีการตอบกลับ Topic นี้", "unwatch.title": "ยกเลิกการติดตาม Topic นี้", "share_this_post": "แชร์โพสต์นี้", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.<br/>Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.", "thread_tools.title": "เครื่องมือช่วยจัดการ Topic", - "thread_tools.markAsUnreadForAll": "ทำหมายว่ายังไม่ได้อ่าน", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "ปักหมุดกระทู้", "thread_tools.unpin": "เลิกปักหมุดกระทู้", "thread_tools.lock": "ล็อคกระทู้", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "กระทู้นี้จะถูกย้ายไปที่หมวดหมู่", "fork_topic_instruction": "คลิกที่โพสที่คุณต้องการที่จะแยก", "fork_no_pids": "ไม่มีโพสต์ที่เลือก!", + "fork_pid_count": "%1 post(s) selected", "fork_success": "Successfully forked topic! Click here to go to the forked topic.", "delete_posts_instruction": "Click the posts you want to delete/purge", "composer.title_placeholder": "ป้อนชื่อกระทู้ของคุณที่นี่ ...", diff --git a/public/language/th/unread.json b/public/language/th/unread.json index 6928cb1366..00f800a723 100644 --- a/public/language/th/unread.json +++ b/public/language/th/unread.json @@ -6,5 +6,8 @@ "selected": "เลือก", "all": "ทั้งหมด", "all_categories": "All categories", - "topics_marked_as_read.success": "Topic ถูกทำเครื่องหมายว่าอ่านแล้วเรียบร้อย" + "topics_marked_as_read.success": "Topic ถูกทำเครื่องหมายว่าอ่านแล้วเรียบร้อย", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics" } \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json index 092f19cf70..24ea6aca74 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -6,6 +6,7 @@ "postcount": "จำนวนโพส", "email": "อีเมล์", "confirm_email": "ยืนยันอีเมล", + "account_info": "Account Info", "ban_account": "Ban Account", "ban_account_confirm": "Do you really want to ban this user?", "unban_account": "Unban Account", @@ -88,14 +89,21 @@ "topics_per_page": "จำนวนกระทู้ต่อหน้า", "posts_per_page": "จำนวนโพสต์ต่อหน้า", "notification_sounds": "Play a sound when you receive a notification", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "เปิดดูการตั้งค่า", "open_links_in_new_tab": "Open outgoing links in new tab", "enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic", "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "delay_image_loading": "Delay Image Loading", + "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", "no-group-title": "No group title", "select-skin": "Select a Skin", "select-homepage": "Select a Homepage", @@ -105,5 +113,17 @@ "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", "sso.title": "Single Sign-on Services", "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "sso.not-associated": "Click here to associate with", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/th/users.json b/public/language/th/users.json index a3f7bcb85e..423257816b 100644 --- a/public/language/th/users.json +++ b/public/language/th/users.json @@ -2,6 +2,7 @@ "latest_users": "ผู้ใช้งานล่าสุด", "top_posters": "ผู้ที่โพสต์มากที่สุด", "most_reputation": "ผู้ที่มีชื่อเสียงมากที่สุด", + "most_flags": "Most Flags", "search": "ค้นหา", "enter_username": "ใส่ชื่อผู้ใช้เพื่อค้นหา", "load_more": "โหลดเพิ่มเติม", diff --git a/public/language/tr/category.json b/public/language/tr/category.json index 836f7a2113..4317cfc2ec 100644 --- a/public/language/tr/category.json +++ b/public/language/tr/category.json @@ -10,7 +10,11 @@ "share_this_category": "Bu kategoriyi paylaş", "watch": "İzle", "ignore": "Yoksay", - "watch.message": "Şuan bu kategorideki güncellemeleri izliyorsunuz", - "ignore.message": "Şuan bu kategoriden güncellemeleri gizliyorsunuz", + "watching": "İzleniyor", + "ignoring": "Yoksayılıyor", + "watching.description": "Okunmamış başlıkları göster", + "ignoring.description": "Okunmamış başlıkları gösterme", + "watch.message": "Sen bu kategori ve bütün alt kategorileri hareketleri izlemeye aldın", + "ignore.message": "Sen bu kategori ve bütün alt kategorileri hareketlere karşı susturdun", "watched-categories": "Takip edilen kategoriler" } \ No newline at end of file diff --git a/public/language/tr/email.json b/public/language/tr/email.json index 7dabee41c7..4353efd42a 100644 --- a/public/language/tr/email.json +++ b/public/language/tr/email.json @@ -24,6 +24,7 @@ "digest.day": "gün", "digest.week": "hafta", "digest.month": "ay", + "digest.subject": "%1 için özet", "notif.chat.subject": "Okunmamış bazı iletileriniz var", "notif.chat.cta": "Sohbete devam etmek için buraya tıklayın", "notif.chat.unsub.info": "Bu bildirim şectiğiniz ayarlar yüzünden gönderildi.", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index bb03c66177..471fbbd8d9 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -14,20 +14,25 @@ "invalid-password": "Geçersiz Şifre", "invalid-username-or-password": "Lütfen kullanıcı ismi ve parola girin.", "invalid-search-term": "Geçersiz arama", + "csrf-invalid": "Büyük olasılıkla süresi dolmuş oturum nedeniyle girişinizi geçersiz kıldık. Lütfen tekrar deneyiniz.", "invalid-pagination-value": "Geçersiz sayfalama değeri, en az %1 ve en fazla %2 olabilir", "username-taken": "Kullanıcı İsmi Alınmış", "email-taken": "E-posta Alınmış", "email-not-confirmed": "E-postanız onaylanmamış, onaylamak için lütfen buraya tıklayın.", "email-not-confirmed-chat": "E-postanız onaylanana kadar sohbet edemezsiniz, onaylamak için lütfen buraya tıklayın.", + "email-not-confirmed-email-sent": "E-postanız onaylanmamış, onaylamak için lütfen gelen kutunuzu kontrol edin.", "no-email-to-confirm": "Bu forum e-posta doğrulaması gerektirir, lütfen buraya bir e-posta adresi girin", "email-confirm-failed": "E-posta adresinizi doğrulayamıyoruz. Lütfen daha sonra tekrar deneyin.", "confirm-email-already-sent": "E-mail onayı zaten gönderilmiş, yeni bir onay göndermek için lütfen 1 dakika bekleyin.", + "sendmail-not-found": "Sendmail yürütülemedi, lüften indirildiğinden ve NodeBB kullanıcısı tarafından uygulanabilir olduğundan emin olun.", "username-too-short": "Kullanıcı ismi çok kısa", "username-too-long": "Kullanıcı ismi çok uzun.", "password-too-long": "Parola çok uzun", "user-banned": "Kullanıcı Yasaklı", + "user-banned-reason": "Kullanıcı yasaklandı (Sebep: %1)", "user-too-new": "Özür dileriz, ilk iletinizi yapmadan önce %1 saniye beklemeniz gerekiyor", "blacklisted-ip": "Üzgünüz, IP adresiniz, bu toplulukta yasaklandı. Bunun bir hata olduğunu düşünüyorsanız, bir yönetici ile irtibata geçiniz.", + "ban-expiry-missing": "Bu yasak için bir bitiş tarihi girin", "no-category": "Kategori Yok", "no-topic": "Başlık Yok", "no-post": "İleti Yok", @@ -38,10 +43,26 @@ "category-disabled": "Kategori aktif değil", "topic-locked": "Başlık Kilitli", "post-edit-duration-expired": "Gönderilen iletiler %1 saniyeden sonra değiştirilemez", + "post-edit-duration-expired-minutes": "Gönderildikten %1 dakika(s) sonra iletini düzenlemene izin verilir.", + "post-edit-duration-expired-minutes-seconds": "Gönderildikten %1 dakika(s) %2 saniye(s) sonra iletini düzenlemene izin verilir.", + "post-edit-duration-expired-hours": "Gönderildikten %1 saat(s) sonra iletini düzenlemene izin verilir.", + "post-edit-duration-expired-hours-minutes": "Gönderildikten %1 saat(s) %2 dakika(s) sonra iletini düzenlemene izin verilir.", + "post-edit-duration-expired-days": "Gönderildikten %1 gün(s) sonra iletini düzenlemene izin verilir.", + "post-edit-duration-expired-days-hours": "Gönderildikten %1 gün(s) %2 saat(s) sonra iletini düzenlemene izin verilir.", + "post-delete-duration-expired": "Gönderildikten %1 saniye(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-minutes": "Gönderildikten %1 dakika(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-minutes-seconds": "Gönderildikten %1 dakika(s) %2 saniye(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-hours": "Gönderildikten %1 saat(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-hours-minutes": "Gönderildikten %1 saat(s) %2 dakika(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-days": "Gönderildikten %1 gün(s) sonra iletini silmene izin verilir.", + "post-delete-duration-expired-days-hours": "Gönderildikten %1 gün(s) %2 saat(s) sonra iletini silmene izin verilir.", + "cant-delete-topic-has-reply": "Bir ileti varken başlığı silemezsiniz", + "cant-delete-topic-has-replies": "Başlığına %1 ileti girildikten sonra silemezsin", "content-too-short": "Lütfen daha uzun bir ileti girin. En az %1 karakter.", "content-too-long": "Lütfen daha kısa bir ileti girin. İletiler %1 karakterden uzun olamaz.", "title-too-short": "Lütfen daha uzun bir başlık girin. Başlıklar en az %1 karakter içermelidir.", "title-too-long": "Lütfen daha kısa bir başlık girin. Başlıklar %1 karakterden uzun olamaz.", + "category-not-selected": "Kategori seçilmedi", "too-many-posts": "%1 saniye içinde yalnızca bir ileti gönderebilirsiniz - tekrar ileti göndermeden önce lütfen bekleyin.", "too-many-posts-newbie": "Yeni bir kullanıcı olarak, %2 saygınlık kazanana kadar %1 saniye içinde bir ileti gönderebilirsiniz - tekrar ileti göndermeden önce lütfen bekleyin.", "tag-too-short": "Lütfen daha uzun bir etiket girin. Etiketler en az %1 karakter içermelidir.", @@ -55,10 +76,12 @@ "already-unfavourited": "Bu iletiyi zaten yer imlerinizden çıkardınız", "cant-ban-other-admins": "Başka yöneticileri yasaklayamazsınız!", "cant-remove-last-admin": "Tek yönetici sizsiniz. Kendinizi adminlikten çıkarmadan önce başka bir kullanıcıyı admin olarak ekleyiniz", + "cant-delete-admin": "Öncelikle yönetici izinlerini kaldırman gerekiyor.", "invalid-image-type": "Geçersiz resim uzantısı. Izin verilen uzantılar: %1", "invalid-image-extension": "Geçersiz resim uzantısı", "invalid-file-type": "Geçersiz dosya türü. İzin verilenler şunlar : %1", "group-name-too-short": "Grup ismi çok kısa", + "group-name-too-long": "Grup adı çok uzun", "group-already-exists": "Grup zaten var", "group-name-change-not-allowed": "Grup ismini değiştiremezsiniz", "group-already-member": "Bu grubun zaten bir parçasısın.", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Bu mesajı düzenlemek için izin verilmez", "cant-remove-last-user": "Son kullanıcıyı silemezsiniz", "cant-delete-chat-message": "Bu mesajı silmek için izin verilmez", + "already-voting-for-this-post": "Bu gönderi için zaten oy verdin.", "reputation-system-disabled": "Saygınlık sistemi kapatılmış.", "downvoting-disabled": "Aşagı oylama kapatılmış", "not-enough-reputation-to-downvote": "Bu iletiyi aşagı oylamak için yeterince saygınlığınız yok.", @@ -99,5 +123,9 @@ "no-session-found": "Giriş yapılmış bir oturum bulunamadı!", "not-in-room": "Odada kullanıcı yok", "no-users-in-room": "Bu odada kullanıcı yok", - "cant-kick-self": "Kendinizi gruptan atamazsınız." + "cant-kick-self": "Kendinizi gruptan atamazsınız.", + "no-users-selected": "Seçili kullanıcı(s) bulunamadı", + "invalid-home-page-route": "Geçersiz anasayfa yolu", + "invalid-session": "Oturum Uyuşmazlığı", + "invalid-session-text": "Senin giriş oturumun pek aktif gözükmüyor, ya da sunucu ile eşleşmiyor. Lütfen sayfayı yenileyiniz." } \ No newline at end of file diff --git a/public/language/tr/global.json b/public/language/tr/global.json index 2cf32930e5..5e00072cb0 100644 --- a/public/language/tr/global.json +++ b/public/language/tr/global.json @@ -6,9 +6,11 @@ "403.message": "Erişim izniniz olmayan bir sayfaya denk gelmiş gibisiniz.", "403.login": "Belki de tekrar <a href='%1/login'>giriş</a> yapmayı denersiniz?", "404.title": "Bulunamadı", - "404.message": "Erişim izniniz olmayan bir sayfaya denk gelmiş gibisiniz.<a href='%1/'>Anasayfa</a>ya geri dönün.", + "404.message": "Erişim izniniz olmayan bir sayfaya denk gelmiş gibisiniz. <a href='%1/'>Anasayfa</a>'ya geri dönün.", "500.title": "Dahili hata.", "500.message": "Ups! Bir şeyler ters gitti sanki!", + "400.title": "Geçersiz istek.", + "400.message": "Bu bağlantı bozuk gibi gözüküyor, lütfen bir kez daha kontrol edin. Aksi taktirde <a href='%1/'>anasayfaya</a> geri dönün.", "register": "Kayıt Ol", "login": "Giriş", "please_log_in": "Lütfen Giriş Yapınız", @@ -17,6 +19,7 @@ "welcome_back": "Tekrar Hoşgeldiniz", "you_have_successfully_logged_in": "Başarıyla giriş yaptınız!", "save_changes": "Değişiklikleri kaydet", + "save": "Kaydet", "close": "Kapat", "pagination": "Sayfalara numara koyma", "pagination.out_of": "%1 - %2", @@ -50,7 +53,9 @@ "topics": "Başlık", "posts": "İleti", "best": "En İyi", + "upvoters": "Artı", "upvoted": "Artı", + "downvoters": "Eksi", "downvoted": "Eksi", "views": "Görünüm", "reputation": "Saygınlık", @@ -90,5 +95,8 @@ "enter_page_number": "Sayfa numarasını girin", "upload_file": "Dosya yükle", "upload": "Yükle", - "allowed-file-types": "İzin verilen dosya tipleri %1" + "allowed-file-types": "İzin verilen dosya tipleri %1", + "unsaved-changes": "Kaydedilmemiş değişiklikler var. Çıkmak istediğinize emin misiniz?", + "reconnecting-message": "%1 ile bağlantınız koptu, yeniden bağlanmak için lütfen bekleyiniz.", + "play": "Oynat" } \ No newline at end of file diff --git a/public/language/tr/groups.json b/public/language/tr/groups.json index a67cd1b2cb..ef28334ed6 100644 --- a/public/language/tr/groups.json +++ b/public/language/tr/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "Gruptan Ayrıl", "membership.reject": "Reddet", "new-group.group_name": "Grup İsmi:", - "upload-group-cover": "Grup kapağı yükle" + "upload-group-cover": "Grup kapağı yükle", + "bulk-invite-instructions": "Bu gruba davet etmek için virgülle ayrılmış adlarının bir listesini girin", + "bulk-invite": "Toplu Davet" } \ No newline at end of file diff --git a/public/language/tr/login.json b/public/language/tr/login.json index bd13e81d46..5db8629859 100644 --- a/public/language/tr/login.json +++ b/public/language/tr/login.json @@ -5,7 +5,8 @@ "remember_me": "Beni Hatırla!", "forgot_password": "Şifrenizi mi unuttunuz?", "alternative_logins": "Alternatif Girişler", - "failed_login_attempt": "Oturum açma girişimi başarısız, lütfen tekrar deneyin.", + "failed_login_attempt": "Giriş Başarısız", "login_successful": "Başarıyla giriş yaptınız!", - "dont_have_account": "Hesabınız yok mu?" + "dont_have_account": "Hesabınız yok mu?", + "logged-out-due-to-inactivity": "Hareketsizlik nedeniyle yönetici panelinden çıkış yapıldı" } \ No newline at end of file diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index b896b442a0..625712c2c3 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Gönder ve Kitle", "composer.toggle_dropdown": "Menü aç", "composer.uploading": "Yükleniyor %1", + "composer.formatting.bold": "Kalın", + "composer.formatting.italic": "İtalik", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Üstüçizili", + "composer.formatting.link": "Bağlantı", + "composer.formatting.picture": "Görsel", + "composer.upload-picture": "Görsel Yükle", + "composer.upload-file": "Dosya Yükle", + "composer.zen_mode": "Zen Modu", + "composer.select_category": "Bir kategori seç", "bootbox.ok": "Tamam", "bootbox.cancel": "İptal", "bootbox.confirm": "Onayla", diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index c4e7a2276b..5474cc8872 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> ve %2 iki kişi daha <strong>%3</strong> içindeki gönderini beğendi.", "moved_your_post": "<strong>%1</strong> senin iletin <strong>%2</strong> taşındı", "moved_your_topic": "<strong>%1</strong> taşındı <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> <strong>%2</strong> içindeki gönderini yer imlerine ekledi.", - "favourited_your_post_in_dual": "<strong>%1</strong> ve <strong>%2</strong> <strong>%3</strong> gönderini yer imlerine ekledi.", - "favourited_your_post_in_multiple": "<strong>%1</strong> ve %2 kişi daha <strong>%3</strong> gönderini yer imlerine ekledi.", "user_flagged_post_in": "<strong>%1</strong> bir iletiyi bayrakladı. <strong>%2</strong>", "user_flagged_post_in_dual": " <strong>%1</strong> ve <strong>%2</strong> <strong>%3</strong> gönderini bayrakladı", "user_flagged_post_in_multiple": "<strong>%1</strong> ve %2 kişi daha <strong>%3</strong> gönderini bayrakladı", diff --git a/public/language/tr/pages.json b/public/language/tr/pages.json index b076bf11d9..509efe63a4 100644 --- a/public/language/tr/pages.json +++ b/public/language/tr/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "En çok ileti gönderen kullanıcılar", "users/sort-reputation": "En çok saygınlığı olan kullanıcılar", "users/banned": "Yasaklanmış Kullanıcılar", + "users/most-flags": "En Fazla Bayraklanan Kullanıcılar", "users/search": "Kullanıcı Ara", "notifications": "Bildirimler", "tags": "Etiketler", "tag": "“%1“ ile etiketlenmiş konular", "register": "Bir hesap aç", + "registration-complete": "Kayıt tamamlandı", "login": "Hesabına giriş yap", "reset": "Hesap şifreni baştan yarat", "categories": "Kategoriler", @@ -28,12 +30,13 @@ "account/edit/password": "\"%1\" parolayı düzenliyor", "account/edit/username": "\"%1\" kullanıcı adını düzenliyor", "account/edit/email": "\"%1\" email adresini düzenliyor", + "account/info": "Hesap Hakkında", "account/following": "%1 tarafından takip edilenler", "account/followers": "%1 takip edenler", "account/posts": "%1 tarafından gönderilen iletiler", "account/topics": "%1 tarafından gönderilen başlıklar", "account/groups": "%1 Kişisine Ait Gruplar", - "account/favourites": "%1'in Yer imleri", + "account/bookmarks": "%1'in yer imine eklenmiş iletiler", "account/settings": "Kullanıcı Ayarları", "account/watched": "%1 tarafından izlenen konular", "account/upvoted": "%1 tarafından artılanan gönderiler", diff --git a/public/language/tr/register.json b/public/language/tr/register.json index c46c27ad32..a11bf78747 100644 --- a/public/language/tr/register.json +++ b/public/language/tr/register.json @@ -1,5 +1,6 @@ { "register": "Kayıt Ol", + "cancel_registration": "Kaydı İptal Et", "help.email": "E-posta adresiniz varsayılan olarak topluluktan gizlidir.", "help.username_restrictions": "%1 ve %2 karakter arası bir kullanıcı ismi. Başkaları sizden @<span id='yourUsername'>isim</span> kullanarak bahsedebilir.", "help.minimum_password_length": "Şifreniz en az %1 karakter olmalı", @@ -15,5 +16,8 @@ "alternative_registration": "Alternatif Kayıt", "terms_of_use": "Kullanım Şartları", "agree_to_terms_of_use": "Kullanım Şartlarını kabul ediyorum", - "registration-added-to-queue": "Kayıt olma isteğiniz kabul listesine eklenmiştir. Yönetici tarafından kabul edildiğinizde mail alacaksınız." + "terms_of_use_error": "Kullanım şartlarını kabul etmen gerekiyor", + "registration-added-to-queue": "Kayıt olma isteğiniz kabul listesine eklenmiştir. Yönetici tarafından kabul edildiğinizde mail alacaksınız.", + "interstitial.intro": "Hesabınızı yaratmadan önce bazı ekstra bilgiler gerekiyor.", + "interstitial.errors-found": "Kaydınınız tamamlanmadı:" } \ No newline at end of file diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index e12c92f005..bda00055ed 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -26,12 +26,30 @@ "tools": "Araçlar", "flag": "Bayrak", "locked": "Kilitli", - "bookmark_instructions": "Bu başlıkta okuduğun en son iletiye gitmek için buraya tıklayınız", + "pinned": "İğnelendi", + "moved": "Taşındı", + "bookmark_instructions": "Bu başlıkta en son kaldığın yere dönmek için tıklayın.", "flag_title": "Bu iletiyi moderatöre haber et", "flag_success": "Bu ileti yöneticilere bildirildi.", + "flag_manage_title": "%1 ileti bayraklandı", + "flag_manage_history": "Hareket Tarihi", + "flag_manage_no_history": "Hiçbir olay geçmişi yok", + "flag_manage_assignee": "Vekil", + "flag_manage_state": "Bildir", + "flag_manage_state_open": "Yeni/Açık", + "flag_manage_state_wip": "Yapım Aşamasında", + "flag_manage_state_resolved": "Çözüldü", + "flag_manage_state_rejected": "Reddedildi", + "flag_manage_notes": "Paylaşılan Not", + "flag_manage_update": "Bayram Durumunu Güncelle", + "flag_manage_history_assignee": "%1 atandı", + "flag_manage_history_state": "%1 bildiri güncellendi", + "flag_manage_history_notes": "Bayrak notu güncellendi", + "flag_manage_saved": "Bayrak Detayları Güncellendi", "deleted_message": "Bu başlık silindi. Sadece başlık düzenleme yetkisi olan kullanıcılar görebilir.", "following_topic.message": "Artık bir kullanıcı bu başlığa ileti gönderdiğinde bildirim alacaksınız.", - "not_following_topic.message": "Artık bu başlık için bildirim almayacaksınız.", + "not_following_topic.message": "Bu başlığı okunmamışlarda göreceksiniz ama biri bir şey yazdığında bildirim gelmeyecek.", + "ignoring_topic.message": "Bu başlığı okunmamış başlıklar alanında görmeyeceksin. Eğer bir iletide bahsedilirsen veya iletin oylanırsa bildiri alacaksın.", "login_to_subscribe": "Lütfen bu iletiyi başlığa üye olmak için giriş yapın.", "markAsUnreadForAll.success": "Başlık herkes için okunmadı olarak işaretlendi.", "mark_unread": "Okunmadı olarak işaretle", @@ -41,8 +59,14 @@ "watch.title": "Bu başlığa gelen yeni iletilerden haberdar ol", "unwatch.title": "Bu konuyu izleme", "share_this_post": "Bu iletiyi paylaş", + "watching": "Takip Et", + "not-watching": "Takip etme", + "ignoring": "Sustur", + "watching.description": "Yeni bir ileti geldiğinde beni bildir.<br/>Okunmamış olarak göster.", + "not-watching.description": "Yeni bir ileti geldiğinde bildirme.<br/>Kategori susturulmamışsa okunmamış olarak göster.", + "ignoring.description": "Yeni bir ileti geldiğinde bildirme.<br/>Okunmamış olarak gösterme.", "thread_tools.title": "Konu Ayaları", - "thread_tools.markAsUnreadForAll": "Okunmadı Olarak İşaretle", + "thread_tools.markAsUnreadForAll": "Hepsini okundu işaretle", "thread_tools.pin": "Başlığı İğnele", "thread_tools.unpin": "Başlığı İğneleme", "thread_tools.lock": "Başlığı Kitle", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Bu konu kategorisine taşınacak", "fork_topic_instruction": "Ayırmak istediğiniz iletileri tıklayın", "fork_no_pids": "Hiç bir ileti seçilmedi!", + "fork_pid_count": "%1 ileti(ler) seçildi", "fork_success": "Başlık başarıyla ayrıldı!", "delete_posts_instruction": "Silmek/temizlemek istediğiniz iletilere tıklayın.", "composer.title_placeholder": "Başlık ismini buraya girin...", diff --git a/public/language/tr/unread.json b/public/language/tr/unread.json index 49b8995dc5..4ef01109e7 100644 --- a/public/language/tr/unread.json +++ b/public/language/tr/unread.json @@ -6,5 +6,8 @@ "selected": "Seçili", "all": "Hepsi", "all_categories": "Tüm kategoriler", - "topics_marked_as_read.success": "Başlıklar okundu olarak işaretlendi!" + "topics_marked_as_read.success": "Başlıklar okundu olarak işaretlendi!", + "all-topics": "Tüm Başlıklar", + "new-topics": "Yeni Başlıklar", + "watched-topics": "İzlenen Başlıklar" } \ No newline at end of file diff --git a/public/language/tr/user.json b/public/language/tr/user.json index 1664a6ec81..3ba395164f 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -6,6 +6,7 @@ "postcount": "Yayın Sayısı", "email": "E-posta", "confirm_email": "E-posta onayla", + "account_info": "Hesap Bilgi", "ban_account": "Hesabı Dondur", "ban_account_confirm": "Hesabı dondurmak istediğinizden emin misiniz!", "unban_account": "Hesabı Kullanıma Aç", @@ -88,14 +89,21 @@ "topics_per_page": "Sayfa başına Konular", "posts_per_page": "Sayfa başına İletiler", "notification_sounds": "Uyarı alındığında ses çal", + "notifications_and_sounds": "Bildiriler & Sesler", + "incoming-message-sound": "Gelen mesaj sesi", + "outgoing-message-sound": "Giden ileti sesi", + "notification-sound": "Bildiri sesi", + "no-sound": "Ses yok", "browsing": "Tarayıcı Ayaları", "open_links_in_new_tab": "Dışarı giden bağlantıları yeni sekmede aç", "enable_topic_searching": "Konu içi aramayı aktive et", "topic_search_help": "Aktive edilirse, konu içi arama tarayıcının normal arama davranışını değiştirerek tüm konuyu aramanızı sağlar", + "delay_image_loading": "Gecikmeli Görsel Yükleme", + "image_load_delay_help": "Aktif hale getirmeniz halinde başlıktaki görseller, sayfa kaydırıldıkca yüklenecek", "scroll_to_my_post": "Cevap yazdıktan sonra yeni gönderiyi göster", - "follow_topics_you_reply_to": "Cevap verdiğim konuları takip et", - "follow_topics_you_create": "Kendi konularımı takip et", - "grouptitle": "Göstermek istediğiniz gurup başlığını seçin", + "follow_topics_you_reply_to": "Cevap verdiğin başlıkları izle", + "follow_topics_you_create": "Oluşturduğunuz başlıkları izle", + "grouptitle": "Grup Başlığı", "no-group-title": "Grup başlığı yok", "select-skin": "Bir deri şeçin", "select-homepage": "Bir anasayfa seçin", @@ -105,5 +113,17 @@ "custom_route_help": "Herhangi bir eğik çizgi olmadan, burada bir yol adını girin (örneğin \"yeniler\" veya \"popüler\")", "sso.title": "Tek giriş servisleri", "sso.associated": "Birleştirilmiş", - "sso.not-associated": "Birleştirmek için buraya tıklayın" + "sso.not-associated": "Birleştirmek için buraya tıklayın", + "info.latest-flags": "Son Bayraklar", + "info.no-flags": "Hiç bayraklanan bir ileti bulunamadı", + "info.ban-history": "Yasaklama Olayları", + "info.no-ban-history": "Bu kullanıcı hiç yasaklanmadı", + "info.banned-until": "Yasaklama süresi %1", + "info.banned-permanently": "Kalıcı yasakla", + "info.banned-reason-label": "Gerekçe", + "info.banned-no-reason": "Gerekçe belirtilmedi.", + "info.username-history": "Kullanıcı Adı Geçmişi", + "info.email-history": "Email Geçmişi", + "info.moderation-note": "Moderasyon Notu", + "info.moderation-note.success": "Moderasyon notu kaydedildi" } \ No newline at end of file diff --git a/public/language/tr/users.json b/public/language/tr/users.json index fa0bc51096..92dbf55d42 100644 --- a/public/language/tr/users.json +++ b/public/language/tr/users.json @@ -2,6 +2,7 @@ "latest_users": "En Yeni Kullanıcılar", "top_posters": "En Fazla Gönderim Yapanlar", "most_reputation": "En Saygın", + "most_flags": "En Fazla Bayrak", "search": "Ara", "enter_username": "Aramak için bir kullanıcı adı girin", "load_more": "Daha Fazla Yükle", diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 77836d6492..494fa8b153 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -10,7 +10,11 @@ "share_this_category": "Chia sẻ chuyên mục này", "watch": "Theo dõi", "ignore": "Bỏ qua", - "watch.message": "Bạn đang theo dõi các cập nhật của danh mục này", - "ignore.message": "Bạn đã tắt cập nhật từ chuyên mục này", - "watched-categories": "Chuyên mục đang theo dõi" + "watching": "Đang theo dõi", + "ignoring": "Bỏ qua", + "watching.description": "Hiện các chủ đề chưa đọc", + "ignoring.description": "Không hiện những chủ đề chưa đọc", + "watch.message": "Bạn đang theo dõi các cập nhật ở chuyên mục này và các chuyên mục con", + "ignore.message": "Bạn đang bỏ qua các cập nhật ở chuyên mục này và các chuyên mục con", + "watched-categories": "Các chuyên mục đã xem" } \ No newline at end of file diff --git a/public/language/vi/email.json b/public/language/vi/email.json index de77b60125..30b3fb658a 100644 --- a/public/language/vi/email.json +++ b/public/language/vi/email.json @@ -24,6 +24,7 @@ "digest.day": "ngày", "digest.week": "tuần", "digest.month": "tháng", + "digest.subject": "Tóm tắt cho %1", "notif.chat.subject": "Bạn có tin nhắn mới từ %1", "notif.chat.cta": "Nhấn vào đây để tiếp tục cuộc hội thoại", "notif.chat.unsub.info": "Thông báo tin nhắn này được gửi tới dựa theo cài đặt theo dõi của bạn.", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index fd5b2a903e..13082bec01 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -14,20 +14,25 @@ "invalid-password": "Mật khẩu không hợp lệ", "invalid-username-or-password": "Xin hãy nhập cả tên đăng nhập và mật khẩu", "invalid-search-term": "Từ khóa không hợp lệ", + "csrf-invalid": "Hệ thống không cho phép bạn đăng nhập, có vẻ như phiên đăng nhập cũ đã hết hạn. Hãy thử đăng nhập lại", "invalid-pagination-value": "Giá trị trang không hợp lệ, tối thiểu phải là %1 và tối đa là %2", "username-taken": "Tên đăng nhập đã tồn tại", "email-taken": "Email đã được đăng kí", "email-not-confirmed": "Email của bạn chưa được xác nhận, xin hãy nhấn vào đây để xác nhận địa chỉ này là của bạn", "email-not-confirmed-chat": "Bạn không được quyền chat nếu email của bạn chưa được xác nhận, vui lòng click vào đây để xác nhận email của bạn.", + "email-not-confirmed-email-sent": "Email của bạn chưa được xác nhận, xin hãy nhấn vào đây để xác nhận địa chỉ này là của bạn", "no-email-to-confirm": "Diễn đàn này yêu cầu xác nhận email, vui lòng nhấn vào đây để nhập email.", "email-confirm-failed": "Chúng tôi không thể xác nhận email của bạn, vui lòng thử lại sau.", "confirm-email-already-sent": "Email xác nhận đã được gửi, vui lòng chờ %1 phút để yêu cầu gửi lại.", + "sendmail-not-found": "Không tìm thấy lệnh thực thi \"sendmail\", hãy chắc chắn nó đã được cài đặt và thực thi bởi người quản trị đang vận hành NodeBB", "username-too-short": "Tên đăng nhập quá ngắn", "username-too-long": "Tên đăng nhập quá dài", "password-too-long": "Mật khẩu quá dài", "user-banned": "Tài khoản bị ban", + "user-banned-reason": "User banned (Reason: %1)", "user-too-new": "Rất tiếc, bạn phải chờ %1 giây để đăng bài viết đầu tiên.", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "blacklisted-ip": "Rất tiếc, địa chỉ IP của bạn đã bị cấm khỏi cộng đồng. Nếu bạn cảm thấy có gì không đúng, hãy liên lạc với người quản trị.", + "ban-expiry-missing": "Vui lòng cung cấp ngày hết hạn của lệnh cấm", "no-category": "Danh mục không tồn tại", "no-topic": "Chủ đề không tồn tại", "no-post": "Bài viết không tồn tại", @@ -38,10 +43,26 @@ "category-disabled": "Danh mục bị khóa", "topic-locked": "Chủ đề bị khóa", "post-edit-duration-expired": "Bạn chỉ được phép sửa bài viết sau khi đăng %1 giây.", + "post-edit-duration-expired-minutes": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 phút(s)", + "post-edit-duration-expired-minutes-seconds": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 phút(s) %2 giây(s)", + "post-edit-duration-expired-hours": "Bạn chỉ được phép sửa bài viết sau khi đăng %1 giờ(s).", + "post-edit-duration-expired-hours-minutes": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 giờ(s) %2 phút(s)", + "post-edit-duration-expired-days": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 ngày(s)", + "post-edit-duration-expired-days-hours": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", + "post-delete-duration-expired": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giây(s)", + "post-delete-duration-expired-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 phút(s)", + "post-delete-duration-expired-minutes-seconds": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 phút(s) %2 giây(s)", + "post-delete-duration-expired-hours": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s)", + "post-delete-duration-expired-hours-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s) 2 phút(s)", + "post-delete-duration-expired-days": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s)", + "post-delete-duration-expired-days-hours": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", "content-too-short": "Vui lòng nhập một bài viết dài hơn. Bài viết phải có tối thiểu %1 ký tự.", "content-too-long": "Vui lòng nhập một bài viết ngắn hơn. Bài viết chỉ có thể có tối đa %1 ký tự.", "title-too-short": "Vui lòng nhập tiêu đề dài hơn. Tiêu đề phải có tối thiểu %1 ký tự.", "title-too-long": "Vui lòng nhập tiêu đề ngắn hơn. Tiêu đề chỉ có thể có tối đa %1 ký tự.", + "category-not-selected": "Category not selected.", "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", "too-many-posts-newbie": "Bạn chỉ có thể đăng bài mỗi %1 giây cho đến khi bạn tích luỹ được %2 điểm tín nhiệm - vui lòng đợi để tiếp tục đăng bài.", "tag-too-short": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", @@ -51,14 +72,16 @@ "still-uploading": "Vui lòng chờ upload", "file-too-big": "Kích cỡ file được cho phép tối đa là %1 kB - vui lòng tải lên file có dung lượng nhỏ hơn.", "guest-upload-disabled": "Khách (chưa có tài khoản) không có quyền tải lên file.", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", - "cant-ban-other-admins": "Bạn không thể ban được các admin khác", + "already-favourited": "Bạn đã đánh dấu bài viết này", + "already-unfavourited": "Bạn đã bỏ đánh dấu bài viết này", + "cant-ban-other-admins": "Bạn không thể cấm được các quản trị viên khác", "cant-remove-last-admin": "Bạn là quản trị viên duy nhất. Hãy cho thành viên khác làm quản trị viên trước khi huỷ bỏ quyền quản trị của bạn.", + "cant-delete-admin": "Hủy quyền quản trị của tài khoản này trước khi xóa", "invalid-image-type": "Định dạng ảnh không hợp lệ. Những định dạng được cho phép là: %1", "invalid-image-extension": "Định dạng ảnh không hợp lệ", "invalid-file-type": "Định dạng file không hợp lệ. Những định dạng được cho phép là: %1", "group-name-too-short": "Tên nhóm quá ngắn", + "group-name-too-long": "Tên nhóm quá dài", "group-already-exists": "Nhóm đã tồn tại", "group-name-change-not-allowed": "Không cho phép đổi tên nhóm", "group-already-member": "Bạn đã là thành viên của nhóm này.", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "Bạn không được phép chỉnh sửa tin nhắn này", "cant-remove-last-user": "Bạn không thể xoá thành viên cuối cùng", "cant-delete-chat-message": "Bạn không được phép xoá tin nhắn này", + "already-voting-for-this-post": "Bạn đã bỏ phiếu cho bài viết này", "reputation-system-disabled": "Hệ thống tín nhiệm đã bị vô hiệu hóa.", "downvoting-disabled": "Downvote đã bị tắt", "not-enough-reputation-to-downvote": "Bạn không có đủ phiếu tín nhiệm để downvote bài này", @@ -98,6 +122,10 @@ "invite-maximum-met": "Bạn đã sử dụng hết số lượng lời mời bạn có thể gửi (%1 đã gửi trên tổng số %2 được cho phép)", "no-session-found": "Không tìm thấy phiên đăng nhập!", "not-in-room": "Thành viên không có trong phòng", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "no-users-in-room": "Không có ai trong phòng này", + "cant-kick-self": "Bạn không thể kick chính bạn ra khỏi nhóm", + "no-users-selected": "Chưa có người dùng(s) nào", + "invalid-home-page-route": "Đường dẫn trang chủ không hợp lệ", + "invalid-session": "Session Mismatch", + "invalid-session-text": "It looks like your login session is no longer active, or no longer matches with the server. Please refresh this page." } \ No newline at end of file diff --git a/public/language/vi/global.json b/public/language/vi/global.json index e466d74fae..625d1a0e6c 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -7,8 +7,10 @@ "403.login": "Có lẽ bạn nên <a href='%1/login'>thử đăng nhập</a>?", "404.title": "Không tìm thấy", "404.message": "Có vẻ như bạn đang cố vào một trang không tồn tại. Hãy trở lại <a href='%1/'>trang chủ</a>.", - "500.title": "Lỗi nội bộ", + "500.title": "Internal Error.", "500.message": "Úi chà! Có vẻ như có trục trặc rồi!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the <a href='%1/'>home page</a>.", "register": "Đăng ký", "login": "Đăng nhập", "please_log_in": "Xin hãy đăng nhập", @@ -17,6 +19,7 @@ "welcome_back": "Chào mừng bạn quay lại", "you_have_successfully_logged_in": "Bạn đã đăng nhập thành công", "save_changes": "Lưu thay đổi", + "save": "Lưu", "close": "Đóng lại", "pagination": "Số trang", "pagination.out_of": "%1 trong số %2", @@ -50,7 +53,9 @@ "topics": "Số Chủ đề", "posts": "Số bài viết", "best": "Hay nhất", + "upvoters": "Tán thành", "upvoted": "Tán thành", + "downvoters": "Phản đối", "downvoted": "Phản đối", "views": "Lượt xem", "reputation": "Điểm tín nhiệm", @@ -87,8 +92,11 @@ "map": "Bản đồ", "sessions": "Phiên đăng nhập", "ip_address": "Địa chỉ IP", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "enter_page_number": "Nhập vào số trang", + "upload_file": "Tải file lên", + "upload": "Tải lên", + "allowed-file-types": "Các định dạng file được cho phép là %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play" } \ No newline at end of file diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json index 1bb89ad728..d8a037b662 100644 --- a/public/language/vi/groups.json +++ b/public/language/vi/groups.json @@ -41,7 +41,7 @@ "details.hidden": "Đã ẩn", "details.hidden_help": "Nếu bật, nhóm này sẽ không được hiện thị trong danh sách nhóm, và thành viên phải được mời để tham gia", "details.delete_group": "Xoá nhóm", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.private_system_help": "Các nhóm kín được vô hiệu hóa bởi hệ thống, tùy chọn này không thực hiện bất cứ điều gì cả", "event.updated": "Thông tin nhóm đã được cập nhật", "event.deleted": "Nhóm \"%1\" đã bị xoá", "membership.accept-invitation": "Chấp nhận lời mời", @@ -50,5 +50,7 @@ "membership.leave-group": "Rời khỏi nhóm", "membership.reject": "Từ chối", "new-group.group_name": "Tên nhóm", - "upload-group-cover": "Upload group cover" + "upload-group-cover": "Tải ảnh bìa lên cho nhóm", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/vi/login.json b/public/language/vi/login.json index 01b7f2dc96..ea130d9ada 100644 --- a/public/language/vi/login.json +++ b/public/language/vi/login.json @@ -5,7 +5,8 @@ "remember_me": "Ghi nhớ?", "forgot_password": "Quên mật khẩu?", "alternative_logins": "Đăng nhập bằng tài khoản khác", - "failed_login_attempt": "Đăng nhập thất bại, xin hãy thử lại", + "failed_login_attempt": "Đăng nhập không thành công", "login_successful": "Bạn đã đăng nhập thành công!", - "dont_have_account": "Chưa có tài khoản?" + "dont_have_account": "Chưa có tài khoản?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity" } \ No newline at end of file diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 0f5ba2fe46..8b6f21e739 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -6,7 +6,7 @@ "chat.user_typing": "%1b đang gõ...", "chat.user_has_messaged_you": "%1 đã gửi tin cho bạn.", "chat.see_all": "Xem tất cả", - "chat.mark_all_read": "Mark all chats read", + "chat.mark_all_read": "Đánh dấu đã đọc toàn bộ hội thoại", "chat.no-messages": "Hãy chọn 1 tài khoản để xem lịch sử chat", "chat.no-users-in-room": "Không có người nào trong phòng này.", "chat.recent-chats": "Vừa chat", @@ -29,6 +29,16 @@ "composer.submit_and_lock": "Đăng và Khoá", "composer.toggle_dropdown": "Đóng/mở dropdown", "composer.uploading": "Đang tải lên %1", + "composer.formatting.bold": "In đậm", + "composer.formatting.italic": "In nghiêng", + "composer.formatting.list": "Danh sách", + "composer.formatting.strikethrough": "Gạch ngang", + "composer.formatting.link": "Đường dẫn", + "composer.formatting.picture": "Hình ảnh", + "composer.upload-picture": "Tải ảnh lên", + "composer.upload-file": "Tải file lên", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Chọn một chuyên mục", "bootbox.ok": "OK", "bootbox.cancel": "Huỷ bỏ", "bootbox.confirm": "Xác nhận", diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json index 1520437ea9..8433a1353d 100644 --- a/public/language/vi/notifications.json +++ b/public/language/vi/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> và %2 others đã tán thành với bài viết của bạn trong <strong>%3</strong>.", "moved_your_post": "<strong>%1</strong> đã chuyển bài viết của bạn tới <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> đã chuyển <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", "user_flagged_post_in": "<strong>%1</strong> gắn cờ 1 bài trong <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> và <strong>%2</strong> đã gắn cờ một bài viết trong <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> và %2 người khác đã gắn cờ bài viết của bạn trong <strong>%3</strong>", @@ -30,7 +27,7 @@ "user_started_following_you_dual": "<strong>%1</strong> và <strong>%2</strong> đã bắt đầu theo dõi bạn.", "user_started_following_you_multiple": "<strong>%1</strong> và %2 người khác đã bắt đầu theo dõi bạn.", "new_register": "<strong>%1</strong> đã gửi một yêu cầu tham gia.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", + "new_register_multiple": "Có <strong>%1</strong> đơn đăng ký đang chờ xem xét.", "email-confirmed": "Đã xác nhận email", "email-confirmed-message": "Cảm ơn bạn đã xác nhận địa chỉ email của bạn. Tài khoản của bạn đã được kích hoạt đầy đủ.", "email-confirm-error-message": "Đã có lỗi khi xác nhận địa chỉ email. Có thể đoạn mã không đúng hoặc đã hết hạn.", diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json index 13b6ab018d..fbf216329f 100644 --- a/public/language/vi/pages.json +++ b/public/language/vi/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "Thành viên có nhiều bài đăng nhất", "users/sort-reputation": "Thành viên có điểm tín nhiệm cao nhất", "users/banned": "Thành viên đã bị cấm", + "users/most-flags": "Most flagged users", "users/search": "Tìm kiếm thành viên", "notifications": "Thông báo", "tags": "Tag", "tag": "Chủ đề được gắn tag \"%1\"", "register": "Đăng ký một tài khoản mới", + "registration-complete": "Đăng ký hoàn tất", "login": "Đăng nhập vào tài khoản của bạn", "reset": "Phục hồi mật khẩu của bạn", "categories": "Chuyên mục", @@ -28,18 +30,19 @@ "account/edit/password": "Chỉnh sửa mật khẩu của \"%1\"", "account/edit/username": "Chỉnh sửa tên đăng nhập của \"%1\"", "account/edit/email": "Chỉnh sửa email của \"%1\"", + "account/info": "Thông tin tài khoản", "account/following": "Thành viên %1 đang theo dõi", "account/followers": "Thành viên đang theo dõi %1", "account/posts": "Bài viết được đăng bởi %1", "account/topics": "Chủ đề được tạo bởi %1", "account/groups": "Nhóm của %1", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "Thiết lập", "account/watched": "Chủ đề %1 đang theo dõi", "account/upvoted": "Bài viết %1 tán thành", "account/downvoted": "Bài viết %1 phản đối", "account/best": "Bài viết hay nhất của %1", - "confirm": "Email Confirmed", + "confirm": "Đã xác nhận email", "maintenance.text": "%1 đang được bảo trì. Xin vui lòng quay lại sau.", "maintenance.messageIntro": "Ban quản lí để lại lời nhắn sau:", "throttled.text": "%1 hiện đang bị quá tải. Vui lòng quay lại sau." diff --git a/public/language/vi/register.json b/public/language/vi/register.json index 91e70a7d0b..07a70bebe7 100644 --- a/public/language/vi/register.json +++ b/public/language/vi/register.json @@ -1,5 +1,6 @@ { "register": "Đăng ký", + "cancel_registration": "Hủy đăng ký", "help.email": "Theo mặc định, Email của bạn sẽ được ẩn và public sẽ không thấy được", "help.username_restrictions": "Một tên truy cập duy nhất có từ %1 đến %2 ký tự. Những người khác có thể nhắc đến bạn bằng @<span id='yourUsername'>tên truy cập</span>.", "help.minimum_password_length": "Mật khẩu của bạn phải có ít nhất %1 ký tự", @@ -15,5 +16,8 @@ "alternative_registration": "Đăng ký tài khoản khác", "terms_of_use": "Điều khoản sử dụng", "agree_to_terms_of_use": "Tôi đồng ý với các điều khoản sử dụng", - "registration-added-to-queue": "Yêu cầu đăng ký của bạn đang chờ được chấp thuận. Bạn sẽ nhận được email khi tài khoản của bạn đã được chấp thuận bởi quản trị viên." + "terms_of_use_error": "Bạn phải đồng ý với các điều khoản sử dụng", + "registration-added-to-queue": "Yêu cầu đăng ký của bạn đang chờ được chấp thuận. Bạn sẽ nhận được email khi tài khoản của bạn đã được chấp thuận bởi quản trị viên.", + "interstitial.intro": "Chúng tôi cần thêm một số thông tin trước khi bạn có thể tạo tài khoản.", + "interstitial.errors-found": "Chúng tôi không thể hoàn thành đăng ký của bạn:" } \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 683acae9fa..a5603031f4 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -26,12 +26,30 @@ "tools": "Công cụ", "flag": "Gắn cờ", "locked": "Khóa", - "bookmark_instructions": "Nhấn vào đây để trở lại bài viết cuối cùng mà bạn chưa đọc trong chủ đề này.", + "pinned": "Đã ghim", + "moved": "Chuyển đi", + "bookmark_instructions": "Bấm vào đây để quay về đọc bài viết mới nhất trong chủ đề này.", "flag_title": "Flag bài viết này để chỉnh sửa", "flag_success": "Chủ đề này đã được flag để chỉnh sửa", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "Chủ đề này đã bị xóa. Chỉ ban quản trị mới xem được.", "following_topic.message": "Từ giờ bạn sẽ nhận được thông báo khi có ai đó gửi bài viết trong chủ đề này", - "not_following_topic.message": "Bạn sẽ không còn nhận được thông báo từ chủ đề này", + "not_following_topic.message": "Bạn có thể xem chủ đề này trong danh sách chủ đề chưa xem, nhưng bạn sẽ không nhận thông báo khi có ai đó đăng bài viết trong chủ đề này", + "ignoring_topic.message": "Bạn sẽ không còn xem được chủ đề này trong danh sách các chủ đề chưa đọc nữa. Bạn sẽ nhận được thông báo khi bạn được đề cập tới hoặc bài viết của bạn được bỏ phiếu.", "login_to_subscribe": "Xin hãy đăng ký hoặc đăng nhập để theo dõi topic này", "markAsUnreadForAll.success": "Chủ đề đã được đánh dấu là chưa đọc toàn bộ", "mark_unread": "Đánh dấu chưa đọc", @@ -41,8 +59,14 @@ "watch.title": "Được thông báo khi có trả lời mới trong chủ đề này", "unwatch.title": "Ngừng theo dõi chủ đề này", "share_this_post": "Chia sẻ bài viết này", + "watching": "Đang xem", + "not-watching": "Không xem", + "ignoring": "Bỏ qua", + "watching.description": "Thông báo cho tôi các trả lời mới. <br/>Hiển thị các mục chưa đọc", + "not-watching.description": "Không thông báo tôi các trả lời mới. <br/>Hiển thị mục chưa đọc nếu danh mục bị bỏ qua.", + "ignoring.description": "Không thông báo tôi các trả lời mới. <br/>Không hiển thị các mục chưa đọc.", "thread_tools.title": "Công cụ", - "thread_tools.markAsUnreadForAll": "Đánh dấu chưa đọc", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "Pin chủ đề", "thread_tools.unpin": "Bỏ pin chủ đề", "thread_tools.lock": "Khóa chủ đề", @@ -65,9 +89,9 @@ "disabled_categories_note": "Các phần mục bị khóa đã được đánh xám", "confirm_move": "Di chuyển", "confirm_fork": "Tạo bảo sao", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "Đánh dấu", + "favourites": "Các đánh dấu", + "favourites.has_no_favourites": "Bạn chưa đánh dấu bất kỳ bài viết nào.", "loading_more_posts": "Tải thêm các bài gửi khác", "move_topic": "Chuyển chủ đề", "move_topics": "Di chuyển chủ đề", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "Chủ đề này sẽ được chuyển tới phần mục", "fork_topic_instruction": "Chọn vào bài gửi mà bạn muốn fork", "fork_no_pids": "Chưa chọn bài gửi nào!", + "fork_pid_count": "%1 bài viết(s) đã được gửi", "fork_success": "Tạo bản sao thành công! Nhấn vào đây để chuyển tới chủ đề vừa tạo.", "delete_posts_instruction": "Chọn những bài viết bạn muốn xoá", "composer.title_placeholder": "Nhập tiêu đề cho chủ đề của bạn tại đây...", diff --git a/public/language/vi/unread.json b/public/language/vi/unread.json index 14233ae900..db616b77e7 100644 --- a/public/language/vi/unread.json +++ b/public/language/vi/unread.json @@ -6,5 +6,8 @@ "selected": "Đã chọn", "all": "Tất cả", "all_categories": "Tất cả chuyên mục", - "topics_marked_as_read.success": "Chủ đề được đánh dấu đã đọc" + "topics_marked_as_read.success": "Chủ đề được đánh dấu đã đọc", + "all-topics": "Toàn bộ chủ đề", + "new-topics": "Các chủ đề mới", + "watched-topics": "Các chủ đề đã xem" } \ No newline at end of file diff --git a/public/language/vi/uploads.json b/public/language/vi/uploads.json index 1622cb5693..8ab633f25f 100644 --- a/public/language/vi/uploads.json +++ b/public/language/vi/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "Đang tải file lên...", + "select-file-to-upload": "Chọn một file để tải lên!", + "upload-success": "Tải file lên thành công!", + "maximum-file-size": "Tối đa %1 kb" } \ No newline at end of file diff --git a/public/language/vi/user.json b/public/language/vi/user.json index c98cd1b9f9..b640e88ddd 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -6,6 +6,7 @@ "postcount": "Số bài viết", "email": "Email", "confirm_email": "Xác nhận email", + "account_info": "Thông tin tài khoản", "ban_account": "Cấm thành viên", "ban_account_confirm": "Bạn có chắc bạn muốn cấm thành viên này?", "unban_account": "Bỏ cấm thành viên", @@ -22,7 +23,7 @@ "profile": "Hồ sơ", "profile_views": "Số lượt người ghé thăm", "reputation": "Mức uy tín", - "favourites": "Bookmarks", + "favourites": "Các đánh dấu", "watched": "Đã theo dõi", "followers": "Số người theo dõi", "following": "Đang theo dõi", @@ -39,7 +40,7 @@ "change_username": "Đổi tên đăng nhập", "change_email": "Đổi email", "edit": "Chỉnh sửa", - "edit-profile": "Edit Profile", + "edit-profile": "Sửa thông tin cá nhân", "default_picture": "Icon mặc định", "uploaded_picture": "Ảnh đã tải lên", "upload_new_picture": "Tải lên ảnh mới", @@ -56,11 +57,11 @@ "password": "Mật khẩu", "username_taken_workaround": "Tên truy cập này đã tồn tại, vì vậy chúng tôi đã sửa đổi nó một chút. Tên truy cập của bạn giờ là <strong>%1</strong>", "password_same_as_username": "Mật khẩu của bạn trùng với tên đăng nhập, vui lòng chọn một mật khẩu khác.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "password_same_as_email": "Mật khẩu của bạn trùng với email của bạn, hãy chọn mật khẩu khác.", "upload_picture": "Tải lên hình ảnh", "upload_a_picture": "Tải lên một hình ảnh", "remove_uploaded_picture": "Xoá ảnh đã tải lên", - "upload_cover_picture": "Upload cover picture", + "upload_cover_picture": "Tải ảnh bìa lên", "settings": "Thiết lập", "show_email": "Hiện Email của tôi", "show_fullname": "Hiện tên đầy đủ", @@ -88,14 +89,21 @@ "topics_per_page": "Số chủ đề trong một trang", "posts_per_page": "Số bài viết trong một trang", "notification_sounds": "Phát âm thanh khi bạn nhận được thông báo mới", + "notifications_and_sounds": "Notifications & Sounds", + "incoming-message-sound": "Incoming message sound", + "outgoing-message-sound": "Outgoing message sound", + "notification-sound": "Notification sound", + "no-sound": "No sound", "browsing": "Đang xem cài đặt", "open_links_in_new_tab": "Mở link trong tab mới.", "enable_topic_searching": "Bật In-topic Searching", "topic_search_help": "Nếu bật, tìm kiếm trong chủ đề sẽ thay thế tính năng tính năng tìm kiếm của trình duyệt và cho phép bạn tìm kiếm trong toàn bộ chủ đề, thay vì chỉ tìm kiếm nội dung đang hiện thị trên màn hình", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Theo dõi chủ đề bạn trả lời", - "follow_topics_you_create": "Theo dõi chủ đề bạn tạo", - "grouptitle": "Chọn tên nhóm mà bạn muốn hiển thị", + "delay_image_loading": "Việc tải ảnh đang bị chậm", + "image_load_delay_help": "Nếu được bật, toàn bộ ảnh trong chủ đề sẽ chỉ được tải khi người dùng kéo chuột tới", + "scroll_to_my_post": "Sau khi đăng một trả lời thì hiển thị bài viết mới", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Tên nhóm", "no-group-title": "Không có tên nhóm", "select-skin": "Chọn một giao diện", "select-homepage": "Chọn Trang chủ", @@ -105,5 +113,17 @@ "custom_route_help": "Nhập đường dẫn ở đây, mà không có dấu gạch chéo ở trước (chẳng hạn \"recent\" hoặc \"popular)", "sso.title": "Đăng nhập một lần", "sso.associated": "Đã liên kết với", - "sso.not-associated": "Nhấn vào đây để liên kết với" + "sso.not-associated": "Nhấn vào đây để liên kết với", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-permanently": "Bị cấm vĩnh viễn", + "info.banned-reason-label": "Lý do", + "info.banned-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/vi/users.json b/public/language/vi/users.json index 0e60994360..5a830cf2de 100644 --- a/public/language/vi/users.json +++ b/public/language/vi/users.json @@ -2,6 +2,7 @@ "latest_users": "Thành viên mới nhất", "top_posters": "Thành viên đăng bài nhiều nhất", "most_reputation": "Thành viên có điểm tín nhiệm cao nhất", + "most_flags": "Most Flags", "search": "Tìm kiếm", "enter_username": "Gõ tên thành viên để tìm kiếm", "load_more": "Tải thêm", diff --git a/public/language/zh_CN/category.json b/public/language/zh_CN/category.json index 6023490c13..68480df46d 100644 --- a/public/language/zh_CN/category.json +++ b/public/language/zh_CN/category.json @@ -10,7 +10,11 @@ "share_this_category": "分享此版块", "watch": "关注", "ignore": "忽略", - "watch.message": "您现在已经关注了此版块", - "ignore.message": "您现在已经取消了此版块的关注", + "watching": "关注中", + "ignoring": "已忽略", + "watching.description": "显示未读帖子", + "ignoring.description": "不显示未读帖子", + "watch.message": "您关注了此版块的动态。", + "ignore.message": "您未关注此版块的动态。", "watched-categories": "已关注的版块" } \ No newline at end of file diff --git a/public/language/zh_CN/email.json b/public/language/zh_CN/email.json index 4d4534bf72..66650a0b02 100644 --- a/public/language/zh_CN/email.json +++ b/public/language/zh_CN/email.json @@ -24,6 +24,7 @@ "digest.day": "天", "digest.week": "周", "digest.month": "月", + "digest.subject": "%1 的摘要", "notif.chat.subject": "收到来自 %1 的新聊天消息", "notif.chat.cta": "点击这里继续会话", "notif.chat.unsub.info": "根据您的订阅设置,为您发送此聊天提醒。", diff --git a/public/language/zh_CN/error.json b/public/language/zh_CN/error.json index 4e8ec6a25f..af7c770ffe 100644 --- a/public/language/zh_CN/error.json +++ b/public/language/zh_CN/error.json @@ -14,20 +14,25 @@ "invalid-password": "无效密码", "invalid-username-or-password": "请确认用户名和密码", "invalid-search-term": "无效的搜索关键字", + "csrf-invalid": "可能是由于会话过期,登录失败。请重试。", "invalid-pagination-value": "无效的分页数值,必须介于 %1 和 %2 之间", "username-taken": "此用户名已被占用", "email-taken": "此电子邮箱已被占用", "email-not-confirmed": "您的电子邮箱尚未确认,请点击这里确认您的电子邮箱。", "email-not-confirmed-chat": "您的电子邮箱尚未确认,无法聊天,请点击这里确认您的电子邮箱。", + "email-not-confirmed-email-sent": "您的邮箱地址还没有被确认,请检查邮箱中的确认邮件。", "no-email-to-confirm": "本论坛需要电子邮箱确认,请点击这里输入电子邮箱地址", "email-confirm-failed": "我们无法确认您的电子邮箱,请重试", "confirm-email-already-sent": "确认邮件已发出,如需重新发送请等待 %1 分钟后再试。", + "sendmail-not-found": "无法找到sendmail可执行程序,请确保sendmail已经安装并可被运行NodeBB的用户执行", "username-too-short": "用户名太短", "username-too-long": "用户名太长", "password-too-long": "密码太长", "user-banned": "用户已禁止", + "user-banned-reason": "用户已禁止(原因:%1)", "user-too-new": "抱歉,您需要等待 %1 秒后,才可以发帖!", "blacklisted-ip": "对不起,您的IP地址已被社区禁用。如果您认为这是一个错误,请与管理员联系。", + "ban-expiry-missing": "请提供此次禁言结束日期", "no-category": "版块不存在", "no-topic": "主题不存在", "no-post": "帖子不存在", @@ -38,10 +43,26 @@ "category-disabled": "版块已禁用", "topic-locked": "主题已锁定", "post-edit-duration-expired": "您必须在发表 %1 秒后才能修改内容", + "post-edit-duration-expired-minutes": "在发表 %1 分钟后才能修改内容", + "post-edit-duration-expired-minutes-seconds": "发表 %1 分 %2 秒后才能修改内容", + "post-edit-duration-expired-hours": "发表 %1 小时后才能修改内容", + "post-edit-duration-expired-hours-minutes": "发表 %1 小时 %2 分钟后才能修改内容", + "post-edit-duration-expired-days": "发表 %1 天后才能修改内容", + "post-edit-duration-expired-days-hours": "发表 %1 天 %2 小时后才能修改内容", + "post-delete-duration-expired": "您只能在发表 %1 秒后删除帖子", + "post-delete-duration-expired-minutes": "您只能在发表 %1 分钟后删除帖子", + "post-delete-duration-expired-minutes-seconds": "您只能在发表 %1 分 %2 秒后删除帖子", + "post-delete-duration-expired-hours": "您只能在发表 %1 小时后删除帖子", + "post-delete-duration-expired-hours-minutes": "您只能在发表 %1 小时 %2 分钟后删除帖子", + "post-delete-duration-expired-days": "您只能在发表 %1 天后删除帖子", + "post-delete-duration-expired-days-hours": "您只能在发表 %1 天 %2 小时后删除帖子", + "cant-delete-topic-has-reply": "您不能删除您的主题,因为已有回复。", + "cant-delete-topic-has-replies": "您不能删除您的主题,因为已有 %1 条回复。", "content-too-short": "请增添发帖内容,不能少于 %1 个字符。", "content-too-long": "请删减发帖内容,不能超过 %1 个字符。", "title-too-short": "请增加标题,不能少于 %1 个字符。", "title-too-long": "请缩减标题,不超过 %1 个字符。", + "category-not-selected": "Category not selected.", "too-many-posts": "发帖需要间隔 %1 秒以上 - 请稍候再发帖", "too-many-posts-newbie": "因为您是新用户,所以限制每隔 %1 秒才能发帖一次,直到您有 %2 点威望为止 —— 请稍候再发帖", "tag-too-short": "话题太短,不能少于 %1 个字符", @@ -55,10 +76,12 @@ "already-unfavourited": "您已取消了此贴的书签", "cant-ban-other-admins": "您不能封禁其他管理员!", "cant-remove-last-admin": "您是唯一的管理员。在删除您的管理员权限前,请添加另一个管理员。", + "cant-delete-admin": "在删除之前请你先把管理员的权限从这个账号移除。", "invalid-image-type": "无效的图像类型。允许的类型有:%1", "invalid-image-extension": "无效的图像扩展", "invalid-file-type": "无效文件格式,允许的格式有:%1", "group-name-too-short": "小组名太短", + "group-name-too-long": "小组名太长", "group-already-exists": "小组已存在", "group-name-change-not-allowed": "不允许更改小组名称", "group-already-member": "已经是此小组的成员", @@ -85,6 +108,7 @@ "cant-edit-chat-message": "您不能编辑这条信息", "cant-remove-last-user": "您不能移除这个用户", "cant-delete-chat-message": "您不允许删除这条消息", + "already-voting-for-this-post": "您已为此帖回复投过票了。", "reputation-system-disabled": "威望系统已禁用。", "downvoting-disabled": "扣分功能已禁用", "not-enough-reputation-to-downvote": "您的威望不足以给此帖扣分", @@ -99,5 +123,9 @@ "no-session-found": "未登录!", "not-in-room": "用户已不在聊天室中", "no-users-in-room": "这个聊天室中没有用户", - "cant-kick-self": "你不能把自己踢出群组" + "cant-kick-self": "您不能把自己踢出群组", + "no-users-selected": "尚未选择用户", + "invalid-home-page-route": "无效的首页路径", + "invalid-session": "Session 无法匹配", + "invalid-session-text": "您的登入状态已经失效,或者是与服务器信息不匹配。请刷新此页面。" } \ No newline at end of file diff --git a/public/language/zh_CN/global.json b/public/language/zh_CN/global.json index d6a10e614c..74915d5a66 100644 --- a/public/language/zh_CN/global.json +++ b/public/language/zh_CN/global.json @@ -7,8 +7,10 @@ "403.login": "或许您应该先 <a href='%1/login'>登录</a>试试?", "404.title": "未找到", "404.message": "您访问的页面不存在。返回<a href='%1/'>首页</a>。", - "500.title": "内部错误。", + "500.title": "内部错误", "500.message": "哎呀!看来是哪里出错了!", + "400.title": "错误的请求", + "400.message": "看起来这个链接的格式不正确,请再次检查并重试。或者返回<a href='%1/'>主页</a>。", "register": "注册", "login": "登录", "please_log_in": "请登录", @@ -17,6 +19,7 @@ "welcome_back": "欢迎回来", "you_have_successfully_logged_in": "您已成功登录", "save_changes": "保存更改", + "save": "保存", "close": "关闭", "pagination": "分页", "pagination.out_of": "%1 / %2", @@ -50,7 +53,9 @@ "topics": "主题", "posts": "帖子", "best": "最佳", + "upvoters": "顶的人", "upvoted": "顶", + "downvoters": "踩的人", "downvoted": "踩", "views": "浏览", "reputation": "威望", @@ -90,5 +95,8 @@ "enter_page_number": "输入页码", "upload_file": "上传文件", "upload": "上传", - "allowed-file-types": "允许的文件类型有 %1" + "allowed-file-types": "允许的文件类型有 %1", + "unsaved-changes": "您有未保存的更改,您确定您要离开么?", + "reconnecting-message": "与 %1 的连接断开,我们正在尝试重连,请耐心等待", + "play": "Play" } \ No newline at end of file diff --git a/public/language/zh_CN/groups.json b/public/language/zh_CN/groups.json index f988de4db6..87ee48c385 100644 --- a/public/language/zh_CN/groups.json +++ b/public/language/zh_CN/groups.json @@ -50,5 +50,7 @@ "membership.leave-group": "退出小组", "membership.reject": "拒绝", "new-group.group_name": "组名: ", - "upload-group-cover": "上传组封面" + "upload-group-cover": "上传组封面", + "bulk-invite-instructions": "输入您要邀请加入此小组的用户名,多个用户以逗号分隔", + "bulk-invite": "批量邀请" } \ No newline at end of file diff --git a/public/language/zh_CN/login.json b/public/language/zh_CN/login.json index 9cf15f0e15..d46196e47d 100644 --- a/public/language/zh_CN/login.json +++ b/public/language/zh_CN/login.json @@ -5,7 +5,8 @@ "remember_me": "记住我?", "forgot_password": "忘记密码?", "alternative_logins": "使用合作网站帐号登录", - "failed_login_attempt": "登录失败,请重试。", + "failed_login_attempt": "登录失败", "login_successful": "您已经成功登录!", - "dont_have_account": "没有帐号?" + "dont_have_account": "没有帐号?", + "logged-out-due-to-inactivity": "由于长时间不活动,您已从控制面板注销" } \ No newline at end of file diff --git a/public/language/zh_CN/modules.json b/public/language/zh_CN/modules.json index bf50d3f496..6da86ec505 100644 --- a/public/language/zh_CN/modules.json +++ b/public/language/zh_CN/modules.json @@ -29,6 +29,16 @@ "composer.submit_and_lock": "提交并锁定", "composer.toggle_dropdown": "标为 Dropdown", "composer.uploading": "正在上传 %1", + "composer.formatting.bold": "加粗", + "composer.formatting.italic": "倾斜", + "composer.formatting.list": "列表", + "composer.formatting.strikethrough": "删除线", + "composer.formatting.link": "链接", + "composer.formatting.picture": "图片", + "composer.upload-picture": "上传图片", + "composer.upload-file": "上传文件", + "composer.zen_mode": "无干扰模式", + "composer.select_category": "选择一个类别", "bootbox.ok": "确认", "bootbox.cancel": "取消", "bootbox.confirm": "确认", diff --git a/public/language/zh_CN/notifications.json b/public/language/zh_CN/notifications.json index 6d45be4d19..30dc07854d 100644 --- a/public/language/zh_CN/notifications.json +++ b/public/language/zh_CN/notifications.json @@ -16,9 +16,6 @@ "upvoted_your_post_in_multiple": "<strong>%1</strong> 和 %2 个其他人在 <strong>%3</strong> 赞了您的帖子。", "moved_your_post": "您的帖子已被 <strong>%1</strong> 移动到了 <strong>%2</strong>", "moved_your_topic": "<strong>%1</strong> 移动到了 <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> 将您在 <strong>%2</strong> 的帖子添加到了书签", - "favourited_your_post_in_dual": "<strong>%1</strong> 和 <strong>%2</strong> 将您在 <strong>%3</strong> 的帖子添加到了书签", - "favourited_your_post_in_multiple": "<strong>%1</strong> 和其他 %2 人 将您在 <strong>%2</strong> 的帖子添加到了书签", "user_flagged_post_in": "<strong>%1</strong> 在 <strong>%2</strong> 标记了一个帖子", "user_flagged_post_in_dual": "<strong>%1</strong> 和 <strong>%2</strong> 在 <strong>%3</strong> 标记了一个帖子", "user_flagged_post_in_multiple": "<strong>%1</strong> 和 %2 个其他人在 <strong>%3</strong> 标记了一个帖子", diff --git a/public/language/zh_CN/pages.json b/public/language/zh_CN/pages.json index 728ed037b8..e78c35cf71 100644 --- a/public/language/zh_CN/pages.json +++ b/public/language/zh_CN/pages.json @@ -12,11 +12,13 @@ "users/sort-posts": "最多发帖的会员", "users/sort-reputation": "最多积分的会员", "users/banned": "被封禁的用户", + "users/most-flags": "被举报次数最多的用户", "users/search": "会员搜索", "notifications": "提醒", "tags": "话题", "tag": "\"%1\" 下的主题", "register": "注册帐号", + "registration-complete": "注册完成", "login": "登录帐号", "reset": "重置帐户密码", "categories": "版面", @@ -28,12 +30,13 @@ "account/edit/password": "正在编辑 \"%1\" 的密码", "account/edit/username": "正在编辑 \"%1\" 的用户名", "account/edit/email": "正在编辑 \"%1\" 的电子邮箱", + "account/info": "账户信息", "account/following": "%1 关注", "account/followers": "关注 %1 的人", "account/posts": "%1 发布的帖子", "account/topics": "%1 创建的主题", "account/groups": "%1 的小组", - "account/favourites": "%1 收藏的帖子", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "用户设置", "account/watched": "主题已被 %1 关注", "account/upvoted": "帖子被 %1 顶过", diff --git a/public/language/zh_CN/register.json b/public/language/zh_CN/register.json index f7ed01d784..11a0706cc9 100644 --- a/public/language/zh_CN/register.json +++ b/public/language/zh_CN/register.json @@ -1,5 +1,6 @@ { "register": "注册", + "cancel_registration": "取消注册", "help.email": "默认情况下,您的电子邮箱不会公开。", "help.username_restrictions": "全局唯一的用户名,长度 %1 到 %2 个字。其他人可以使用 @<span id='yourUsername'>用户名</span> 提及您。", "help.minimum_password_length": "您的密码长度必须不少于 %1 个字。", @@ -15,5 +16,8 @@ "alternative_registration": "其他方式注册", "terms_of_use": "使用条款", "agree_to_terms_of_use": "我同意使用条款", - "registration-added-to-queue": "您的注册正在等待批准。一旦通过,管理员会发送邮件通知您。" + "terms_of_use_error": "您必须同意使用条款", + "registration-added-to-queue": "您的注册正在等待批准。一旦通过,管理员会发送邮件通知您。", + "interstitial.intro": "在创建您的账户前我们需要一些额外信息。", + "interstitial.errors-found": "我们无法完成您的注册:" } \ No newline at end of file diff --git a/public/language/zh_CN/topic.json b/public/language/zh_CN/topic.json index c123366bb0..e2abc0a60b 100644 --- a/public/language/zh_CN/topic.json +++ b/public/language/zh_CN/topic.json @@ -26,12 +26,30 @@ "tools": "工具", "flag": "举报", "locked": "已锁定", - "bookmark_instructions": "点击这里返回最新的未读回帖。", + "pinned": "已固定", + "moved": "已移动", + "bookmark_instructions": "点击阅读本主题帖中的最新回复", "flag_title": "举报此帖", "flag_success": "已举报此回帖。", + "flag_manage_title": "在 1% 中已举报的帖子", + "flag_manage_history": "操作历史", + "flag_manage_no_history": "没有事件历史可汇报", + "flag_manage_assignee": "代理人", + "flag_manage_state": "状态", + "flag_manage_state_open": "新建/打开", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "已解决", + "flag_manage_state_rejected": "已拒绝", + "flag_manage_notes": "共享的笔记", + "flag_manage_update": "更新标记状态", + "flag_manage_history_assignee": "已指派给 %1", + "flag_manage_history_state": "更新状态到 1%", + "flag_manage_history_notes": "更新标记笔记", + "flag_manage_saved": "Flag Details Updated", "deleted_message": "此主题已被删除。只有拥有主题管理权限的用户可以查看。", "following_topic.message": "当有人回复此主题时,您会收到通知。", - "not_following_topic.message": "您已停止接收此主题的通知。", + "not_following_topic.message": "您将在未读主题列表中看到这个主题,但您不会在帖子被回复时收到通知。", + "ignoring_topic.message": "您将不会在未读主题列表里看到这个主题,但在被提到以及帖子被顶时仍将收到通知。", "login_to_subscribe": "请注册或登录后,再订阅此主题。", "markAsUnreadForAll.success": "将全部主题标为未读。", "mark_unread": "标记为未读", @@ -41,8 +59,14 @@ "watch.title": "当此主题有新回复时,通知我", "unwatch.title": "取消关注此主题", "share_this_post": "分享", + "watching": "关注中", + "not-watching": "未关注", + "ignoring": "忽略中", + "watching.description": "有新回复时通知我。<br/>在未读主题中显示。", + "not-watching.description": "不要在有新回复时通知我。<br/>如果这个分类未被忽略则在未读主题中显示。", + "ignoring.description": "不要在有新回复时通知我。<br/>不要在未读主题中显示该主题。", "thread_tools.title": "主题工具", - "thread_tools.markAsUnreadForAll": "标记为未读", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "置顶主题", "thread_tools.unpin": "取消置顶主题", "thread_tools.lock": "锁定主题", @@ -77,6 +101,7 @@ "topic_will_be_moved_to": "此主题将被移动到版块", "fork_topic_instruction": "点击将分割的帖子", "fork_no_pids": "未选中帖子!", + "fork_pid_count": "选择了 %1 个帖子", "fork_success": "成功分割主题! 点这里跳转到分割后的主题。", "delete_posts_instruction": "点击想要删除/永久删除的帖子", "composer.title_placeholder": "在此输入您主题的标题...", diff --git a/public/language/zh_CN/unread.json b/public/language/zh_CN/unread.json index 58ab5b0389..bd4d52074b 100644 --- a/public/language/zh_CN/unread.json +++ b/public/language/zh_CN/unread.json @@ -6,5 +6,8 @@ "selected": "已选", "all": "全部", "all_categories": "全部分类", - "topics_marked_as_read.success": "主题被标为已读!" + "topics_marked_as_read.success": "主题被标为已读!", + "all-topics": "全部主题", + "new-topics": "新建主题", + "watched-topics": "看过的主题" } \ No newline at end of file diff --git a/public/language/zh_CN/user.json b/public/language/zh_CN/user.json index e713189b25..9a3478ba02 100644 --- a/public/language/zh_CN/user.json +++ b/public/language/zh_CN/user.json @@ -6,6 +6,7 @@ "postcount": "发帖数", "email": "电子邮件", "confirm_email": "确认电子邮箱", + "account_info": "账户信息", "ban_account": "禁止账户", "ban_account_confirm": "您确定禁止这位用户吗?", "unban_account": "解禁账户", @@ -26,7 +27,7 @@ "watched": "已订阅", "followers": "粉丝", "following": "关注", - "aboutme": "About me", + "aboutme": "关于我", "signature": "签名档", "birthday": "生日", "chat": "聊天", @@ -88,14 +89,21 @@ "topics_per_page": "每页主题数", "posts_per_page": "每页帖子数", "notification_sounds": "收到通知时播放提示音", + "notifications_and_sounds": "通知 & 提示音", + "incoming-message-sound": "消息到达提示音", + "outgoing-message-sound": "消息送出提示音", + "notification-sound": "通知提示音", + "no-sound": "No sound", "browsing": "浏览设置", "open_links_in_new_tab": "在新标签打开外部链接", "enable_topic_searching": "启用主题内搜索", "topic_search_help": "如果启用此项,主题内搜索会替代浏览器默认的页面搜索,您将可以在整个主题内搜索,而不仅仅只搜索页面上展现的内容。", + "delay_image_loading": "延迟图片加载", + "image_load_delay_help": "启用后,帖子中的图片仅在用户滚动到图片所在位置时加载", "scroll_to_my_post": "在提交回复之后显示新帖子", - "follow_topics_you_reply_to": "关注您回复的主题", - "follow_topics_you_create": "关注您创建的主题", - "grouptitle": "选择展示的小组称号", + "follow_topics_you_reply_to": "关注你回复过的主题", + "follow_topics_you_create": "关注你创建的主题", + "grouptitle": "小组标题", "no-group-title": "不展示小组称号", "select-skin": "选择皮肤", "select-homepage": "选择首页", @@ -105,5 +113,17 @@ "custom_route_help": "输入路由名称,前面不需要斜杠 ( 例如, \"recent\" 或 \"popular\" )", "sso.title": "单点登录服务", "sso.associated": "关联到", - "sso.not-associated": "点击这里关联" + "sso.not-associated": "点击这里关联", + "info.latest-flags": "最新举报", + "info.no-flags": "没有找到被举报的帖子", + "info.ban-history": "最近封禁历史", + "info.no-ban-history": "该用户从未被封禁", + "info.banned-until": "封禁到 %1", + "info.banned-permanently": "永久封禁", + "info.banned-reason-label": "原因", + "info.banned-no-reason": "没有原因", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/zh_CN/users.json b/public/language/zh_CN/users.json index 50d6dc0c23..44af27c94d 100644 --- a/public/language/zh_CN/users.json +++ b/public/language/zh_CN/users.json @@ -2,6 +2,7 @@ "latest_users": "最新会员", "top_posters": "发帖排行", "most_reputation": "威望排行", + "most_flags": "最多举报", "search": "搜索", "enter_username": "输入用户名搜索", "load_more": "加载更多", diff --git a/public/language/zh_TW/category.json b/public/language/zh_TW/category.json index c1ab395ea5..06bd70351d 100644 --- a/public/language/zh_TW/category.json +++ b/public/language/zh_TW/category.json @@ -1,16 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", + "category": "類別", + "subcategories": "子類別", "new_topic_button": "新主題", - "guest-login-post": "登錄後才能發表", - "no_topics": "<strong>這個類別還沒有任何主題。</strong><br />為何不來發點東西呢?", + "guest-login-post": "登入後才能張貼文章", + "no_topics": "<strong>這個類別目前沒有任何主題。</strong><br />你要不要來張貼個新主題呢?", "browsing": "正在瀏覽", "no_replies": "還沒有回覆", - "no_new_posts": "No new posts.", + "no_new_posts": "沒有新的張貼", "share_this_category": "分享這類別", - "watch": "觀看", + "watch": "關注", "ignore": "忽略", - "watch.message": "您正觀看著此類別的更新", - "ignore.message": "您已忽略此類別的更新", - "watched-categories": "Watched categories" + "watching": "關注", + "ignoring": "忽略", + "watching.description": "顯示未讀的主題", + "ignoring.description": "不要顯示未讀的主題", + "watch.message": "You are now watching updates from this category and all subcategories", + "ignore.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "關注的類別" } \ No newline at end of file diff --git a/public/language/zh_TW/email.json b/public/language/zh_TW/email.json index 2eeb7a8e13..d34fae331b 100644 --- a/public/language/zh_TW/email.json +++ b/public/language/zh_TW/email.json @@ -1,35 +1,36 @@ { "password-reset-requested": "已要求重設密碼 - %1!", - "welcome-to": "歡迎來到%1", - "invite": "Invitation from %1", - "greeting_no_name": "您好", - "greeting_with_name": "您好,%1", - "welcome.text1": "多謝登記%1!", - "welcome.text2": "要啟用你的帳戶,我們先要驗證你用作登記的電郵地址", - "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", - "welcome.cta": "請點擊此處確認您的電郵地址", - "invitation.text1": "%1 has invited you to join %2", - "invitation.ctr": "Click here to create your account.", + "welcome-to": "歡迎來到 %1", + "invite": "邀請來自 %1", + "greeting_no_name": "你好", + "greeting_with_name": "%1 你好", + "welcome.text1": "感謝註冊 %1!", + "welcome.text2": "要啟動你的帳戶,我們先要驗證你用作註冊的電子郵件地址", + "welcome.text3": "管理者已經批準你的註冊申請。你現在可以使用你的帳號/密碼進行登入。", + "welcome.cta": "請點擊此處來確認你的電子郵件地址", + "invitation.text1": "%1 邀請你加入 %2", + "invitation.ctr": "點擊這裡來建立你的帳號", "reset.text1": "我們收到一個重設密碼的請求,你忘掉了密碼嗎?如果不是,請忽略這封郵件。", "reset.text2": "要繼續重置密碼,請點擊以下鏈接:", "reset.cta": "點擊這裡重置密碼", "reset.notify.subject": "密碼修改成功", - "reset.notify.text1": "提醒您密碼已於 %1 修改成功", - "reset.notify.text2": "如果您未允許此動作, 請即刻通知系統管理者", - "digest.notifications": "你有來自$1的未讀通知:", - "digest.latest_topics": "來自%1的最新話題", - "digest.cta": "點擊這裡訪問%1", - "digest.unsub.info": "本摘要按您的訂閱設置發送給您。", - "digest.no_topics": "在過去%1沒有活躍的話題", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", - "notif.chat.subject": "收到來自$1的聊天消息", - "notif.chat.cta": "點擊此處繼續對話", - "notif.chat.unsub.info": "本聊天通知按您的訂閱設置發送給您。", - "notif.post.cta": "按此以閱讀完整標題", - "notif.post.unsub.info": "本發文通知按您的訂閱設置發送給您。", - "test.text1": "這是一個測試電郵,以確認您的NodeBB郵件器設置正確。", - "unsub.cta": "點擊這裡更改這些設置", - "closing": "謝謝!" + "reset.notify.text1": "我們要通知你在 %1 的密碼已成功更改", + "reset.notify.text2": "如果你未允許此動作, 請立即通知系統管理者", + "digest.notifications": "你有來自 %1 的未讀通知:", + "digest.latest_topics": "來自 %1 的最新主題", + "digest.cta": "點擊這裡訪問 %1", + "digest.unsub.info": "本摘要依照你的訂閱設置發送給你。", + "digest.no_topics": "在過去 %1 沒有活躍的主題", + "digest.day": "日", + "digest.week": "週", + "digest.month": "月", + "digest.subject": "摘要於 %1", + "notif.chat.subject": "收到來自 %1 的聊天訊息", + "notif.chat.cta": "點擊此處繼續聊天對話", + "notif.chat.unsub.info": "本聊天通知按你的訂閱設置發送給你。", + "notif.post.cta": "點擊此處來閱讀完整主題", + "notif.post.unsub.info": "本張貼通知按你的訂閱設置發送給你。", + "test.text1": "這是一個測試電子郵件,用於確認你的NodeBB郵件功能是否設置正確。", + "unsub.cta": "點擊此處來更改這些設置", + "closing": "感謝!" } \ No newline at end of file diff --git a/public/language/zh_TW/error.json b/public/language/zh_TW/error.json index 0db5a671c2..5069c63a22 100644 --- a/public/language/zh_TW/error.json +++ b/public/language/zh_TW/error.json @@ -1,8 +1,8 @@ { "invalid-data": "無效的資料", - "not-logged-in": "您似乎還沒有登入喔!", - "account-locked": "您的帳戶暫時被鎖定!", - "search-requires-login": "Searching requires an account - please login or register.", + "not-logged-in": "你似乎還沒有登入喔!", + "account-locked": "你的帳戶暫時被鎖定!", + "search-requires-login": "進行搜尋需要有帳號 - 請先註冊或登入", "invalid-cid": "無效的類別 ID", "invalid-tid": "無效的主題 ID", "invalid-pid": "無效的文章 ID", @@ -12,92 +12,120 @@ "invalid-title": "無效的主題!", "invalid-user-data": "無效的使用者資料", "invalid-password": "無效的密碼", - "invalid-username-or-password": "請指定用戶名和密碼", + "invalid-username-or-password": "請指定帳號和密碼", "invalid-search-term": "無效的搜索字詞", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "我們無法讓你登入,似乎是因為連線階段已到期。請再重試一次。", + "invalid-pagination-value": "無效的分頁數值, 必需是至少 %1 與最多 %2", "username-taken": "該使用者名稱已被使用", "email-taken": "該信箱已被使用", - "email-not-confirmed": "您的電郵尚未得到確認,請點擊此處確認您的電子郵件。", - "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", - "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email", - "email-confirm-failed": "We could not confirm your email, please try again later.", - "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", - "username-too-short": "使用者名稱太簡短", - "username-too-long": "使用者名稱太長", - "password-too-long": "Password too long", + "email-not-confirmed": "你的電子郵件尚未確認,請點擊此處確認你的電子郵件。", + "email-not-confirmed-chat": "你需要先確認電子郵件後才能進行聊天,請點擊這裡來確認你的電子郵件。", + "email-not-confirmed-email-sent": "你的電子郵件地址還沒有確認,請確認一下你的收信箱是不是有確認信。", + "no-email-to-confirm": "討論區要求電子郵件確認,請點擊這裡輸入一個電子郵件。", + "email-confirm-failed": "我們無法確認你的Email,請之後再重試。", + "confirm-email-already-sent": "確認電子郵件已經寄送,請等待 %1 分鐘才能再寄送另一封。", + "sendmail-not-found": "沒有找到sendmail可執行程序,請確認它是不是已經被安裝與可讓執行NodeBB的使用者執行的", + "username-too-short": "帳號太短", + "username-too-long": "帳號太長", + "password-too-long": "密碼太長", "user-banned": "該使用者已被停用", - "user-too-new": "抱歉,發表您第一篇文章須要等待 %1 秒", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "user-banned-reason": "User banned (Reason: %1)", + "user-too-new": "抱歉,發表你第一篇文章須要等待 %1 秒", + "blacklisted-ip": "抱歉,你的IP位置已經被這個社群禁用了。如果你覺得這是一個失誤,請連絡管理員。", + "ban-expiry-missing": "請提供這個禁用的到期時間", "no-category": "類別並不存在", "no-topic": "主題並不存在", "no-post": "文章並不存在", "no-group": "群組並不存在", "no-user": "使用者並不存在", "no-teaser": "Teaser 並不存在", - "no-privileges": "您似乎沒有執行這個行為的權限!", + "no-privileges": "你沒有執行這個行為的足夠權限!", "category-disabled": "該類別已被關閉", "topic-locked": "該主題已被鎖定", - "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", - "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", - "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", - "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", - "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", - "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", - "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", - "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", - "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", - "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", - "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "post-edit-duration-expired": "在張貼 %1 秒後,你才能編輯張貼文章", + "post-edit-duration-expired-minutes": "在張貼 %1 秒後,你才能編輯張貼文章", + "post-edit-duration-expired-minutes-seconds": "你只被允許在張貼的 %1 分鐘又 %2 秒後才能編輯", + "post-edit-duration-expired-hours": "你只被允許在張貼的 %1 小時後才能編輯", + "post-edit-duration-expired-hours-minutes": "你只被允許在張貼的 %1 小時 %2 分鐘後才能編輯", + "post-edit-duration-expired-days": "你只被允許在張貼的 %1 天後才能編輯", + "post-edit-duration-expired-days-hours": "你只被允許在張貼的 %1 天又 %2 小時後才能編輯", + "post-delete-duration-expired": "你只被允許在張貼的 %1 秒後才能刪除", + "post-delete-duration-expired-minutes": "你只被允許在張貼的 %1 分鐘後才能刪除", + "post-delete-duration-expired-minutes-seconds": "你只被允許在張貼的 %1 分鐘又 %2 秒後才能刪除", + "post-delete-duration-expired-hours": "你只被允許在張貼的 %1 小時後才能刪除", + "post-delete-duration-expired-hours-minutes": "你只被允許在張貼的 %1 小時又 %2 分鐘後才能刪除", + "post-delete-duration-expired-days": "你只被允許在張貼的 %1 天後才能刪除", + "post-delete-duration-expired-days-hours": "你只被允許在張貼的 %1 天又 %2 小時後才能刪除", + "cant-delete-topic-has-reply": "你不能刪除你的主題,在它已經有一筆回覆時", + "cant-delete-topic-has-replies": "你不能刪除你的主題,在它已經有 %1 筆回覆時", + "content-too-short": "請輸入一個長一點的張貼內容。張貼內容長度不能少於 %1 字元。", + "content-too-long": "請輸入一個短一點的張貼內容。張貼內容長度不能超過 %1 字元。", + "title-too-short": "請輸入一個長一點的標題。標題長度不能少於 %1 字元。", + "title-too-long": "請輸入一個短一點的標題。標題長度不能超過 %1 字元。", + "category-not-selected": "Category not selected.", + "too-many-posts": "在張貼 %1 秒後,你才能再張貼文章 - 請在重新張貼前等待這個時間。", + "too-many-posts-newbie": "新使用者需要直到獲得 %2 聲譽後,才能在每 %1 秒後進行張貼一篇文章 - 請在重新張貼前等待這個時間。", + "tag-too-short": "請輸入一個長一點的標籤。標籤長度不能少於 %1 字元。", + "tag-too-long": "請輸入一個短一點的標籤。標籤長度不能超過 %1 字元。", + "not-enough-tags": "標籤數量不足夠。主題需要至少 %1 標籤。", + "too-many-tags": "過多標籤數量。主題無法擁有超過 %1 個標籤。", "still-uploading": "請等待上傳完成。", - "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", - "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", - "cant-ban-other-admins": "您無法禁止其他的管理員!", - "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "file-too-big": "最大允許的檔案大小是 %1 kB - 請上傳一個小於這個數值的檔案。", + "guest-upload-disabled": "訪客上傳是被禁止的", + "already-favourited": "你已經將這篇張貼加入書籤", + "already-unfavourited": "你已經將這篇張貼移除書籤", + "cant-ban-other-admins": "你無法封鎖其他的管理員!", + "cant-remove-last-admin": "你是唯一的管理員。在你移除自己為管理員前,需要新增另一個使用者為管理員。", + "cant-delete-admin": "在要刪除這個帳戶前,請先移除這個帳戶的管理員權限", "invalid-image-type": "無效的圖像類型。允許的類型:%1", "invalid-image-extension": "無效的圖像擴充元件", "invalid-file-type": "無效的檔案類型。允許的類型:%1", "group-name-too-short": "群組名稱太短了", + "group-name-too-long": "群組名稱太長", "group-already-exists": "群組名稱已存在", "group-name-change-not-allowed": "變更群組名稱不被允許", - "group-already-member": "Already part of this group", - "group-not-member": "Not a member of this group", - "group-needs-owner": "This group requires at least one owner", - "group-already-invited": "This user has already been invited", - "group-already-requested": "Your membership request has already been submitted", + "group-already-member": "已經加入這個群組", + "group-not-member": "不是這個群組的一員", + "group-needs-owner": "這個群組需要至少一個擁有者", + "group-already-invited": "這個使用者已經被邀請", + "group-already-requested": "你的會員申請已經被提交", "post-already-deleted": "此文章已經被刪除", "post-already-restored": "此文章已還原", "topic-already-deleted": "此主題已經被刪除", "topic-already-restored": "此主題已還原", - "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "cant-purge-main-post": "你無法清除主要的張貼文,請改為刪除這個張貼文。", "topic-thumbnails-are-disabled": "禁用主題縮圖", "invalid-file": "無效的檔案", "uploads-are-disabled": "上傳功能被停用", - "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", - "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "signature-too-long": "抱歉,你的簽名長度不能超過 %1 字元。", + "about-me-too-long": "抱歉,關於我長度不能超過 %1 字元。", "cant-chat-with-yourself": "你不能與自己聊天!", - "chat-restricted": "此用戶已限制了他的聊天功能。你要在他關注你之後,才能跟他聊天", - "chat-disabled": "Chat system disabled", - "too-many-messages": "You have sent too many messages, please wait awhile.", - "invalid-chat-message": "Invalid chat message", - "chat-message-too-long": "Chat message is too long", - "cant-edit-chat-message": "You are not allowed to edit this message", - "cant-remove-last-user": "You can't remove the last user", - "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-restricted": "此使用者已限制了他的聊天功能。你要在他(她)關注你之後,才能跟他聊天", + "chat-disabled": "聊天系統被禁止", + "too-many-messages": "你已經送出過多的訊息,請稍等一下。", + "invalid-chat-message": "無效的聊天訊息", + "chat-message-too-long": "聊天訊息太長", + "cant-edit-chat-message": "你不被允許編輯這條訊息", + "cant-remove-last-user": "你不能移除最後的使用者", + "cant-delete-chat-message": "你不被允許刪除這條訊息", + "already-voting-for-this-post": "你已經對這個張貼投過票了", "reputation-system-disabled": "信譽系統已停用。", - "downvoting-disabled": "Downvoting已停用", - "not-enough-reputation-to-downvote": "你沒有足夠的信譽downvote這個帖子", + "downvoting-disabled": "反向投票已停用", + "not-enough-reputation-to-downvote": "你沒有足夠的信譽可以對這個張貼進行反向投票", "not-enough-reputation-to-flag": "你沒有足夠的信譽來舉報這個帖子", - "already-flagged": "You have already flagged this post", + "already-flagged": "你已經對這個張貼標記過了", "reload-failed": "NodeBB重載\"%1\"時遇到了問題。 NodeBB將繼續提供現有的客戶端資源,但請你撤消重載前的動作。", "registration-error": "註冊錯誤", - "parse-error": "Something went wrong while parsing server response", - "wrong-login-type-email": "請使用您的電子郵件進行登錄", - "wrong-login-type-username": "請使用您的使用者名稱進行登錄", - "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", - "no-session-found": "No login session found!", - "not-in-room": "User not in room", - "no-users-in-room": "No users in this room", - "cant-kick-self": "You can't kick yourself from the group" + "parse-error": "當剖析伺服器回應時發生了某個錯誤", + "wrong-login-type-email": "請使用你的電子郵件進行登入", + "wrong-login-type-username": "請使用你的帳號進行登入", + "invite-maximum-met": "你已經邀請最多可邀請的人數限制 (%1 於 %2)。", + "no-session-found": "沒有找到登入的連線階段!", + "not-in-room": "使用者沒有在聊天室中", + "no-users-in-room": "沒有使用者在聊天室中", + "cant-kick-self": "你不能把自己從群組中踢出", + "no-users-selected": "沒有選定使用者", + "invalid-home-page-route": "無效的首頁路由", + "invalid-session": "會話階段錯誤", + "invalid-session-text": "看起來你的登入會話階段已經無效,或是不符合於伺服器。請重新整理這個頁面。" } \ No newline at end of file diff --git a/public/language/zh_TW/global.json b/public/language/zh_TW/global.json index 10e4169e07..3bcd5010b2 100644 --- a/public/language/zh_TW/global.json +++ b/public/language/zh_TW/global.json @@ -3,92 +3,100 @@ "search": "搜尋", "buttons.close": "關閉", "403.title": "禁止存取", - "403.message": "你沒有該頁面的存取權限", - "403.login": "可能是因為你尚未<a href='%1/login'>登入</a>?", + "403.message": "你沒有該頁面的存取權限。", + "403.login": "或許是你應該 <a href='%1/login'>試著登入</a>?", "404.title": "無法找到該頁", - "404.message": "你所查找的頁面並不存在,返回<a href='%1/'>主頁</a>。", + "404.message": "你所查找的頁面並不存在。返回<a href='%1/'>首頁</a>。", "500.title": "內部錯誤", - "500.message": "不好!看來是哪裡出錯了!", - "register": "注冊", - "login": "登錄", - "please_log_in": "請登入", - "logout": "退出", - "posting_restriction_info": "發表文章目前僅限於註冊的會員,點擊此處進行登錄。", + "500.message": "糟糕! 看來是不知道哪裡出錯了!", + "400.title": "錯誤要求", + "400.message": "看起來這個連結是有問題的,請點擊兩次然後重新嘗試。要不然,回到<a href='%1/'>首頁</a>。", + "register": "註冊", + "login": "登入", + "please_log_in": "請先登入", + "logout": "登出", + "posting_restriction_info": "張貼文章目前只限註冊會員,點擊此處進行登入。", "welcome_back": "歡迎回來!", - "you_have_successfully_logged_in": "您已經成功登錄!", - "save_changes": "保存修改", + "you_have_successfully_logged_in": "你已成功登入!", + "save_changes": "儲存變更", + "save": "Save", "close": "關閉", "pagination": "分頁", - "pagination.out_of": "%1 out of %2", - "pagination.enter_index": "輸入Index", + "pagination.out_of": "%1 於 %2", + "pagination.enter_index": "輸入索引", "header.admin": "管理", - "header.categories": "Categories", + "header.categories": "類別", "header.recent": "最近", "header.unread": "未讀", "header.tags": "標籤", "header.popular": "熱門", - "header.users": "用戶", + "header.users": "使用者", "header.groups": "群組", "header.chats": "聊天", "header.notifications": "通知", "header.search": "搜尋", - "header.profile": "設置", - "header.navigation": "Navigation", - "notifications.loading": "消息載入中", - "chats.loading": "聊天載入中···", + "header.profile": "個人資料", + "header.navigation": "導覽", + "notifications.loading": "通知載入中", + "chats.loading": "聊天載入中", "motd.welcome": "歡迎來到 NodeBB,一個未來的討論平台。", "previouspage": "上一頁", "nextpage": "下一頁", "alert.success": "成功", "alert.error": "錯誤", "alert.banned": "封鎖", - "alert.banned.message": "你已被封鎖並將登出!", - "alert.unfollow": "你已不再關注 %1!", - "alert.follow": "你正在關注 %1!", - "online": "在線", + "alert.banned.message": "你剛被封鎖,將會被登出!", + "alert.unfollow": "你已不再跟隨 %1!", + "alert.follow": "你正在跟隨 %1!", + "online": "線上", "users": "使用者", "topics": "主題", - "posts": "文章", - "best": "Best", - "upvoted": "Upvoted", - "downvoted": "Downvoted", - "views": "Views", - "reputation": "聲譽", - "read_more": "閱讀更多...", - "more": "More", - "posted_ago_by_guest": "posted %1 by Guest", - "posted_ago_by": "posted %1 by %2", - "posted_ago": "posted %1", - "posted_in": "posted in %1", - "posted_in_by": "posted in %1 by %2", - "posted_in_ago": "posted in %1", - "posted_in_ago_by": "posted in %1 %2 by %3", - "user_posted_ago": "%1 posted %2", - "guest_posted_ago": "Guest posted %1", - "last_edited_by": "last edited by %1", - "norecentposts": "最近沒新文章", - "norecenttopics": "最近沒新主題", - "recentposts": "最近的文章", - "recentips": "最近登錄的 IP 來源位址", + "posts": "張貼", + "best": "最棒", + "upvoters": "正向投票", + "upvoted": "正向投票", + "downvoters": "負向投票", + "downvoted": "負向投票", + "views": "瀏覽", + "reputation": "聲望", + "read_more": "閱讀更多", + "more": "更多", + "posted_ago_by_guest": "訪客張貼 %1", + "posted_ago_by": "%2 張貼 %1 ", + "posted_ago": "張貼 %1", + "posted_in": "張貼在 %1", + "posted_in_by": "張貼於 %1 由 %2", + "posted_in_ago": "張貼在 %1 %2", + "posted_in_ago_by": "由 %3 張貼在 %1 %2 ", + "user_posted_ago": "%1 張貼 %2", + "guest_posted_ago": "訪客張貼 %1", + "last_edited_by": "最後編輯由 %1", + "norecentposts": "沒新張貼文", + "norecenttopics": "沒新主題", + "recentposts": "最近的張貼文", + "recentips": "最近登入的 IP 位址", "away": "離開", - "dnd": "Do not disturb", - "invisible": "隱藏", + "dnd": "勿打擾", + "invisible": "隱形", "offline": "離線", "email": "Email", "language": "語言", - "guest": "Guest", - "guests": "Guests", - "updated.title": "討論區更新完畢", - "updated.message": "這個討論區最近被更新至最新的版本. 按此來重整這個頁面", + "guest": "訪客", + "guests": "訪客", + "updated.title": "討論區已更新", + "updated.message": "這個討論區最近已更新至最新的版本。點擊此處來重整這個頁面。", "privacy": "隱私", - "follow": "追蹤", - "unfollow": "取消追蹤", + "follow": "跟隨", + "unfollow": "取消跟隨", "delete_all": "全部刪除", - "map": "Map", - "sessions": "Login Sessions", - "ip_address": "IP Address", - "enter_page_number": "Enter page number", - "upload_file": "Upload file", - "upload": "Upload", - "allowed-file-types": "Allowed file types are %1" + "map": "地圖", + "sessions": "登入連線階段", + "ip_address": "IP地址", + "enter_page_number": "輸入頁碼", + "upload_file": "上傳檔案", + "upload": "上傳", + "allowed-file-types": "允許的檔案類型是 %1", + "unsaved-changes": "你還沒有儲存更動。你確定想要離開這個頁面?", + "reconnecting-message": "看起來你的連線到 %1 已經遺失,請稍等一下我們嘗試重新連線。", + "play": "Play" } \ No newline at end of file diff --git a/public/language/zh_TW/groups.json b/public/language/zh_TW/groups.json index d562fb5ccd..bc5dd42334 100644 --- a/public/language/zh_TW/groups.json +++ b/public/language/zh_TW/groups.json @@ -6,25 +6,25 @@ "no_groups_found": "這裡看不到任何群組", "pending.accept": "接受", "pending.reject": "拒絕", - "pending.accept_all": "Accept All", - "pending.reject_all": "Reject All", - "pending.none": "There are no pending members at this time", - "invited.none": "There are no invited members at this time", - "invited.uninvite": "Rescind Invitation", - "invited.search": "Search for a user to invite to this group", - "invited.notification_title": "You have been invited to join <strong>%1</strong>", - "request.notification_title": "Group Membership Request from <strong>%1</strong>", - "request.notification_text": "<strong>%1</strong> has requested to become a member of <strong>%2</strong>", + "pending.accept_all": "同意所有", + "pending.reject_all": "拒絕所有", + "pending.none": "目前沒有等待中的會員", + "invited.none": "目前沒有邀請的會員", + "invited.uninvite": "撤銷邀請", + "invited.search": "搜尋要邀請加入這個群組的使用者", + "invited.notification_title": "你已被邀請加入<strong>%1</strong>", + "request.notification_title": "群組會員要求,來自<strong>%1</strong>", + "request.notification_text": "<strong>%1</strong>已經要求成為<strong>%2</strong>群組的會員", "cover-save": "儲存", "cover-saving": "儲存中", "details.title": "群組詳細信息", "details.members": "成員列表", "details.pending": "待審成員", - "details.invited": "Invited Members", + "details.invited": "邀請會員", "details.has_no_posts": "這個群組的成員還未發出任何帖子。", "details.latest_posts": "最新文章", "details.private": "私人", - "details.disableJoinRequests": "Disable join requests", + "details.disableJoinRequests": "禁止加入要求", "details.grant": "准許/撤銷 所有權", "details.kick": "剔除", "details.owner_options": "群組管理員", @@ -37,18 +37,20 @@ "details.change_colour": "變更顏色", "details.badge_text": "徽章字串", "details.userTitleEnabled": "顯示徽章", - "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.private_help": "如果開啟,加入群組需要經過群組擁有者批準", "details.hidden": "隱藏", - "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", - "details.delete_group": "Delete Group", - "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "details.hidden_help": "如果開啟的話,群組將不會在群組列表中被看到,而且使用者將需要手動邀請", + "details.delete_group": "刪除群組", + "details.private_system_help": "私有群組在系統層級被禁用,這個選項沒有任何作用", "event.updated": "群組詳細訊息已被更新", "event.deleted": "此 \"%1\" 群組已被刪除了", - "membership.accept-invitation": "Accept Invitation", - "membership.invitation-pending": "Invitation Pending", - "membership.join-group": "Join Group", - "membership.leave-group": "Leave Group", - "membership.reject": "Reject", - "new-group.group_name": "Group Name:", - "upload-group-cover": "Upload group cover" + "membership.accept-invitation": "同意邀請", + "membership.invitation-pending": "邀請等待中", + "membership.join-group": "加入群組", + "membership.leave-group": "離開群組", + "membership.reject": "拒絕", + "new-group.group_name": "群組名稱:", + "upload-group-cover": "上傳群組封面圖", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite" } \ No newline at end of file diff --git a/public/language/zh_TW/login.json b/public/language/zh_TW/login.json index 4ffc8773e6..bd3cf262d9 100644 --- a/public/language/zh_TW/login.json +++ b/public/language/zh_TW/login.json @@ -1,11 +1,12 @@ { - "username-email": "用戶名稱 / 電子郵件", - "username": "用戶名稱", + "username-email": "帳號 / 電子郵件", + "username": "帳號", "email": "電子郵件", "remember_me": "記住我?", "forgot_password": "忘記密碼?", - "alternative_logins": "其他登錄方式", - "failed_login_attempt": "登錄失敗,請再嘗試。", - "login_successful": "你已成功登錄!", - "dont_have_account": "還沒有賬號?" + "alternative_logins": "其他登入方式", + "failed_login_attempt": "登入失敗", + "login_successful": "你已成功登入!", + "dont_have_account": "還沒有帳號?", + "logged-out-due-to-inactivity": "你已登出管理控制台,因為一段時間沒有任何動作。" } \ No newline at end of file diff --git a/public/language/zh_TW/modules.json b/public/language/zh_TW/modules.json index c34132769a..bb6c359596 100644 --- a/public/language/zh_TW/modules.json +++ b/public/language/zh_TW/modules.json @@ -4,11 +4,11 @@ "chat.send": "發送", "chat.no_active": "暫無聊天", "chat.user_typing": "%1 正在輸入中...", - "chat.user_has_messaged_you": "%1 已傳送訊息給你了", - "chat.see_all": "See all chats", - "chat.mark_all_read": "Mark all chats read", + "chat.user_has_messaged_you": "%1 傳送訊息給你", + "chat.see_all": "顯示全部聊天", + "chat.mark_all_read": "所有訊息標為已讀", "chat.no-messages": "請選擇收件人來查看聊天記錄", - "chat.no-users-in-room": "No users in this room", + "chat.no-users-in-room": "沒有使用者在聊天室中", "chat.recent-chats": "最近的聊天記錄", "chat.contacts": "通訊錄", "chat.message-history": "消息記錄", @@ -17,22 +17,32 @@ "chat.seven_days": "7日", "chat.thirty_days": "30日", "chat.three_months": "3個月", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", - "chat.roomname": "Chat Room %1", - "chat.add-users-to-room": "Add users to room", - "composer.compose": "Compose", - "composer.show_preview": "Show Preview", - "composer.hide_preview": "Hide Preview", - "composer.user_said_in": "%1在%2裡說:", - "composer.user_said": "%1說:", + "chat.delete_message_confirm": "你確定要刪除這個訊息?", + "chat.roomname": "聊天室 %1", + "chat.add-users-to-room": "將使用者加入聊天室中", + "composer.compose": "撰寫", + "composer.show_preview": "顯示預覽", + "composer.hide_preview": "隱藏預覽", + "composer.user_said_in": "%1 在 %2 裡說:", + "composer.user_said": "%1 說:", "composer.discard": "你確定要放棄這帖子嗎?", - "composer.submit_and_lock": "Submit and Lock", - "composer.toggle_dropdown": "Toggle Dropdown", - "composer.uploading": "Uploading %1", - "bootbox.ok": "OK", - "bootbox.cancel": "Cancel", - "bootbox.confirm": "Confirm", - "cover.dragging_title": "Cover Photo Positioning", - "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", - "cover.saved": "Cover photo image and position saved" + "composer.submit_and_lock": "提交然後鎖定", + "composer.toggle_dropdown": "切換下拉選單", + "composer.uploading": "上傳中 %1", + "composer.formatting.bold": "粗體", + "composer.formatting.italic": "斜體", + "composer.formatting.list": "列表項目", + "composer.formatting.strikethrough": "刪除線", + "composer.formatting.link": "連結", + "composer.formatting.picture": "圖片", + "composer.upload-picture": "上傳圖片", + "composer.upload-file": "上傳檔案", + "composer.zen_mode": "禪(Zen)模式", + "composer.select_category": "Select a category", + "bootbox.ok": "好", + "bootbox.cancel": "取消", + "bootbox.confirm": "確認", + "cover.dragging_title": "封面照片位置", + "cover.dragging_message": "拖拉封面照片到想要的位置,然後按下\"儲存\"", + "cover.saved": "封面照片與位置已儲存" } \ No newline at end of file diff --git a/public/language/zh_TW/notifications.json b/public/language/zh_TW/notifications.json index 3174feb38e..6028595b2f 100644 --- a/public/language/zh_TW/notifications.json +++ b/public/language/zh_TW/notifications.json @@ -1,38 +1,35 @@ { "title": "通知", "no_notifs": "沒有新消息", - "see_all": "See all notifications", + "see_all": "觀看所有通知", "mark_all_read": "所有訊息設為已讀", - "back_to_home": "返回%1", + "back_to_home": "返回 %1", "outgoing_link": "站外連結", - "outgoing_link_message": "You are now leaving %1", + "outgoing_link_message": "你現在離開了 %1", "continue_to": "繼續前往 %1", - "return_to": "返回%1", + "return_to": "返回 %1", "new_notification": "新訊息通知", - "you_have_unread_notifications": "您有未讀的訊息!", + "you_have_unread_notifications": "你有未讀的通知。", "new_message_from": "來自 <strong>%1</strong> 的新訊息", - "upvoted_your_post_in": "<strong>%1</strong> upvote了您在 <strong>%2</strong>的post。", - "upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.", - "upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.", - "moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>", - "moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>", - "favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.", - "favourited_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have bookmarked your post in <strong>%3</strong>.", - "favourited_your_post_in_multiple": "<strong>%1</strong> and %2 others have bookmarked your post in <strong>%3</strong>.", - "user_flagged_post_in": "<strong>%1</strong> 舉報了 <strong>%2</strong>裡的一個post。", - "user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>", - "user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>", + "upvoted_your_post_in": "<strong>%1</strong> 對你在 <strong>%2</strong>的張貼文投了正向票。", + "upvoted_your_post_in_dual": "<strong>%1</strong> 與 <strong>%2</strong> 已經對你在<strong>%3</strong>的張貼作正向投票。", + "upvoted_your_post_in_multiple": "<strong>%1</strong> 與 %2 其他人已經對你在<strong>%3</strong>正向投票。", + "moved_your_post": "<strong>%1</strong> 已經移動你的張貼到 <strong>%2</strong>", + "moved_your_topic": "<strong>%1</strong> 已經移動到 <strong>%2</strong>", + "user_flagged_post_in": "<strong>%1</strong> 標記了 <strong>%2</strong>裡的一個張貼。", + "user_flagged_post_in_dual": "<strong>%1</strong> 與 <strong>%2</strong> 標記一個張貼在 <strong>%3</strong>", + "user_flagged_post_in_multiple": "<strong>%1</strong> 與 %2 其他人標記一個張貼在 <strong>%3</strong>", "user_posted_to": "<strong>%1</strong> 發布一個回覆給: <strong>%2</strong>", - "user_posted_to_dual": "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>", - "user_posted_to_multiple": "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>", + "user_posted_to_dual": "<strong>%1</strong> 與 <strong>%2</strong> 已經張貼回覆到: <strong>%3</strong>", + "user_posted_to_multiple": "<strong>%1</strong> 與 %2 其他人已經張貼回覆到: <strong>%3</strong>", "user_posted_topic": "<strong>%1</strong> 發布了一個新的主題: <strong>%2</strong>", "user_started_following_you": "<strong>%1</strong> 開始關注你。", - "user_started_following_you_dual": "<strong>%1</strong> and <strong>%2</strong> started following you.", - "user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.", - "new_register": "<strong>%1</strong> sent a registration request.", - "new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.", - "email-confirmed": "已確認電郵", - "email-confirmed-message": "感謝您驗證您的電郵。您的帳戶現已全面啟用。", - "email-confirm-error-message": "驗證您的電郵地址時出現問題。也許啟動碼無效或已過期。", - "email-confirm-sent": "已發送確認電郵。" + "user_started_following_you_dual": "<strong>%1</strong> 與 <strong>%2</strong> 開始跟隨你。", + "user_started_following_you_multiple": "<strong>%1</strong> 與 %2 其他的開始跟隨你。", + "new_register": "<strong>%1</strong>傳送了註冊要求。", + "new_register_multiple": "目前有 <strong>%1</strong> 個註冊要求等待審核中。", + "email-confirmed": "已確認電子郵件", + "email-confirmed-message": "感謝你驗證電子郵件。你的帳戶現已完整的啟動。", + "email-confirm-error-message": "驗證你的電子郵件地址時發生問題。也許是啟動碼無效或是已過期。", + "email-confirm-sent": "已發送確認電子郵件。" } \ No newline at end of file diff --git a/public/language/zh_TW/pages.json b/public/language/zh_TW/pages.json index 121be06593..b6814ee916 100644 --- a/public/language/zh_TW/pages.json +++ b/public/language/zh_TW/pages.json @@ -1,46 +1,49 @@ { "home": "首頁", "unread": "未讀的主題", - "popular-day": "Popular topics today", - "popular-week": "Popular topics this week", - "popular-month": "Popular topics this month", - "popular-alltime": "All time popular topics", + "popular-day": "今天受歡迎的主題", + "popular-week": "本週受歡迎的主題", + "popular-month": "本月受歡迎的主題", + "popular-alltime": "所有時間受歡迎的主題", "recent": "近期的主題", - "flagged-posts": "Flagged Posts", - "users/online": "Online Users", - "users/latest": "Latest Users", - "users/sort-posts": "Users with the most posts", - "users/sort-reputation": "Users with the most reputation", - "users/banned": "Banned Users", - "users/search": "User Search", + "flagged-posts": "標記的張貼", + "users/online": "線上使用者", + "users/latest": "最近使用者", + "users/sort-posts": "最多張貼的使用者", + "users/sort-reputation": "最多聲譽的使用者", + "users/banned": "已封鎖使用者", + "users/most-flags": "最多標註的使用者", + "users/search": "使用者搜尋", "notifications": "新訊息通知", "tags": "標籤", - "tag": "Topics tagged under \"%1\"", - "register": "Register an account", - "login": "Login to your account", - "reset": "Reset your account password", - "categories": "Categories", - "groups": "Groups", - "group": "%1 group", - "chats": "Chats", - "chat": "Chatting with %1", - "account/edit": "Editing \"%1\"", - "account/edit/password": "Editing password of \"%1\"", - "account/edit/username": "Editing username of \"%1\"", - "account/edit/email": "Editing email of \"%1\"", - "account/following": "People %1 follows", - "account/followers": "People who follow %1", - "account/posts": "Posts made by %1", - "account/topics": "Topics created by %1", - "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", - "account/settings": "User Settings", - "account/watched": "Topics watched by %1", - "account/upvoted": "Posts upvoted by %1", - "account/downvoted": "Posts downvoted by %1", - "account/best": "Best posts made by %1", - "confirm": "Email Confirmed", + "tag": "有\"%1\"標籤的主題", + "register": "註冊帳號", + "registration-complete": "Registration complete", + "login": "登入帳號", + "reset": "重設你的帳號密碼", + "categories": "類別", + "groups": "群組", + "group": "%1 群組", + "chats": "聊天", + "chat": "與 %1 聊天", + "account/edit": "編輯 \"%1\"", + "account/edit/password": "編輯 \"%1\" 的密碼", + "account/edit/username": "編輯\"%1\"的帳號", + "account/edit/email": "編輯\"%1\"的電子郵件", + "account/info": "帳戶資訊", + "account/following": "使用者 %1 跟隨", + "account/followers": "跟隨 %1 的使用者", + "account/posts": "由 %1 發表的張貼", + "account/topics": "由 %1 建立的主題", + "account/groups": "%1 的群組", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "使用者設定", + "account/watched": "%1 所觀看的主題", + "account/upvoted": "%1 所正向投票的張貼", + "account/downvoted": "%1 所負向投票的張貼", + "account/best": "由 %1 建立的最佳張貼文", + "confirm": "已確認電子郵件", "maintenance.text": "目前 %1 正在進行維修。請稍後再來。", "maintenance.messageIntro": "此外,管理員有以下訊息:", - "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." + "throttled.text": "%1 目前無法提供,是因為使用量過度。請之後再回來操作。" } \ No newline at end of file diff --git a/public/language/zh_TW/recent.json b/public/language/zh_TW/recent.json index cf46a366d3..6076b0acb6 100644 --- a/public/language/zh_TW/recent.json +++ b/public/language/zh_TW/recent.json @@ -7,13 +7,13 @@ "alltime": "所有時間", "no_recent_topics": "最近沒有新的主題。", "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.", - "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.", + "there-is-a-new-topic": "有一篇新主題", + "there-is-a-new-topic-and-a-new-post": "有一篇新主題與一篇新張貼", + "there-is-a-new-topic-and-new-posts": "有一篇新主題與 %1 篇新張貼", + "there-are-new-topics": "有 %1 篇新主題", + "there-are-new-topics-and-a-new-post": "有 %1 篇新主題與一篇新張貼", + "there-are-new-topics-and-new-posts": "有 %1 篇新主題與 %2 篇新張貼", + "there-is-a-new-post": "有一篇新張貼", + "there-are-new-posts": "有 %1 篇新張貼", "click-here-to-reload": "點擊這裡進行重整。" } \ No newline at end of file diff --git a/public/language/zh_TW/register.json b/public/language/zh_TW/register.json index e690779ae8..9369d59865 100644 --- a/public/language/zh_TW/register.json +++ b/public/language/zh_TW/register.json @@ -1,12 +1,13 @@ { "register": "註冊", - "help.email": "默認情況下,你的郵箱不會公開。", - "help.username_restrictions": "用戶名由%1到%2個字符組成。其他人可以通過 @<span id='yourUsername'>用戶名</span> 點名你。", - "help.minimum_password_length": "密碼必須至少包含%1個字符。", + "cancel_registration": "取消註冊", + "help.email": "在預設情況下,你的電子郵件地址不會被公開。", + "help.username_restrictions": "獨立的帳號由 %1 到 %2 個字元組成。其他人可以通過 @<span id='yourUsername'>帳號</span> 提及你。", + "help.minimum_password_length": "密碼必須至少包含 %1 個字元。", "email_address": "Email", - "email_address_placeholder": "輸入郵箱地址", - "username": "用戶名", - "username_placeholder": "輸入用戶名", + "email_address_placeholder": "輸入電子郵件地址", + "username": "帳號", + "username_placeholder": "輸入帳號", "password": "密碼", "password_placeholder": "輸入密碼", "confirm_password": "確認密碼", @@ -15,5 +16,8 @@ "alternative_registration": "其他註冊方式", "terms_of_use": "使用條款", "agree_to_terms_of_use": "同意遵守使用條款", - "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." + "terms_of_use_error": "你需要同意使用條款", + "registration-added-to-queue": "你的註冊已經被加入到審核序列中。你將會在管理者批準後收到一封電子郵件。", + "interstitial.intro": "我們要求一些額外的資訊,在我們建立你的帳戶之前。", + "interstitial.errors-found": "我們無法完成你的註冊" } \ No newline at end of file diff --git a/public/language/zh_TW/reset_password.json b/public/language/zh_TW/reset_password.json index 5c2964579c..f7d83c8dd1 100644 --- a/public/language/zh_TW/reset_password.json +++ b/public/language/zh_TW/reset_password.json @@ -4,14 +4,14 @@ "password_changed.title": "密碼已更改", "password_changed.message": "<p>密碼重設成功,請重新<a href=\"/login\">登錄</a>。", "wrong_reset_code.title": "驗証碼不正確", - "wrong_reset_code.message": "您輸入的驗証碼有誤,請重新輸入,或<a href=\"/reset\">申請新的驗証碼</a>。", + "wrong_reset_code.message": "重置驗証碼不正確。請重新輸入,或是<a href=\"/reset\">申請新重置驗証碼</a>。", "new_password": "輸入新的密碼", "repeat_password": "再次確認新密碼", - "enter_email": "請輸入您的<strong>電子郵件地址</strong>,我們會寄送郵件告訴您如何重設密碼。", + "enter_email": "請輸入你的<strong>電子郵件地址</strong>,我們會寄送郵件告訴你如何重置你的帳戶。", "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_expired": "Your password has expired, please choose a new password" + "password_too_short": "輸入的密碼太短,請使用另一個不同的密碼", + "passwords_do_not_match": "你已經輸入的兩個密碼不一樣", + "password_expired": "你的密碼已過期,請選擇一組新密碼" } \ No newline at end of file diff --git a/public/language/zh_TW/search.json b/public/language/zh_TW/search.json index 9eadd12f6d..58b8258ed4 100644 --- a/public/language/zh_TW/search.json +++ b/public/language/zh_TW/search.json @@ -1,17 +1,17 @@ { - "results_matching": "有%1個跟\"%2\"相符的結果(%3秒)", + "results_matching": "有 %1 個與 \"%2\" 符合的結果 (%3秒)", "no-matches": "沒有找到相符的主題", - "advanced-search": "Advanced Search", + "advanced-search": "進階搜尋", "in": "在", "titles": "標題", - "titles-posts": "標題與發布", - "posted-by": "Posted by", + "titles-posts": "標題與張貼", + "posted-by": "張貼由", "in-categories": "在類別中", "search-child-categories": "搜尋子類別", "reply-count": "回覆數量", "at-least": "最少", "at-most": "最多", - "post-time": "發布時間", + "post-time": "張貼時間", "newer-than": "較新", "older-than": "較舊", "any-date": "任意日期", @@ -22,7 +22,7 @@ "three-months": "三個月", "six-months": "六個月", "one-year": "一年", - "sort-by": "排序依據", + "sort-by": "排序依照", "last-reply-time": "最後回覆時間", "topic-title": "主題", "number-of-replies": "回覆數量", diff --git a/public/language/zh_TW/success.json b/public/language/zh_TW/success.json index 7d3170435b..1d652b82e6 100644 --- a/public/language/zh_TW/success.json +++ b/public/language/zh_TW/success.json @@ -1,6 +1,6 @@ { "success": "成功", - "topic-post": "您的文章已張貼成功", + "topic-post": "你的文章已張貼成功", "authentication-successful": "驗證成功", "settings-saved": "設定已儲存" } \ No newline at end of file diff --git a/public/language/zh_TW/tags.json b/public/language/zh_TW/tags.json index ce9de5c88b..8b2046b293 100644 --- a/public/language/zh_TW/tags.json +++ b/public/language/zh_TW/tags.json @@ -1,7 +1,7 @@ { "no_tag_topics": "沒有此標籤的主題。", "tags": "標籤", - "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here": "在這裡輸入標籤,每個介於 %1 到 %2 字元。 ", "enter_tags_here_short": "輸入標籤...", "no_tags": "還沒有標籤呢。" } \ No newline at end of file diff --git a/public/language/zh_TW/topic.json b/public/language/zh_TW/topic.json index 62f48e9523..50ea975eb8 100644 --- a/public/language/zh_TW/topic.json +++ b/public/language/zh_TW/topic.json @@ -13,7 +13,7 @@ "notify_me": "該主題有新回覆時通知我", "quote": "引用", "reply": "回覆", - "reply-as-topic": "Reply as topic", + "reply-as-topic": "回復為另一個新主題", "guest-login-reply": "登入以回覆", "edit": "編輯", "delete": "刪除", @@ -26,32 +26,56 @@ "tools": "工具", "flag": "檢舉", "locked": "已鎖定", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "pinned": "釘住", + "moved": "移動", + "bookmark_instructions": "點擊這裡返回到這個討論串的最後一篇張貼文", "flag_title": "檢舉這篇文章, 交給仲裁者來審閱.", "flag_success": "這文章已經被檢舉要求仲裁.", - "deleted_message": "此主題已被刪除。只有具有主題管理權限的用戶才能看到它。", + "flag_manage_title": "Flagged post in %1", + "flag_manage_history": "Action History", + "flag_manage_no_history": "No event history to report", + "flag_manage_assignee": "Assignee", + "flag_manage_state": "State", + "flag_manage_state_open": "New/Open", + "flag_manage_state_wip": "Work in Progress", + "flag_manage_state_resolved": "Resolved", + "flag_manage_state_rejected": "Rejected", + "flag_manage_notes": "Shared Notes", + "flag_manage_update": "Update Flag Status", + "flag_manage_history_assignee": "Assigned to %1", + "flag_manage_history_state": "Updated state to %1", + "flag_manage_history_notes": "Updated flag notes", + "flag_manage_saved": "Flag Details Updated", + "deleted_message": "此主題已被刪除。只有具有主題管理權限的使用者才能看到它。", "following_topic.message": "有人貼文回覆主題時, 你將會收到新通知.", - "not_following_topic.message": "有人貼文回覆主題時, 你將不會收到通知.", + "not_following_topic.message": "你將會看到這個主題在未讀主題列表中出現,但你將不會在其他人張貼到這個主題時接收到通知。", + "ignoring_topic.message": "你將不會再未讀主題列表中看到這個主題。當你被提及或你的張貼被正向投票時,你會被通知。", "login_to_subscribe": "請先註冊或登錄, 才可訂閱此主題.", "markAsUnreadForAll.success": "將全部的主題設為未讀.", - "mark_unread": "Mark unread", - "mark_unread.success": "Topic marked as unread.", + "mark_unread": "標為未讀", + "mark_unread.success": "標記主題為未讀", "watch": "關注", "unwatch": "取消關注", "watch.title": "當主題有新回覆時將收到通知", "unwatch.title": "停止關注這個主題", "share_this_post": "分享這篇文章", + "watching": "關注", + "not-watching": "取消關注", + "ignoring": "忽略", + "watching.description": "有新的回覆通知我。<br/>在未讀頁中顯示主題。", + "not-watching.description": "有新的回覆不用通知我。<br/>如果類別不是被忽略的,在未讀頁中顯示主題。", + "ignoring.description": "有新的回覆不用通知我。<br/>在未讀頁中不顯示主題。", "thread_tools.title": "主題工具箱", - "thread_tools.markAsUnreadForAll": "設為未讀", + "thread_tools.markAsUnreadForAll": "Mark unread for all", "thread_tools.pin": "釘選主題", "thread_tools.unpin": "取消釘選主題", "thread_tools.lock": "鎖定主題", "thread_tools.unlock": "解除主題鎖定", "thread_tools.move": "移動主題", "thread_tools.move_all": "移動全部", - "thread_tools.fork": "Fork 主題", + "thread_tools.fork": "分叉主題", "thread_tools.delete": "刪除主題", - "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete-posts": "刪除張貼", "thread_tools.delete_confirm": "你確定要刪除這個主題?", "thread_tools.restore": "還原刪除的主題", "thread_tools.restore_confirm": "你確定你要恢復這個主題嗎?", @@ -65,9 +89,9 @@ "disabled_categories_note": "停用的版面為灰色", "confirm_move": "移動", "confirm_fork": "作為主題", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "favourite": "書籤", + "favourites": "書籤", + "favourites.has_no_favourites": "你尚未將任何張貼加入書籤", "loading_more_posts": "載入更多文章", "move_topic": "移動主題", "move_topics": "移動主題", @@ -77,8 +101,9 @@ "topic_will_be_moved_to": "這個主題將會被移動到類別", "fork_topic_instruction": "點擊要作為主題的文章", "fork_no_pids": "尚未選擇文章!", + "fork_pid_count": "%1 張貼已選定", "fork_success": "成功分叉成新的主題!點擊這裡進入新的主題。", - "delete_posts_instruction": "Click the posts you want to delete/purge", + "delete_posts_instruction": "點擊你想要刪除/清除的張貼", "composer.title_placeholder": "輸入標題...", "composer.handle_placeholder": "名字", "composer.discard": "放棄", @@ -92,21 +117,21 @@ "composer.thumb_file_label": "或上傳檔案", "composer.thumb_remove": "清除所有欄目", "composer.drag_and_drop_images": "拖曳影像到此", - "more_users_and_guests": "%1 個用戶和 %2個訪客", - "more_users": "%1 個用戶", + "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": "從新到舊", "most_votes": "得票最多", - "most_posts": "最多post", - "stale.title": "Create new topic instead?", - "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", - "stale.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", - "link_back": "Re: [%1](%2)", - "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "most_posts": "最多張貼", + "stale.title": "改為建立新的主題?", + "stale.warning": "你正回覆的主題是非常舊的一篇。你想要改為建立一個新主題,然後參考到這篇你回覆的?", + "stale.create": "建立新主題", + "stale.reply_anyway": "無論如何都回覆這個主題", + "link_back": "回覆: [%1](%2)", + "spam": "灌水", + "offensive": "攻擊", + "custom-flag-reason": "輸入標記的理由" } \ No newline at end of file diff --git a/public/language/zh_TW/unread.json b/public/language/zh_TW/unread.json index b8e2086168..b11d2ee762 100644 --- a/public/language/zh_TW/unread.json +++ b/public/language/zh_TW/unread.json @@ -5,6 +5,9 @@ "mark_as_read": "標記成已讀", "selected": "已選擇", "all": "全部", - "all_categories": "All categories", - "topics_marked_as_read.success": "標記主題成已讀!" + "all_categories": "所有類別", + "topics_marked_as_read.success": "標記主題成已讀!", + "all-topics": "所有主題", + "new-topics": "新主題", + "watched-topics": "已觀看主題" } \ No newline at end of file diff --git a/public/language/zh_TW/uploads.json b/public/language/zh_TW/uploads.json index 1622cb5693..aaef40f16c 100644 --- a/public/language/zh_TW/uploads.json +++ b/public/language/zh_TW/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "檔案上傳中...", + "select-file-to-upload": "選擇要上傳的檔案!", + "upload-success": "檔案已成功上傳!", + "maximum-file-size": "最大大小 %1 kb" } \ No newline at end of file diff --git a/public/language/zh_TW/user.json b/public/language/zh_TW/user.json index 118ba2f471..7ba2d27311 100644 --- a/public/language/zh_TW/user.json +++ b/public/language/zh_TW/user.json @@ -5,14 +5,15 @@ "joindate": "加入時間", "postcount": "文章數量", "email": "電子郵件", - "confirm_email": "確認電郵", - "ban_account": "Ban Account", - "ban_account_confirm": "Do you really want to ban this user?", - "unban_account": "Unban Account", + "confirm_email": "確認電子郵件", + "account_info": "帳戶資訊", + "ban_account": "禁用帳號", + "ban_account_confirm": "你確定要禁用這個使用者?", + "unban_account": "取消禁用帳號", "delete_account": "刪除帳戶", - "delete_account_confirm": "你確定要刪除自己的帳戶? <br /><strong>此操作不能復原,您將無法恢復任何數據</strong><br /><br />輸入您的使用者名稱,以確認您希望刪除這個帳戶。", - "delete_this_account_confirm": "Are you sure you want to delete this account? <br /><strong>This action is irreversible and you will not be able to recover any data</strong><br /><br />", - "account-deleted": "Account deleted", + "delete_account_confirm": "你確定要刪除自己的帳戶? <br /><strong>此操作不能復原,你將無法恢復任何資料</strong><br /><br />輸入你的帳號,來確認你希望刪除這個帳戶。", + "delete_this_account_confirm": "你確定要刪除這個帳戶? <br /><strong>此操作是不能還原的,你將無法回復任何資料</strong><br />", + "account-deleted": "帳號已刪除", "fullname": "全名", "website": "網站", "location": "地址", @@ -22,88 +23,107 @@ "profile": "個人資料", "profile_views": "資料被查看", "reputation": "聲譽", - "favourites": "Bookmarks", + "favourites": "書籤", "watched": "觀看者", "followers": "跟隨者", "following": "正在關注", - "aboutme": "About me", + "aboutme": "關於我", "signature": "簽名", "birthday": "生日", "chat": "聊天", - "chat_with": "Chat with %1", + "chat_with": "與 %1 聊天", "follow": "跟隨", "unfollow": "取消跟隨", "more": "更多", - "profile_update_success": "您的個人資料已更新成功!", + "profile_update_success": "你的個人資料已更新成功!", "change_picture": "改變頭像", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "更改帳號", + "change_email": "更改電子郵件", "edit": "編輯", - "edit-profile": "Edit Profile", - "default_picture": "Default Icon", + "edit-profile": "編輯個人資料", + "default_picture": "預設圖示", "uploaded_picture": "已有頭像", "upload_new_picture": "上傳新頭像", - "upload_new_picture_from_url": "上傳新頭像(URL)", + "upload_new_picture_from_url": "從網址上傳新頭像", "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_error_privileges": "你沒有變更此密碼的權限!", "change_password_success": "你的密碼已經更新!", "confirm_password": "確認密碼", "password": "密碼", - "username_taken_workaround": "您所註冊的使用者名稱已經被使用了,所以我們將它略微改變。你現在改稱 <strong>%1</strong>", - "password_same_as_username": "Your password is the same as your username, please select another password.", - "password_same_as_email": "Your password is the same as your email, please select another password.", + "username_taken_workaround": "你想要註冊的帳號已經被使用了,所以我們將它略作改變。你現在的帳號名稱是 <strong>%1</strong>", + "password_same_as_username": "你的密碼和帳號是一樣的,請選擇另一組密碼。", + "password_same_as_email": "你的密碼和電子郵件是一樣的,請選擇另一組密碼。", "upload_picture": "上傳頭像", "upload_a_picture": "上傳一張照片", - "remove_uploaded_picture": "Remove Uploaded Picture", - "upload_cover_picture": "Upload cover picture", + "remove_uploaded_picture": "移除上傳的圖片", + "upload_cover_picture": "上傳封面圖片", "settings": "設定", - "show_email": "顯示我的郵箱", + "show_email": "顯示我的電子郵件地址", "show_fullname": "顯示我的全名", - "restrict_chats": "只允許我跟隨的用戶和我聊天", + "restrict_chats": "只允許我跟隨的使用者和我聊天", "digest_label": "訂閱摘要", - "digest_description": "使用 eamil 訂閱本論壇新的訊息通知與主題 (根據您所設的時間表)", + "digest_description": "根據你所設的時間排程,用電子郵件訂閱這個討論區 (新的通知與主題)。", "digest_off": "關閉", "digest_daily": "每日", "digest_weekly": "每週", "digest_monthly": "每月", "send_chat_notifications": "如果有新的聊天消息而我不在線,發送郵件給我", - "send_post_notifications": "Send an email when replies are made to topics I am subscribed to", - "settings-require-reload": "Some setting changes require a reload. Click here to reload the page.", - "has_no_follower": "該用戶還沒有被任何人關注。", - "follows_no_one": "該用戶還沒有關注過任何人。", - "has_no_posts": "This user hasn't posted anything yet.", - "has_no_topics": "This user hasn't posted any topics yet.", - "has_no_watched_topics": "This user hasn't watched any topics yet.", - "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", - "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", - "has_no_voted_posts": "This user has no voted posts", - "email_hidden": "郵箱被隱藏", + "send_post_notifications": "當我訂閱的主題有新回覆時寄送Email給我", + "settings-require-reload": "有些設定的更動是需要重新整理。點擊這裡來重新整理頁面。", + "has_no_follower": "該使用者還沒有被任何人關注。", + "follows_no_one": "該使用者還沒有關注過任何人。", + "has_no_posts": "使用者還沒有發表任何張貼", + "has_no_topics": "使用者還沒有發表任何主題", + "has_no_watched_topics": "使用者還沒有觀看任何主題", + "has_no_upvoted_posts": "使用者還沒有對任何主題投正向票", + "has_no_downvoted_posts": "使用者還沒有對任何主題投負向票", + "has_no_voted_posts": "這個使用者沒有投票的張貼", + "email_hidden": "電子郵件地址被隱藏", "hidden": "隱藏", - "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "paginate_description": "將主題與張貼用分頁來顯示,取代使用無盡的捲動方式。", "topics_per_page": "每頁的主題數", "posts_per_page": "每頁的文章數", - "notification_sounds": "Play a sound when you receive a notification", + "notification_sounds": "當你接收到通知時撥放音效", + "notifications_and_sounds": "通知&音效", + "incoming-message-sound": "收到訊息音效", + "outgoing-message-sound": "發出訊息音效", + "notification-sound": "通知音效", + "no-sound": "No sound", "browsing": "瀏覽設定", - "open_links_in_new_tab": "Open outgoing links in new tab", - "enable_topic_searching": "Enable In-Topic Searching", - "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", - "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Follow topics that you reply to", - "follow_topics_you_create": "Follow topics you create", - "grouptitle": "Select the group title you would like to display", + "open_links_in_new_tab": "在新的資料標籤裡打開外部的連結", + "enable_topic_searching": "啟用在主題中的搜尋", + "topic_search_help": "如果啟用的話,在主題中搜尋將會覆蓋瀏覽器預設網頁搜尋的行為,這會允許你搜尋整篇的主題,而不只是顯示出來在畫面上的而已。", + "delay_image_loading": "延遲圖片載入", + "image_load_delay_help": "如果啟用的話,在主題中的圖片直到它們被捲到到可視範圍時才會載入。", + "scroll_to_my_post": "在張貼回覆後,顯示新的張貼", + "follow_topics_you_reply_to": "關注你有回覆的主題", + "follow_topics_you_create": "關注你建立的主題", + "grouptitle": "群組標題", "no-group-title": "無此群組標題", - "select-skin": "Select a Skin", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", - "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", - "custom_route": "Custom Homepage Route", - "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")", - "sso.title": "Single Sign-on Services", - "sso.associated": "Associated with", - "sso.not-associated": "Click here to associate with" + "select-skin": "選擇外觀", + "select-homepage": "選擇首頁", + "homepage": "首頁", + "homepage_description": "選擇一個要用來當作討論區的首頁的頁面,或是選None使用預設的首頁。", + "custom_route": "自訂首頁路由", + "custom_route_help": "在這裡輸入路由名稱,不需要在前面的斜線(例如recent或popular)", + "sso.title": "單一簽入SSO服務", + "sso.associated": "關連於", + "sso.not-associated": "點擊這裡進行關連於", + "info.latest-flags": "最近標註", + "info.no-flags": "沒有找到標註的張貼", + "info.ban-history": "最近禁用歷史", + "info.no-ban-history": "這個使用者永遠不會被禁用", + "info.banned-until": "禁用至 %1", + "info.banned-permanently": "永久禁用", + "info.banned-reason-label": "理由", + "info.banned-no-reason": "沒有給理由", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved" } \ No newline at end of file diff --git a/public/language/zh_TW/users.json b/public/language/zh_TW/users.json index 9d42c05416..dc08b3056e 100644 --- a/public/language/zh_TW/users.json +++ b/public/language/zh_TW/users.json @@ -1,20 +1,21 @@ { - "latest_users": "最近用戶", + "latest_users": "最近使用者", "top_posters": "發文數最多", "most_reputation": "聲望最高", + "most_flags": "最多標註", "search": "搜尋", "enter_username": "輸入想找的使用者帳號", "load_more": "載入更多", - "users-found-search-took": "發現 %1 用戶!搜尋只用 %2 秒。", - "filter-by": "Filter By", + "users-found-search-took": "找到 %1 使用者! 搜尋用了 %2 秒。", + "filter-by": "過濾依照", "online-only": "線上僅有", - "invite": "Invite", - "invitation-email-sent": "An invitation email has been sent to %1", - "user_list": "User List", - "recent_topics": "Recent Topics", - "popular_topics": "Popular Topics", - "unread_topics": "Unread Topics", - "categories": "Categories", - "tags": "Tags", - "no-users-found": "No users found!" + "invite": "邀請", + "invitation-email-sent": "所有邀請Email已經被寄送到 %1", + "user_list": "使用者列表", + "recent_topics": "最新的主題", + "popular_topics": "受歡迎的主題", + "unread_topics": "未讀的主題", + "categories": "類別", + "tags": "標籤", + "no-users-found": "沒有找到使用者!" } \ No newline at end of file diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index e733d8d9bd..0ceeb050e5 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -11,6 +11,7 @@ @import "./manage/categories"; @import "./manage/tags"; @import "./manage/groups"; +@import "./manage/registration"; @import "./manage/users"; @import "./appearance/customise"; @import "./appearance/themes"; @@ -18,6 +19,7 @@ @import "./extend/rewards"; @import "./advanced/database"; @import "./advanced/logs"; +@import "./advanced/errors"; @import "./settings"; @import "../flags"; @@ -26,6 +28,11 @@ @import "./modules/alerts"; @import "./modules/selectable"; @import "./modules/snackbar"; +@import "./modules/nprogress"; + +body { + overflow-y: scroll; +} .admin { background: #fff; @@ -50,12 +57,30 @@ color: @link-color; } + // .floating-button can either be a container or the button itself .floating-button { - background: @brand-primary !important; position: fixed; right: 30px; bottom: 30px; z-index: 1; + max-width: 56px; + + button { + &.primary { + background: @brand-primary !important; + } + + &.success { + background: @brand-success !important; + } + + &:not(:last-child) { + margin-bottom: 2rem; + } + } + } + button.floating-button { + background: @brand-primary !important; } .user-img { @@ -240,4 +265,10 @@ [class^="col-"] .mdl-switch__label { padding-right: 15px; +} + +.ui-selectable-helper { + border: 1px dashed @brand-success; + background: lighten(@brand-success, 10%); + opacity: 0.5; } \ No newline at end of file diff --git a/public/less/admin/advanced/errors.less b/public/less/admin/advanced/errors.less new file mode 100644 index 0000000000..89b9dda540 --- /dev/null +++ b/public/less/admin/advanced/errors.less @@ -0,0 +1,26 @@ + +.page-advanced-errors { + .table { + table-layout: fixed; + + th { + &:first-child { + width: 90%; + } + + &:last-child { + text-align: center; + } + } + + td { + &:first-child { + word-wrap: break-word; + } + + &:last-child { + text-align: center; + } + } + } +} \ No newline at end of file diff --git a/public/less/admin/bootstrap/bootstrap.less b/public/less/admin/bootstrap/bootstrap.less index 1c0477805f..f0aa08f3a6 100644 --- a/public/less/admin/bootstrap/bootstrap.less +++ b/public/less/admin/bootstrap/bootstrap.less @@ -1,6 +1,6 @@ /*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ diff --git a/public/less/admin/bootstrap/button-groups.less b/public/less/admin/bootstrap/button-groups.less index 293245a650..16db0c6135 100644 --- a/public/less/admin/bootstrap/button-groups.less +++ b/public/less/admin/bootstrap/button-groups.less @@ -59,7 +59,7 @@ .border-right-radius(0); } } -// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it +// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { .border-left-radius(0); diff --git a/public/less/admin/bootstrap/forms.less b/public/less/admin/bootstrap/forms.less index e8b071a138..9377d3846b 100644 --- a/public/less/admin/bootstrap/forms.less +++ b/public/less/admin/bootstrap/forms.less @@ -181,7 +181,7 @@ input[type="search"] { // set a pixel line-height that matches the given height of the input, but only // for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848 // -// Note that as of 8.3, iOS doesn't support `datetime` or `week`. +// Note that as of 9.3, iOS doesn't support `week`. @media screen and (-webkit-min-device-pixel-ratio: 0) { input[type="date"], diff --git a/public/less/admin/bootstrap/input-groups.less b/public/less/admin/bootstrap/input-groups.less index 5f73eec40c..d0763db7ff 100644 --- a/public/less/admin/bootstrap/input-groups.less +++ b/public/less/admin/bootstrap/input-groups.less @@ -29,7 +29,7 @@ width: 100%; margin-bottom: 0; - + &:focus { z-index: 3; } diff --git a/public/less/admin/bootstrap/mixins/tab-focus.less b/public/less/admin/bootstrap/mixins/tab-focus.less index 1f1f05ab05..d12d23629f 100644 --- a/public/less/admin/bootstrap/mixins/tab-focus.less +++ b/public/less/admin/bootstrap/mixins/tab-focus.less @@ -1,9 +1,9 @@ // WebKit-style focus .tab-focus() { - // Default - outline: thin dotted; - // WebKit + // WebKit-specific. Other browsers will keep their default outline style. + // (Initially tried to also force default via `outline: initial`, + // but that seems to erroneously remove the outline in Firefox altogether.) outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } diff --git a/public/less/admin/bootstrap/panels.less b/public/less/admin/bootstrap/panels.less index 425eb5e642..65aa3a83f3 100644 --- a/public/less/admin/bootstrap/panels.less +++ b/public/less/admin/bootstrap/panels.less @@ -214,7 +214,7 @@ } -// Collapsable panels (aka, accordion) +// Collapsible panels (aka, accordion) // // Wrap a series of panels in `.panel-group` to turn them into an accordion with // the help of our collapse JavaScript plugin. diff --git a/public/less/admin/bootstrap/scaffolding.less b/public/less/admin/bootstrap/scaffolding.less index 1929bfc5cf..64a29c6a5e 100644 --- a/public/less/admin/bootstrap/scaffolding.less +++ b/public/less/admin/bootstrap/scaffolding.less @@ -120,7 +120,7 @@ hr { // Only display content to screen readers // -// See: http://a11yproject.com/posts/how-to-hide-content/ +// See: http://a11yproject.com/posts/how-to-hide-content .sr-only { position: absolute; diff --git a/public/less/admin/bootstrap/theme.less b/public/less/admin/bootstrap/theme.less index 8f51d913dc..fb6174427b 100644 --- a/public/less/admin/bootstrap/theme.less +++ b/public/less/admin/bootstrap/theme.less @@ -1,6 +1,6 @@ /*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ diff --git a/public/less/admin/bootstrap/variables.less b/public/less/admin/bootstrap/variables.less index d0cf54f043..eed5b3cfae 100644 --- a/public/less/admin/bootstrap/variables.less +++ b/public/less/admin/bootstrap/variables.less @@ -1,4 +1,4 @@ -// Paper 3.3.5 +// Paper 3.3.7 // Variables // -------------------------------------------------- @@ -102,16 +102,16 @@ @line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome @line-height-small: 1.5; -@border-radius-base: 0px; -@border-radius-large: 0px; -@border-radius-small: 0px; +@border-radius-base: 3px; +@border-radius-large: 3px; +@border-radius-small: 3px; //** Global color for active items (e.g., navs or dropdowns). @component-active-color: #fff; //** Global background color for active items (e.g., navs or dropdowns). @component-active-bg: @brand-primary; -//** Width of the `border` for generating carets that indicator dropdowns. +//** Width of the `border` for generating carets that indicate dropdowns. @caret-width-base: 4px; //** Carets increase slightly in size for larger components. @caret-width-large: 5px; @@ -486,7 +486,7 @@ @jumbotron-padding: 30px; @jumbotron-color: inherit; -@jumbotron-bg: #f9f9f9; +@jumbotron-bg: #f5f5f5; @jumbotron-heading-color: @headings-color; @jumbotron-font-size: ceil((@font-size-base * 1.5)); @jumbotron-heading-font-size: ceil((@font-size-base * 4.5)); @@ -555,7 +555,7 @@ //** Popover outer arrow width @popover-arrow-outer-width: (@popover-arrow-width + 1); //** Popover outer arrow color -@popover-arrow-outer-color: fadein(@popover-border-color, 7.5%); +@popover-arrow-outer-color: fadein(@popover-border-color, 12%); //** Popover outer arrow fallback color @popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); @@ -760,7 +760,7 @@ // //## -@well-bg: #f9f9f9; +@well-bg: #f5f5f5; @well-border: transparent; @@ -866,4 +866,4 @@ //** Point at which .dl-horizontal becomes horizontal @dl-horizontal-breakpoint: @grid-float-breakpoint; //** Horizontal line color. -@hr-border: @gray-lighter; +@hr-border: @gray-lighter; \ No newline at end of file diff --git a/public/less/admin/general/navigation.less b/public/less/admin/general/navigation.less index e56f4cc409..3e10639b89 100644 --- a/public/less/admin/general/navigation.less +++ b/public/less/admin/general/navigation.less @@ -1,7 +1,10 @@ #navigation { #active-navigation { width: 100%; - + min-height: 50px; + border: 1px solid #eee; + overflow: auto; + .active { background-color: #eee; } diff --git a/public/less/admin/header.less b/public/less/admin/header.less index ea0e9fb0d1..7dd9540c38 100644 --- a/public/less/admin/header.less +++ b/public/less/admin/header.less @@ -29,6 +29,11 @@ } } + .fa-home { + margin-top: 12px; + font-size: 25px; + } + #user_dropdown { font-size: 25px; color: #eee; @@ -89,4 +94,11 @@ } } } -} \ No newline at end of file + + .reconnect-spinner { + left: auto; + right: 380px; + bottom: initial; + top: 14px; + } +} diff --git a/public/less/admin/manage/registration.less b/public/less/admin/manage/registration.less new file mode 100644 index 0000000000..ea483e727d --- /dev/null +++ b/public/less/admin/manage/registration.less @@ -0,0 +1,7 @@ +@media screen and (max-width: @screen-sm-max) { + .page-manage-registration { + .users-list { + font-size: 10px; + } + } +} \ No newline at end of file diff --git a/public/less/admin/manage/tags.less b/public/less/admin/manage/tags.less index 6800778237..34075816b1 100644 --- a/public/less/admin/manage/tags.less +++ b/public/less/admin/manage/tags.less @@ -5,14 +5,23 @@ } .tag-row { - padding: 5px; + padding: 0.5rem; float: left; + margin-left: 0.5rem; .tag-item { cursor: pointer; display: inline-block; font-size: 11px; } + + &.ui-selected { + background: lighten(@brand-success, 25%); + } + + &.ui-selecting { + background: lighten(@brand-success, 40%); + } } } diff --git a/public/less/admin/manage/users.less b/public/less/admin/manage/users.less index bed33b77a1..0ab74c7544 100644 --- a/public/less/admin/manage/users.less +++ b/public/less/admin/manage/users.less @@ -16,6 +16,7 @@ height: auto; max-width: 145px; min-width: 145px; + padding: 1rem; img, .user-icon { .user-icon-style(80px, 4rem); @@ -44,5 +45,13 @@ } } } + + .ui-selected { + background: lighten(@brand-success, 25%); + } + + .ui-selecting { + background: lighten(@brand-success, 40%); + } } } \ No newline at end of file diff --git a/public/less/admin/mobile.less b/public/less/admin/mobile.less index a324072679..e0e8ddb1dc 100644 --- a/public/less/admin/mobile.less +++ b/public/less/admin/mobile.less @@ -37,12 +37,12 @@ } #mobile-menu { - width: 31px; + width: 22px; background: none; border: none; vertical-align: 10%; margin-right: 10px; - margin-left: -15px; + margin-left: -5px; outline: none !important; display: block; diff --git a/public/less/admin/modules/nprogress.less b/public/less/admin/modules/nprogress.less new file mode 100644 index 0000000000..c1b6effe02 --- /dev/null +++ b/public/less/admin/modules/nprogress.less @@ -0,0 +1,80 @@ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #29d; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #29d, 0 0 5px #29d; + opacity: 1.0; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 165px; + right: 35px; +} + +@media (max-width: @screen-xs-max) { + #nprogress .spinner { + bottom: 15px; + right: 15px; + top: initial; + } +} + + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #29d; + border-left-color: #29d; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes nprogress-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + diff --git a/public/less/flags.less b/public/less/flags.less index da44e61552..29b5998933 100644 --- a/public/less/flags.less +++ b/public/less/flags.less @@ -31,4 +31,8 @@ .user-icon-style(24px, 1.5rem); } } + + [component="posts/flag/history"] .avatar { + margin-right: 1rem; + } } \ No newline at end of file diff --git a/public/less/generics.less b/public/less/generics.less index fee1157662..8533dd33b9 100644 --- a/public/less/generics.less +++ b/public/less/generics.less @@ -15,6 +15,18 @@ .define-if-not-set(); +#move_thread_modal .category-list { + height: 500px; + overflow-y: auto; + overflow-x: hidden; +} + +.topic-watch-dropdown { + .help-text { + margin-left: 20px; + } +} + .category-list { padding: 0; @@ -76,12 +88,24 @@ /* Contains the user icon class as a mixin, so there's no need to include that in the template */ .user-icon; + &.avatar-xs { + width: 16px; + height: 16px; + .user-icon-style(16px, 1rem); + } + &.avatar-sm { width: 24px; height: 24px; .user-icon-style(24px, 1.5rem); } + &.avatar-md { + width: 32px; + height: 32px; + .user-icon-style(32px, 1.5rem); + } + &.avatar-lg { width: 128px; height: 128px; @@ -91,4 +115,18 @@ &.avatar-rounded { border-radius: 50%; } +} + +.ban-modal { + .form-inline, .form-group { + width: 100%; + } + + .units { + line-height: 5rem; + } +} + +.admin .ban-modal .units { + line-height: 1.846; } \ No newline at end of file diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index bed154107a..9d56403854 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -1,17 +1,45 @@ "use strict"; -/*global config, translator, componentHandler, define, socket, app, ajaxify, utils, bootbox, Mousetrap, Hammer, Slideout, RELATIVE_PATH*/ +/*global config, componentHandler, socket, app, bootbox, Slideout, NProgress*/ -(function() { - $(document).ready(function() { - setupKeybindings(); +(function () { + var logoutTimer = 0; + function startLogoutTimer() { + if (logoutTimer) { + clearTimeout(logoutTimer); + } + + logoutTimer = setTimeout(function () { + require(['translator'], function (translator) { + translator.translate('[[login:logged-out-due-to-inactivity]]', function (translated) { + bootbox.alert({ + closeButton: false, + message: translated, + callback: function (){ + window.location.reload(); + } + }); + }); + }); + }, 3600000); + } + + $(window).on('action:ajaxify.end', function () { + showCorrectNavTab(); + startLogoutTimer(); + }); - // on page reload show correct tab if url has # + function showCorrectNavTab() { + // show correct tab if url has # if (window.location.hash) { - $('.nav-pills a[href=' + window.location.hash + ']').tab('show'); + $('.nav-pills a[href="' + window.location.hash + '"]').tab('show'); } + } + + $(document).ready(function () { + setupKeybindings(); if(!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { - require(['admin/modules/search'], function(search) { + require(['admin/modules/search'], function (search) { search.init(); }); } @@ -20,32 +48,43 @@ app.alert = launchSnackbar; configureSlidemenu(); + setupNProgress(); }); - $(window).on('action:ajaxify.contentLoaded', function(ev, data) { - var url = data.url; - + $(window).on('action:ajaxify.contentLoaded', function (ev, data) { selectMenuItem(data.url); setupRestartLinks(); componentHandler.upgradeDom(); }); - function setupKeybindings() { - Mousetrap.bind('ctrl+shift+a r', function() { - require(['admin/modules/instance'], function(instance) { - instance.reload(); - }); + function setupNProgress() { + $(window).on('action:ajaxify.start', function () { + NProgress.set(0.7); }); - Mousetrap.bind('ctrl+shift+a R', function() { - socket.emit('admin.restart'); + $(window).on('action:ajaxify.end', function () { + NProgress.done(); }); + } + + function setupKeybindings() { + require(['mousetrap'], function (mousetrap) { + mousetrap.bind('ctrl+shift+a r', function () { + require(['admin/modules/instance'], function (instance) { + instance.reload(); + }); + }); - Mousetrap.bind('/', function(e) { - $('#acp-search input').focus(); + mousetrap.bind('ctrl+shift+a R', function () { + socket.emit('admin.restart'); + }); + + mousetrap.bind('/', function () { + $('#acp-search input').focus(); - return false; + return false; + }); }); } @@ -61,7 +100,7 @@ } $('#main-menu li').removeClass('active'); - $('#main-menu a').removeClass('active').each(function() { + $('#main-menu a').removeClass('active').each(function () { var menu = $(this), href = menu.attr('href'), isLink = menu.parent().attr('data-link') === '1'; @@ -76,7 +115,7 @@ }); var acpPath = url.replace('admin/', '').split('/'); - acpPath.forEach(function(path, i) { + acpPath.forEach(function (path, i) { acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); }); acpPath = acpPath.join(' > '); @@ -85,18 +124,18 @@ } function setupRestartLinks() { - $('.restart').off('click').on('click', function() { - bootbox.confirm('Are you sure you wish to restart NodeBB?', function(confirm) { + $('.restart').off('click').on('click', function () { + bootbox.confirm('Are you sure you wish to restart NodeBB?', function (confirm) { if (confirm) { - require(['admin/modules/instance'], function(instance) { + require(['admin/modules/instance'], function (instance) { instance.restart(); }); } }); }); - $('.reload').off('click').on('click', function() { - require(['admin/modules/instance'], function(instance) { + $('.reload').off('click').on('click', function () { + require(['admin/modules/instance'], function (instance) { instance.reload(); }); }); @@ -105,8 +144,8 @@ function launchSnackbar(params) { var message = (params.title ? "<strong>" + params.title + "</strong>" : '') + (params.message ? params.message : ''); - require(['translator'], function(translator) { - translator.translate(message, function(html) { + require(['translator'], function (translator) { + translator.translate(message, function (html) { var bar = $.snackbar({ content: html, timeout: 3000, @@ -128,15 +167,15 @@ 'tolerance': 70 }); - $('#mobile-menu').on('click', function() { + $('#mobile-menu').on('click', function () { slideout.toggle(); }); - $('#menu a').on('click', function() { + $('#menu a').on('click', function () { slideout.close(); }); - $(window).on('resize', function() { + $(window).on('resize', function () { slideout.close(); }); @@ -151,7 +190,7 @@ slideout.on('open', onOpeningMenu); slideout.on('translate', onOpeningMenu); - slideout.on('close', function() { + slideout.on('close', function () { $('#header').css({ 'top': '0px', 'position': 'fixed' diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js new file mode 100644 index 0000000000..29ee7e362c --- /dev/null +++ b/public/src/admin/advanced/errors.js @@ -0,0 +1,113 @@ +"use strict"; +/*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ + +define('admin/advanced/errors', ['Chart'], function (Chart) { + var Errors = {}; + + Errors.init = function () { + Errors.setupCharts(); + + $('[data-action="clear"]').on('click', Errors.clear404); + }; + + Errors.clear404 = function () { + bootbox.confirm('Are you sure you wish to clear the 404 error logs?', function (ok) { + if (ok) { + socket.emit('admin.errors.clear', {}, function (err) { + if (err) { + return app.alertError(err.message); + } + + ajaxify.refresh(); + app.alertSuccess('"404 Not Found" errors cleared'); + }); + } + }); + }; + + Errors.setupCharts = function () { + var notFoundCanvas = document.getElementById('not-found'), + tooBusyCanvas = document.getElementById('toobusy'), + dailyLabels = utils.getDaysArray(); + + dailyLabels = dailyLabels.slice(-7); + + if (utils.isMobile()) { + Chart.defaults.global.tooltips.enabled = false; + } + + var data = { + 'not-found': { + labels: dailyLabels, + datasets: [ + { + label: "", + backgroundColor: "rgba(186,139,175,0.2)", + borderColor: "rgba(186,139,175,1)", + pointBackgroundColor: "rgba(186,139,175,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(186,139,175,1)", + data: ajaxify.data.analytics['not-found'] + } + ] + }, + 'toobusy': { + labels: dailyLabels, + datasets: [ + { + label: "", + backgroundColor: "rgba(151,187,205,0.2)", + borderColor: "rgba(151,187,205,1)", + pointBackgroundColor: "rgba(151,187,205,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(151,187,205,1)", + data: ajaxify.data.analytics['toobusy'] + } + ] + } + }; + + notFoundCanvas.width = $(notFoundCanvas).parent().width(); + tooBusyCanvas.width = $(tooBusyCanvas).parent().width(); + + new Chart(notFoundCanvas.getContext('2d'), { + type: 'line', + data: data['not-found'], + options: { + responsive: true, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); + + new Chart(tooBusyCanvas.getContext('2d'), { + type: 'line', + data: data['toobusy'], + options: { + responsive: true, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } + }); + }; + + return Errors; +}); \ No newline at end of file diff --git a/public/src/admin/advanced/events.js b/public/src/admin/advanced/events.js index 9075947d60..b4f18414ea 100644 --- a/public/src/admin/advanced/events.js +++ b/public/src/admin/advanced/events.js @@ -1,14 +1,15 @@ "use strict"; -/* global define, socket, app, templates */ +/* global define, socket, app */ -define('admin/advanced/events', ['forum/infinitescroll'], function(infinitescroll) { + +define('admin/advanced/events', function () { var Events = {}; - Events.init = function() { + Events.init = function () { - $('[data-action="clear"]').on('click', function() { - socket.emit('admin.deleteAllEvents', function(err) { + $('[data-action="clear"]').on('click', function () { + socket.emit('admin.deleteAllEvents', function (err) { if (err) { return app.alertError(err.message); } @@ -16,25 +17,6 @@ define('admin/advanced/events', ['forum/infinitescroll'], function(infinitescrol }); }); - infinitescroll.init(function(direction) { - if (direction < 0 || !$('.events').length) { - return; - } - - infinitescroll.loadMore('admin.getMoreEvents', $('[data-next]').attr('data-next'), function(data, done) { - if (data.events && data.events.length) { - templates.parse('admin/advanced/events', 'events', {events: data.events}, function(html) { - $('.events-list').append(html); - done(); - }); - - $('[data-next]').attr('data-next', data.next); - } else { - done(); - } - }); - }); - }; return Events; diff --git a/public/src/admin/advanced/logs.js b/public/src/admin/advanced/logs.js index ea0503d8a6..2ea10f1b36 100644 --- a/public/src/admin/advanced/logs.js +++ b/public/src/admin/advanced/logs.js @@ -1,22 +1,22 @@ "use strict"; /* global define, socket, app */ -define('admin/advanced/logs', function() { +define('admin/advanced/logs', function () { var Logs = {}; - Logs.init = function() { + Logs.init = function () { var logsEl = $('.logs pre'); logsEl.scrollTop(logsEl.prop('scrollHeight')); // Affix menu $('.affix').affix(); - $('.logs').find('button[data-action]').on('click', function(e) { + $('.logs').find('button[data-action]').on('click', function (event) { var btnEl = $(this), action = btnEl.attr('data-action'); switch(action) { case 'reload': - socket.emit('admin.logs.get', function(err, logs) { + socket.emit('admin.logs.get', function (err, logs) { if (!err) { logsEl.text(logs); logsEl.scrollTop(logsEl.prop('scrollHeight')); @@ -27,7 +27,7 @@ define('admin/advanced/logs', function() { break; case 'clear': - socket.emit('admin.logs.clear', function(err) { + socket.emit('admin.logs.clear', function (err) { if (!err) { app.alertSuccess('Logs Cleared!'); btnEl.prev().click(); diff --git a/public/src/admin/appearance/customise.js b/public/src/admin/appearance/customise.js index a135a240fe..d8ad5e3ecb 100644 --- a/public/src/admin/appearance/customise.js +++ b/public/src/admin/appearance/customise.js @@ -1,11 +1,11 @@ "use strict"; /* global ace, define, app, socket */ -define('admin/appearance/customise', ['admin/settings'], function(Settings) { +define('admin/appearance/customise', ['admin/settings'], function (Settings) { var Customise = {}; - Customise.init = function() { - Settings.prepare(function() { + Customise.init = function () { + Settings.prepare(function () { $('#customCSS').text($('#customCSS-holder').val()); $('#customHTML').text($('#customHTML-holder').val()); @@ -15,14 +15,18 @@ define('admin/appearance/customise', ['admin/settings'], function(Settings) { customCSS.setTheme("ace/theme/twilight"); customCSS.getSession().setMode("ace/mode/css"); - customCSS.on('change', function(e) { + customCSS.on('change', function (event) { + app.flags = app.flags || {}; + app.flags._unsaved = true; $('#customCSS-holder').val(customCSS.getValue()); }); customHTML.setTheme("ace/theme/twilight"); customHTML.getSession().setMode("ace/mode/html"); - customHTML.on('change', function(e) { + customHTML.on('change', function (event) { + app.flags = app.flags || {}; + app.flags._unsaved = true; $('#customHTML-holder').val(customHTML.getValue()); }); }); diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js index 4d0d8ffc04..9dc02a1efc 100644 --- a/public/src/admin/appearance/skins.js +++ b/public/src/admin/appearance/skins.js @@ -1,17 +1,17 @@ "use strict"; /* global define, app, socket, templates */ -define('admin/appearance/skins', function() { +define('admin/appearance/skins', function () { var Skins = {}; - Skins.init = function() { + Skins.init = function () { // Populate skins from Bootswatch API $.ajax({ method: 'get', url: 'https://bootswatch.com/api/3.json' }).done(Skins.render); - $('#skins').on('click', function(e){ + $('#skins').on('click', function (e){ var target = $(e.target); if (!target.attr('data-action')) { @@ -31,7 +31,7 @@ define('admin/appearance/skins', function() { type: themeType, id: themeId, src: cssSrc - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -49,11 +49,11 @@ define('admin/appearance/skins', function() { }); }; - Skins.render = function(bootswatch) { + Skins.render = function (bootswatch) { var themeContainer = $('#bootstrap_themes'); templates.parse('admin/partials/theme_list', { - themes: bootswatch.themes.map(function(theme) { + themes: bootswatch.themes.map(function (theme) { return { type: 'bootswatch', id: theme.name, @@ -66,13 +66,13 @@ define('admin/appearance/skins', function() { }; }), showRevert: true - }, function(html) { + }, function (html) { themeContainer.html(html); if (config['theme:src']) { var skin = config['theme:src'] .match(/latest\/(\S+)\/bootstrap.min.css/)[1] - .replace(/(^|\s)([a-z])/g , function(m,p1,p2){return p1+p2.toUpperCase();}); + .replace(/(^|\s)([a-z])/g , function (m,p1,p2){return p1 + p2.toUpperCase();}); highlightSelectedTheme(skin); } @@ -82,7 +82,7 @@ define('admin/appearance/skins', function() { function highlightSelectedTheme(themeId) { $('[data-theme]') .removeClass('selected') - .find('[data-action="use"]').each(function() { + .find('[data-action="use"]').each(function () { if ($(this).parents('[data-theme]').attr('data-theme')) { $(this) .html('Select Skin') diff --git a/public/src/admin/appearance/themes.js b/public/src/admin/appearance/themes.js index 901f64b688..41adcc1589 100644 --- a/public/src/admin/appearance/themes.js +++ b/public/src/admin/appearance/themes.js @@ -1,11 +1,11 @@ "use strict"; /* global define, app, socket, bootbox, templates, config */ -define('admin/appearance/themes', function() { +define('admin/appearance/themes', function () { var Themes = {}; - Themes.init = function() { - $('#installed_themes').on('click', function(e){ + Themes.init = function () { + $('#installed_themes').on('click', function (e){ var target = $(e.target), action = target.attr('data-action'); @@ -19,7 +19,7 @@ define('admin/appearance/themes', function() { type: themeType, id: themeId, src: cssSrc - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -31,7 +31,7 @@ define('admin/appearance/themes', function() { title: 'Theme Changed', message: 'Please restart your NodeBB to fully activate this theme', timeout: 5000, - clickfn: function() { + clickfn: function () { socket.emit('admin.restart'); } }); @@ -39,13 +39,13 @@ define('admin/appearance/themes', function() { } }); - $('#revert_theme').on('click', function() { - bootbox.confirm('Are you sure you wish to restore the default NodeBB theme?', function(confirm) { + $('#revert_theme').on('click', function () { + bootbox.confirm('Are you sure you wish to restore the default NodeBB theme?', function (confirm) { if (confirm) { socket.emit('admin.themes.set', { type: 'local', id: 'nodebb-theme-persona' - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -62,7 +62,7 @@ define('admin/appearance/themes', function() { }); }); - socket.emit('admin.themes.getInstalled', function(err, themes) { + socket.emit('admin.themes.getInstalled', function (err, themes) { if(err) { return app.alertError(err.message); } @@ -75,10 +75,12 @@ define('admin/appearance/themes', function() { } else { templates.parse('admin/partials/theme_list', { themes: themes - }, function(html) { - translator.translate(html, function(html) { - instListEl.html(html); - highlightSelectedTheme(config['theme:id']); + }, function (html) { + require(['translator'], function (translator) { + translator.translate(html, function (html) { + instListEl.html(html); + highlightSelectedTheme(config['theme:id']); + }); }); }); } diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index 9a130723bf..bb89256959 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -1,9 +1,9 @@ "use strict"; /* global define, app, socket, bootbox */ -define('admin/extend/plugins', function() { +define('admin/extend/plugins', function () { var Plugins = {}; - Plugins.init = function() { + Plugins.init = function () { var pluginsList = $('.plugins'), numPlugins = pluginsList[0].querySelectorAll('li').length, pluginID; @@ -15,29 +15,38 @@ define('admin/extend/plugins', function() { $('#plugin-search').val(''); - pluginsList.on('click', 'button[data-action="toggleActive"]', function() { - pluginID = $(this).parents('li').attr('data-plugin-id'); - var btn = $(this); - socket.emit('admin.plugins.toggleActive', pluginID, function(err, status) { + pluginsList.on('click', 'button[data-action="toggleActive"]', function () { + var pluginEl = $(this).parents('li'); + pluginID = pluginEl.attr('data-plugin-id'); + var btn = $('#' + pluginID + ' [data-action="toggleActive"]'); + socket.emit('admin.plugins.toggleActive', pluginID, function (err, status) { + if (err) { + return app.alertError(err); + } btn.html('<i class="fa fa-power-off"></i> ' + (status.active ? 'Deactivate' : 'Activate')); btn.toggleClass('btn-warning', status.active).toggleClass('btn-success', !status.active); + //clone it to active plugins tab + if (status.active && !$('#active #' + pluginID).length) { + $('#active ul').prepend(pluginEl.clone(true)); + } + app.alert({ alert_id: 'plugin_toggled', title: 'Plugin ' + (status.active ? 'Enabled' : 'Disabled'), - message: status.active ? 'Please reload your NodeBB to fully activate this plugin' : 'Plugin successfully deactivated', + message: status.active ? 'Please restart your NodeBB to fully activate this plugin' : 'Plugin successfully deactivated', type: status.active ? 'warning' : 'success', timeout: 5000, - clickfn: function() { - require(['admin/modules/instance'], function(instance) { - instance.reload(); + clickfn: function () { + require(['admin/modules/instance'], function (instance) { + instance.restart(); }); } }); }); }); - pluginsList.on('click', 'button[data-action="toggleInstall"]', function() { + pluginsList.on('click', 'button[data-action="toggleInstall"]', function () { var btn = $(this); btn.attr('disabled', true); pluginID = $(this).parents('li').attr('data-plugin-id'); @@ -46,9 +55,9 @@ define('admin/extend/plugins', function() { return Plugins.toggleInstall(pluginID, $(this).parents('li').attr('data-version')); } - Plugins.suggest(pluginID, function(err, payload) { + Plugins.suggest(pluginID, function (err, payload) { if (err) { - bootbox.confirm('<p>NodeBB could not reach the package manager, proceed with installation of latest version?</p><div class="alert alert-danger"><strong>Server returned (' + err.status + ')</strong>: ' + err.responseText + '</div>', function(confirm) { + bootbox.confirm('<p>NodeBB could not reach the package manager, proceed with installation of latest version?</p><div class="alert alert-danger"><strong>Server returned (' + err.status + ')</strong>: ' + err.responseText + '</div>', function (confirm) { if (confirm) { Plugins.toggleInstall(pluginID, 'latest'); } else { @@ -58,11 +67,11 @@ define('admin/extend/plugins', function() { return; } - require(['semver'], function(semver) { + require(['semver'], function (semver) { if (payload.version !== 'latest') { Plugins.toggleInstall(pluginID, payload.version); } else if (payload.version === 'latest') { - confirmInstall(pluginID, function(confirm) { + confirmInstall(pluginID, function (confirm) { if (confirm) { Plugins.toggleInstall(pluginID, 'latest'); } else { @@ -76,21 +85,21 @@ define('admin/extend/plugins', function() { }); }); - pluginsList.on('click', 'button[data-action="upgrade"]', function() { + pluginsList.on('click', 'button[data-action="upgrade"]', function () { var btn = $(this); var parent = btn.parents('li'); pluginID = parent.attr('data-plugin-id'); - Plugins.suggest(pluginID, function(err, payload) { + Plugins.suggest(pluginID, function (err, payload) { if (err) { return bootbox.alert('<p>NodeBB could not reach the package manager, an upgrade is not suggested at this time.</p>'); } - require(['semver'], function(semver) { + require(['semver'], function (semver) { if (payload.version !== 'latest' && semver.gt(payload.version, parent.find('.currentVersion').text())) { upgrade(pluginID, btn, payload.version); } else if (payload.version === 'latest') { - confirmInstall(pluginID, function() { + confirmInstall(pluginID, function () { upgrade(pluginID, btn, payload.version); }); } else { @@ -100,22 +109,22 @@ define('admin/extend/plugins', function() { }); }); - $('#plugin-search').on('input propertychange', function() { + $('#plugin-search').on('input propertychange', function () { var term = $(this).val(); - $('.plugins li').each(function() { + $('.plugins li').each(function () { var pluginId = $(this).attr('data-plugin-id'); $(this).toggleClass('hide', pluginId && pluginId.indexOf(term) === -1); }); }); - $('#plugin-order').on('click', function() { + $('#plugin-order').on('click', function () { $('#order-active-plugins-modal').modal('show'); - socket.emit('admin.plugins.getActive', function(err, activePlugins) { + socket.emit('admin.plugins.getActive', function (err, activePlugins) { if (err) { return app.alertError(err); } var html = ''; - activePlugins.forEach(function(plugin) { + activePlugins.forEach(function (plugin) { html += '<li class="">' + plugin + '</li>'; }); if (!activePlugins.length) { @@ -125,14 +134,14 @@ define('admin/extend/plugins', function() { }); }); - $('#save-plugin-order').on('click', function() { + $('#save-plugin-order').on('click', function () { var plugins = $('#order-active-plugins-modal .plugin-list').children(); var data = []; - plugins.each(function(index, el) { + plugins.each(function (index, el) { data.push({name: $(el).text(), order: index}); }); - socket.emit('admin.plugins.orderActivePlugins', data, function(err) { + socket.emit('admin.plugins.orderActivePlugins', data, function (err) { if (err) { return app.alertError(err.message); } @@ -141,6 +150,7 @@ define('admin/extend/plugins', function() { }); populateUpgradeablePlugins(); + populateActivePlugins(); }; function confirmInstall(pluginID, callback) { @@ -148,7 +158,7 @@ define('admin/extend/plugins', function() { '<div class="alert alert-warning"><p><strong>No Compatibility Infomation Found</strong></p><p>This plugin did not specify a specific version for installation given your NodeBB version. Full compatibility cannot be guaranteed, and may cause your NodeBB to no longer start properly.</p></div>' + '<p>In the event that NodeBB cannot boot properly:</p>' + '<pre><code>$ ./nodebb reset plugin="' + pluginID + '"</code></pre>' + - '<p>Continue installation of latest version of this plugin?</p>', function(confirm) { + '<p>Continue installation of latest version of this plugin?</p>', function (confirm) { callback(confirm); }); } @@ -158,7 +168,7 @@ define('admin/extend/plugins', function() { socket.emit('admin.plugins.upgrade', { id: pluginID, version: version - }, function(err, isActive) { + }, function (err, isActive) { if (err) { return app.alertError(err.message); } @@ -173,8 +183,8 @@ define('admin/extend/plugins', function() { message: 'Please reload your NodeBB to fully upgrade this plugin', type: 'warning', timeout: 5000, - clickfn: function() { - require(['admin/modules/instance'], function(instance) { + clickfn: function () { + require(['admin/modules/instance'], function (instance) { instance.reload(); }); } @@ -183,7 +193,7 @@ define('admin/extend/plugins', function() { }); } - Plugins.toggleInstall = function(pluginID, version, callback) { + Plugins.toggleInstall = function (pluginID, version, callback) { var btn = $('li[data-plugin-id="' + pluginID + '"] button[data-action="toggleInstall"]'); var activateBtn = btn.siblings('[data-action="toggleActive"]'); btn.find('i').attr('class', 'fa fa-refresh fa-spin'); @@ -191,7 +201,7 @@ define('admin/extend/plugins', function() { socket.emit('admin.plugins.toggleInstall', { id: pluginID, version: version - }, function(err, pluginData) { + }, function (err, pluginData) { if (err) { btn.removeAttr('disabled'); return app.alertError(err.message); @@ -213,7 +223,7 @@ define('admin/extend/plugins', function() { }); }; - Plugins.suggest = function(pluginId, callback) { + Plugins.suggest = function (pluginId, callback) { var nbbVersion = app.config.version.match(/^\d\.\d\.\d/); $.ajax((app.config.registry || 'https://packages.nodebb.org') + '/api/v1/suggest', { type: 'GET', @@ -222,18 +232,28 @@ define('admin/extend/plugins', function() { version: nbbVersion[0] }, dataType: 'json' - }).done(function(payload) { + }).done(function (payload) { callback(undefined, payload); }).fail(callback); }; function populateUpgradeablePlugins() { - $('#installed ul li').each(function() { + $('#installed ul li').each(function () { if ($(this).children('[data-action="upgrade"]').length) { $('#upgrade ul').append($(this).clone(true)); } }); } + function populateActivePlugins() { + $('#installed ul li').each(function () { + if ($(this).hasClass('active')) { + $('#active ul').append($(this).clone(true)); + } else { + $('#deactive ul').append($(this).clone(true)); + } + }); + } + return Plugins; }); diff --git a/public/src/admin/extend/rewards.js b/public/src/admin/extend/rewards.js index 255b53dcbf..939bdbb20d 100644 --- a/public/src/admin/extend/rewards.js +++ b/public/src/admin/extend/rewards.js @@ -1,7 +1,7 @@ "use strict"; /* global define, app, ajaxify, socket, templates, bootbox */ -define('admin/extend/rewards', function() { +define('admin/extend/rewards', function () { var rewards = {}; @@ -10,25 +10,25 @@ define('admin/extend/rewards', function() { conditions, conditionals; - rewards.init = function() { + rewards.init = function () { available = ajaxify.data.rewards; active = ajaxify.data.active; conditions = ajaxify.data.conditions; conditionals = ajaxify.data.conditionals; - $('[data-selected]').each(function() { + $('[data-selected]').each(function () { select($(this)); }); $('#active') - .on('change', '[data-selected]', function() { + .on('change', '[data-selected]', function () { update($(this)); }) - .on('click', '.delete', function() { + .on('click', '.delete', function () { var parent = $(this).parents('[data-id]'), id = parent.attr('data-id'); - socket.emit('admin.rewards.delete', {id: id}, function(err) { + socket.emit('admin.rewards.delete', {id: id}, function (err) { if (err) { app.alertError(err.message); } else { @@ -39,7 +39,7 @@ define('admin/extend/rewards', function() { parent.remove(); return false; }) - .on('click', '.toggle', function() { + .on('click', '.toggle', function () { var btn = $(this), disabled = btn.hasClass('btn-success'), id = $(this).parents('[data-id]').attr('data-id'); @@ -93,17 +93,17 @@ define('admin/extend/rewards', function() { return app.alertError('Illegal reward - no inputs found! ' + el.attr('data-selected')); } - inputs.forEach(function(input) { + inputs.forEach(function (input) { html += '<label for="' + input.name + '">' + input.label + '<br />'; switch (input.type) { case 'select': html += '<select name="' + input.name + '">'; - input.values.forEach(function(value) { + input.values.forEach(function (value) { html += '<option value="' + value.value + '">' + value.name + '</option>'; }); break; case 'text': - html += '<input type="text" name="' + input.name +'" />'; + html += '<input type="text" name="' + input.name + '" />'; break; } html += '</label><br />'; @@ -113,7 +113,7 @@ define('admin/extend/rewards', function() { } function populateInputs() { - $('[data-rid]').each(function(i) { + $('[data-rid]').each(function (i) { var div = $(this).find('.inputs'), rewards = active[i].rewards; @@ -141,7 +141,7 @@ define('admin/extend/rewards', function() { rewards: available, }; - templates.parse('admin/extend/rewards', 'active', data, function(li) { + templates.parse('admin/extend/rewards', 'active', data, function (li) { li = $(li); ul.append(li); li.find('select').val(''); @@ -151,16 +151,16 @@ define('admin/extend/rewards', function() { function saveRewards() { var activeRewards = []; - $('#active li').each(function() { + $('#active li').each(function () { var data = {rewards: {}}, main = $(this).find('form.main').serializeArray(), rewards = $(this).find('form.rewards').serializeArray(); - main.forEach(function(obj) { + main.forEach(function (obj) { data[obj.name] = obj.value; }); - rewards.forEach(function(obj) { + rewards.forEach(function (obj) { data.rewards[obj.name] = obj.value; }); @@ -170,7 +170,7 @@ define('admin/extend/rewards', function() { activeRewards.push(data); }); - socket.emit('admin.rewards.save', activeRewards, function(err) { + socket.emit('admin.rewards.save', activeRewards, function (err) { if (err) { app.alertError(err.message); } else { diff --git a/public/src/admin/extend/widgets.js b/public/src/admin/extend/widgets.js index 4ac3fdeaf0..761a57f423 100644 --- a/public/src/admin/extend/widgets.js +++ b/public/src/admin/extend/widgets.js @@ -1,11 +1,11 @@ "use strict"; /* global define, app, socket */ -define('admin/extend/widgets', function() { +define('admin/extend/widgets', ['jqueryui'], function (jqueryui) { var Widgets = {}; - Widgets.init = function() { - $('#widgets .nav-pills a').on('click', function(ev) { + Widgets.init = function () { + $('#widgets .nav-pills a').on('click', function (ev) { var $this = $(this); $('#widgets .nav-pills li').removeClass('active'); $this.parent().addClass('active'); @@ -17,11 +17,11 @@ define('admin/extend/widgets', function() { return false; }); - $('#widget-selector').on('change', function() { + $('#widget-selector').on('change', function () { $('.available-widgets [data-widget]').addClass('hide'); $('.available-widgets [data-widget="' + $(this).val() + '"]').removeClass('hide'); }); - + $('#widget-selector').trigger('change'); loadWidgetData(); @@ -31,8 +31,8 @@ define('admin/extend/widgets', function() { $('[data-location="drafts"]').insertAfter($('[data-location="drafts"]').closest('.tab-content')); $('#widgets .available-widgets .widget-panel').draggable({ - helper: function(e) { - return $(e.target).parents('.widget-panel').clone().addClass('block').width($(e.target.parentNode).width()); + helper: function (e) { + return $(e.target).parents('.widget-panel').clone(); }, distance: 10, connectToSortable: ".widget-area" @@ -40,7 +40,7 @@ define('admin/extend/widgets', function() { $('#widgets .available-containers .containers > [data-container-html]') .draggable({ - helper: function(e) { + helper: function (e) { var target = $(e.target); target = target.attr('data-container-html') ? target : target.parents('[data-container-html]'); @@ -48,7 +48,7 @@ define('admin/extend/widgets', function() { }, distance: 10 }) - .each(function() { + .each(function () { $(this).attr('data-container-html', $(this).attr('data-container-html').replace(/\\\{([\s\S]*?)\\\}/g, '{$1}')); }); @@ -57,15 +57,15 @@ define('admin/extend/widgets', function() { appendToggle(ui.item); }, connectWith: "div" - }).on('click', '.delete-widget', function() { + }).on('click', '.delete-widget', function () { var panel = $(this).parents('.widget-panel'); - bootbox.confirm('Are you sure you wish to delete this widget?', function(confirm) { + bootbox.confirm('Are you sure you wish to delete this widget?', function (confirm) { if (confirm) { panel.remove(); } }); - }).on('mouseup', '> .panel > .panel-heading', function(evt) { + }).on('mouseup', '> .panel > .panel-heading', function (evt) { if ( !( $(this).parent().is('.ui-sortable-helper') || $(evt.target).closest('.delete-widget').length ) ) { $(this).parent().children('.panel-body').toggleClass('hidden'); } @@ -76,7 +76,7 @@ define('admin/extend/widgets', function() { function saveWidgets() { var total = $('#widgets [data-template][data-location]').length; - $('#widgets [data-template][data-location]').each(function(i, el) { + $('#widgets [data-template][data-location]').each(function (i, el) { el = $(el); var template = el.attr('data-template'), @@ -84,7 +84,7 @@ define('admin/extend/widgets', function() { area = el.children('.widget-area'), widgets = []; - area.find('.widget-panel[data-widget]').each(function() { + area.find('.widget-panel[data-widget]').each(function () { var widgetData = {}, data = $(this).find('form').serializeArray(); @@ -113,7 +113,7 @@ define('admin/extend/widgets', function() { template: template, location: location, widgets: widgets - }, function(err) { + }, function (err) { total--; if (err) { @@ -134,13 +134,13 @@ define('admin/extend/widgets', function() { }); } - $('.color-selector').on('click', '.btn', function() { + $('.color-selector').on('click', '.btn', function () { var btn = $(this), selector = btn.parents('.color-selector'), container = selector.parents('[data-container-html]'), classList = []; - selector.children().each(function() { + selector.children().each(function () { classList.push($(this).attr('data-class')); }); @@ -156,10 +156,10 @@ define('admin/extend/widgets', function() { function appendToggle(el) { if (!el.hasClass('block')) { - el.addClass('block') + el.addClass('block').css('width', '').css('height', '') .droppable({ accept: '[data-container-html]', - drop: function(event, ui) { + drop: function (event, ui) { var el = $(this); el.find('.panel-body .container-html').val(ui.draggable.attr('data-container-html')); @@ -180,7 +180,7 @@ define('admin/extend/widgets', function() { title.text(title.text() + ' - ' + data.title); } - widget.find('input, textarea, select').each(function() { + widget.find('input, textarea, select').each(function () { var input = $(this), value = data[input.attr('name')]; @@ -194,16 +194,16 @@ define('admin/extend/widgets', function() { return widget; } - $.get(RELATIVE_PATH + '/api/admin/extend/widgets', function(data) { + $.get(RELATIVE_PATH + '/api/admin/extend/widgets', function (data) { var areas = data.areas; - for(var i=0; i<areas.length; ++i) { + for(var i = 0; i < areas.length; ++i) { var area = areas[i], widgetArea = $('#widgets .area[data-template="' + area.template + '"][data-location="' + area.location + '"]').find('.widget-area'); widgetArea.html(''); - for (var k=0; k<area.data.length; ++k) { + for (var k = 0; k < area.data.length; ++k) { var widgetData = area.data[k], widgetEl = $('.available-widgets [data-widget="' + widgetData.widget + '"]').clone(true).removeClass('hide'); diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index 8595ac3b81..d6f02e6e3b 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -1,19 +1,19 @@ "use strict"; -/*global define, ajaxify, app, socket, utils, bootbox, Chart, RELATIVE_PATH*/ +/*global define, ajaxify, app, socket, utils, bootbox, RELATIVE_PATH*/ -define('admin/general/dashboard', ['semver'], function(semver) { - var Admin = {}, - intervals = { +define('admin/general/dashboard', ['semver', 'Chart'], function (semver, Chart) { + var Admin = {}; + var intervals = { rooms: false, graphs: false - }, - isMobile = false, - isPrerelease = /^v?\d+\.\d+\.\d+-.+$/, - graphData = { + }; + var isMobile = false; + var isPrerelease = /^v?\d+\.\d+\.\d+-.+$/; + var graphData = { rooms: {}, traffic: {} - }, - currentGraph = { + }; + var currentGraph = { units: 'hours', until: undefined }; @@ -24,7 +24,7 @@ define('admin/general/dashboard', ['semver'], function(semver) { realtimeInterval: 1500 }; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { clearInterval(intervals.rooms); clearInterval(intervals.graphs); @@ -35,19 +35,19 @@ define('admin/general/dashboard', ['semver'], function(semver) { usedTopicColors.length = 0; }); - Admin.init = function() { + Admin.init = function () { app.enterRoom('admin'); socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - $.get('https://api.github.com/repos/NodeBB/NodeBB/tags', function(releases) { + $.get('https://api.github.com/repos/NodeBB/NodeBB/tags', function (releases) { // Re-sort the releases, as they do not follow Semver (wrt pre-releases) - releases = releases.sort(function(a, b) { + releases = releases.sort(function (a, b) { a = a.name.replace(/^v/, ''); b = b.name.replace(/^v/, ''); return semver.lt(a, b) ? 1 : -1; - }).filter(function(version) { + }).filter(function (version) { return !isPrerelease.test(version.name); // filter out automated prerelease versions }); @@ -79,7 +79,7 @@ define('admin/general/dashboard', ['semver'], function(semver) { initiateDashboard(); }; - Admin.updateRoomUsage = function(err, data) { + Admin.updateRoomUsage = function (err, data) { if (err) { return app.alertError(err.message); } @@ -91,19 +91,19 @@ define('admin/general/dashboard', ['semver'], function(semver) { graphData.rooms = data; var html = '<div class="text-center pull-left">' + - '<div>'+ data.onlineRegisteredCount +'</div>' + + '<div>' + data.onlineRegisteredCount + '</div>' + '<div>Users</div>' + '</div>' + '<div class="text-center pull-left">' + - '<div>'+ data.onlineGuestCount +'</div>' + + '<div>' + data.onlineGuestCount + '</div>' + '<div>Guests</div>' + '</div>' + '<div class="text-center pull-left">' + - '<div>'+ (data.onlineRegisteredCount + data.onlineGuestCount) +'</div>' + + '<div>' + (data.onlineRegisteredCount + data.onlineGuestCount) + '</div>' + '<div>Total</div>' + '</div>' + '<div class="text-center pull-left">' + - '<div>'+ data.socketCount +'</div>' + + '<div>' + data.socketCount + '</div>' + '<div>Connections</div>' + '</div>'; @@ -121,8 +121,8 @@ define('admin/general/dashboard', ['semver'], function(semver) { topics: null }; - var topicColors = ["#bf616a","#5B90BF","#d08770","#ebcb8b","#a3be8c","#96b5b4","#8fa1b3","#b48ead","#ab7967","#46BFBD"], - usedTopicColors = []; + var topicColors = ["#bf616a","#5B90BF","#d08770","#ebcb8b","#a3be8c","#96b5b4","#8fa1b3","#b48ead","#ab7967","#46BFBD"]; + var usedTopicColors = []; // from chartjs.org function lighten(col, amt) { @@ -150,7 +150,7 @@ define('admin/general/dashboard', ['semver'], function(semver) { if (g > 255) g = 255; else if (g < 0) g = 0; - return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16); + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); } function setupGraphs() { @@ -165,109 +165,119 @@ define('admin/general/dashboard', ['semver'], function(semver) { trafficLabels = utils.getHoursArray(); if (isMobile) { - Chart.defaults.global.showTooltips = false; - Chart.defaults.global.animation = false; + Chart.defaults.global.tooltips.enabled = false; } var data = { - labels: trafficLabels, - datasets: [ - { - label: "Page Views", - fillColor: "rgba(220,220,220,0.2)", - strokeColor: "rgba(220,220,220,1)", - pointColor: "rgba(220,220,220,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(220,220,220,1)", - data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - }, - { - label: "Unique Visitors", - fillColor: "rgba(151,187,205,0.2)", - strokeColor: "rgba(151,187,205,1)", - pointColor: "rgba(151,187,205,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(151,187,205,1)", - data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - } - ] - }; + labels: trafficLabels, + datasets: [ + { + label: "Page Views", + backgroundColor: "rgba(220,220,220,0.2)", + borderColor: "rgba(220,220,220,1)", + pointBackgroundColor: "rgba(220,220,220,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(220,220,220,1)", + data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + }, + { + label: "Unique Visitors", + backgroundColor: "rgba(151,187,205,0.2)", + borderColor: "rgba(151,187,205,1)", + pointBackgroundColor: "rgba(151,187,205,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(151,187,205,1)", + data: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + } + ] + }; trafficCanvas.width = $(trafficCanvas).parent().width(); - graphs.traffic = new Chart(trafficCtx).Line(data, { - responsive: true + graphs.traffic = new Chart(trafficCtx, { + type: 'line', + data: data, + options: { + responsive: true, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } }); - - graphs.registered = new Chart(registeredCtx).Doughnut([{ - value: 1, - color:"#F7464A", - highlight: "#FF5A5E", - label: "Registered Users" - }, - { - value: 1, - color: "#46BFBD", - highlight: "#5AD3D1", - label: "Anonymous Users" - }], { - responsive: true - }); - - graphs.presence = new Chart(presenceCtx).Doughnut([{ - value: 1, - color:"#F7464A", - highlight: "#FF5A5E", - label: "On categories list" - }, - { - value: 1, - color: "#46BFBD", - highlight: "#5AD3D1", - label: "Reading posts" - }, - { - value: 1, - color: "#FDB45C", - highlight: "#FFC870", - label: "Browsing topics" - }, - { - value: 1, - color: "#949FB1", - highlight: "#A8B3C5", - label: "Recent" + + graphs.registered = new Chart(registeredCtx, { + type: 'doughnut', + data: { + labels: ["Registered Users", "Anonymous Users"], + datasets: [{ + data: [1, 1], + backgroundColor: ["#F7464A", "#46BFBD"], + hoverBackgroundColor: ["#FF5A5E", "#5AD3D1"] + }] }, - { - value: 1, - color: "#9FB194", - highlight: "#A8B3C5", - label: "Unread" + options: { + responsive: true, + legend: { + display: false + } } - ], { - responsive: true - }); + }); - graphs.topics = new Chart(topicsCtx).Doughnut([], {responsive: true}); - topicsCanvas.onclick = function(evt){ - var obj = graphs.topics.getSegmentsAtEvent(evt); - if (obj && obj[0]) { - window.open(RELATIVE_PATH + '/topic/' + obj[0].tid); - } - }; + graphs.presence = new Chart(presenceCtx, { + type: 'doughnut', + data: { + labels: ["On categories list", "Reading posts", "Browsing topics", "Recent", "Unread"], + datasets: [{ + data: [1, 1, 1, 1, 1], + backgroundColor: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#9FB194"], + hoverBackgroundColor: ["#FF5A5E", "#5AD3D1", "#FFC870", "#A8B3C5", "#A8B3C5"] + }] + }, + options: { + responsive: true, + legend: { + display: false + } + } + }); + + graphs.topics = new Chart(topicsCtx, { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + backgroundColor: [], + hoverBackgroundColor: [] + }] + }, + options: { + responsive: true, + legend: { + display: false + } + } + }); updateTrafficGraph(); $(window).on('resize', adjustPieCharts); adjustPieCharts(); - $('[data-action="updateGraph"]').on('click', function() { + $('[data-action="updateGraph"]').on('click', function () { var until = undefined; switch($(this).attr('data-until')) { case 'last-month': var lastMonth = new Date(); - lastMonth.setDate(lastMonth.getDate()-30); + lastMonth.setDate(lastMonth.getDate() - 30); until = lastMonth.getTime(); } updateTrafficGraph($(this).attr('data-units'), until); @@ -275,7 +285,7 @@ define('admin/general/dashboard', ['semver'], function(semver) { } function adjustPieCharts() { - $('.pie-chart.legend-up').each(function() { + $('.pie-chart.legend-up').each(function () { var $this = $(this); if ($this.width() < 320) { @@ -296,21 +306,19 @@ define('admin/general/dashboard', ['semver'], function(semver) { units: units || 'hours', until: until }, function (err, data) { + if (err) { + return app.alertError(err.message); + } if (JSON.stringify(graphData.traffic) === JSON.stringify(data)) { return; } graphData.traffic = data; - // If new data set contains fewer points than currently shown, truncate - while(graphs.traffic.datasets[0].points.length > data.pageviews.length) { - graphs.traffic.removeData(); - } - if (units === 'days') { - graphs.traffic.scale.xLabels = utils.getDaysArray(until); + graphs.traffic.data.xLabels = utils.getDaysArray(until); } else { - graphs.traffic.scale.xLabels = utils.getHoursArray(); + graphs.traffic.data.xLabels = utils.getHoursArray(); $('#pageViewsThisMonth').html(data.monthlyPageViews.thisMonth); $('#pageViewsLastMonth').html(data.monthlyPageViews.lastMonth); @@ -320,17 +328,9 @@ define('admin/general/dashboard', ['semver'], function(semver) { utils.addCommasToNumbers($('#pageViewsPastDay')); } - for (var i = 0, ii = data.pageviews.length; i < ii; i++) { - if (graphs.traffic.datasets[0].points[i]) { - graphs.traffic.datasets[0].points[i].value = data.pageviews[i]; - graphs.traffic.datasets[0].points[i].label = graphs.traffic.scale.xLabels[i]; - graphs.traffic.datasets[1].points[i].value = data.uniqueVisitors[i]; - graphs.traffic.datasets[1].points[i].label = graphs.traffic.scale.xLabels[i]; - } else { - // No points to replace? Add data. - graphs.traffic.addData([data.pageviews[i], data.uniqueVisitors[i]], graphs.traffic.scale.xLabels[i]); - } - } + graphs.traffic.data.datasets[0].data = data.pageviews; + graphs.traffic.data.datasets[1].data = data.uniqueVisitors; + graphs.traffic.data.labels = graphs.traffic.data.xLabels; graphs.traffic.update(); currentGraph.units = units; @@ -339,22 +339,21 @@ define('admin/general/dashboard', ['semver'], function(semver) { } function updateRegisteredGraph(registered, anonymous) { - graphs.registered.segments[0].value = registered; - graphs.registered.segments[1].value = anonymous; + graphs.registered.data.datasets[0].data[0] = registered; + graphs.registered.data.datasets[0].data[1] = anonymous; graphs.registered.update(); } function updatePresenceGraph(users) { - graphs.presence.segments[0].value = users.categories; - graphs.presence.segments[1].value = users.topics; - graphs.presence.segments[2].value = users.category; - graphs.presence.segments[3].value = users.recent; - graphs.presence.segments[4].value = users.unread; - + graphs.presence.data.datasets[0].data[0] = users.categories; + graphs.presence.data.datasets[0].data[1] = users.topics; + graphs.presence.data.datasets[0].data[2] = users.category; + graphs.presence.data.datasets[0].data[3] = users.recent; + graphs.presence.data.datasets[0].data[4] = users.unread; graphs.presence.update(); } - + function updateTopicsGraph(topics) { if (!Object.keys(topics).length) { topics = {"0": { @@ -363,93 +362,41 @@ define('admin/general/dashboard', ['semver'], function(semver) { }}; } - var tids = Object.keys(topics), - segments = graphs.topics.segments; - - function reassignExistingTopics() { - for (var i = segments.length - 1; i >= 0; i--) { - if (!segments[i]) { - continue; - } - - var tid = segments[i].tid; - - if ($.inArray(tid, tids) === -1) { - usedTopicColors.splice($.inArray(segments[i].fillColor, usedTopicColors), 1); - graphs.topics.removeData(i); - } else { - graphs.topics.segments[i].value = topics[tid].value; - delete topics[tid]; - } - } - } - - function assignNewTopics() { - while (segments.length < 10 && tids.length > 0) { - var tid = tids.pop(), - data = topics[tid], - color = null; - - if (!data) { - continue; - } - - if (tid === '0') { - color = '#4D5360'; - } else { - do { - for (var i = 0, ii = topicColors.length; i < ii; i++) { - var chosenColor = topicColors[i]; - - if ($.inArray(chosenColor, usedTopicColors) === -1) { - color = chosenColor; - usedTopicColors.push(color); - break; - } - } - } while (color === null && usedTopicColors.length < topicColors.length); - } - - if (color) { - graphs.topics.addData({ - value: data.value, - color: color, - highlight: lighten(color, 10), - label: data.title - }); - - segments[segments.length - 1].tid = tid; - } - } + var tids = Object.keys(topics); + + graphs.topics.data.labels = []; + graphs.topics.data.datasets[0].data = []; + graphs.topics.data.datasets[0].backgroundColor = []; + graphs.topics.data.datasets[0].hoverBackgroundColor = []; + + for (var i = 0, ii = tids.length; i < ii; i++) { + graphs.topics.data.labels.push(topics[tids[i]].title); + graphs.topics.data.datasets[0].data.push(topics[tids[i]].value); + graphs.topics.data.datasets[0].backgroundColor.push(topicColors[i]); + graphs.topics.data.datasets[0].hoverBackgroundColor.push(lighten(topicColors[i], 10)); } - + function buildTopicsLegend() { var legend = $('#topics-legend').html(''); - segments.sort(function(a, b) { - return b.value - a.value; - }); - for (var i = 0, ii = segments.length; i < ii; i++) { - var topic = segments[i], - label = topic.tid === '0' ? topic.label : '<a title="' + topic.label + '"href="' + RELATIVE_PATH + '/topic/' + topic.tid + '" target="_blank"> ' + topic.label + '</a>'; - + for (var i = 0, ii = tids.length; i < ii; i++) { + var topic = topics[tids[i]]; + var label = topic.value === '0' ? topic.title : '<a title="' + topic.title + '"href="' + RELATIVE_PATH + '/topic/' + tids[i] + '" target="_blank"> ' + topic.title + '</a>'; + legend.append( '<li>' + - '<div style="background-color: ' + topic.highlightColor + '; border-color: ' + topic.strokeColor + '"></div>' + - '<span>' + label + '</span>' + + '<div style="background-color: ' + topicColors[i] + ';"></div>' + + '<span>' + label + '</span>' + '</li>'); } } - reassignExistingTopics(); - assignNewTopics(); buildTopicsLegend(); - graphs.topics.update(); } function setupRealtimeButton() { - $('#toggle-realtime .fa').on('click', function() { + $('#toggle-realtime .fa').on('click', function () { var $this = $(this); if ($this.hasClass('fa-toggle-on')) { $this.removeClass('fa-toggle-on').addClass('fa-toggle-off'); @@ -467,13 +414,13 @@ define('admin/general/dashboard', ['semver'], function(semver) { clearInterval(intervals.rooms); clearInterval(intervals.graphs); - intervals.rooms = setInterval(function() { + intervals.rooms = setInterval(function () { if (app.isFocused && app.isConnected) { socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); } }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval); - intervals.graphs = setInterval(function() { + intervals.graphs = setInterval(function () { updateTrafficGraph(currentGraph.units, currentGraph.until); }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval); } diff --git a/public/src/admin/general/homepage.js b/public/src/admin/general/homepage.js index 96a6aac0eb..f0239e784c 100644 --- a/public/src/admin/general/homepage.js +++ b/public/src/admin/general/homepage.js @@ -1,7 +1,7 @@ "use strict"; /*global define*/ -define('admin/general/homepage', ['admin/settings'], function(Settings) { +define('admin/general/homepage', ['admin/settings'], function (Settings) { function toggleCustomRoute() { if ($('[data-field="homePageRoute"]').val()) { @@ -13,7 +13,7 @@ define('admin/general/homepage', ['admin/settings'], function(Settings) { var Homepage = {}; - Homepage.init = function() { + Homepage.init = function () { $('[data-field="homePageRoute"]').on('change', toggleCustomRoute); toggleCustomRoute(); diff --git a/public/src/admin/general/languages.js b/public/src/admin/general/languages.js index 24d6a418c9..7d597ffc46 100644 --- a/public/src/admin/general/languages.js +++ b/public/src/admin/general/languages.js @@ -1,10 +1,10 @@ "use strict"; /*global define*/ -define('admin/general/languages', ['admin/settings'], function(Settings) { +define('admin/general/languages', ['admin/settings'], function (Settings) { var Languages = {} - Languages.init = function() { + Languages.init = function () { Settings.prepare(); }; diff --git a/public/src/admin/general/navigation.js b/public/src/admin/general/navigation.js index 94fbe01549..35ca62e094 100644 --- a/public/src/admin/general/navigation.js +++ b/public/src/admin/general/navigation.js @@ -1,18 +1,18 @@ "use strict"; /* global define, app, ajaxify, socket, templates */ -define('admin/general/navigation', ['translator', 'iconSelect'], function(translator, iconSelect) { +define('admin/general/navigation', ['translator', 'iconSelect', 'jqueryui'], function (translator, iconSelect, jqueryui) { var navigation = {}, available; - navigation.init = function() { + navigation.init = function () { available = ajaxify.data.available; - $('#enabled .unescape').each(function() { + $('#enabled .unescape').each(function () { $(this).val(translator.unescape($(this).val())); }); - translator.translate(translator.unescape($('#available').html()), function(html) { + translator.translate(translator.unescape($('#available').html()), function (html) { $('#available').html(html) .find('li .drag-item').draggable({ connectToSortable: '#active-navigation', @@ -21,14 +21,14 @@ define('admin/general/navigation', ['translator', 'iconSelect'], function(transl stop: drop }); }); - + $('#active-navigation').sortable().droppable({ accept: $('#available li .drag-item') }); - $('#enabled').on('click', '.iconPicker', function() { + $('#enabled').on('click', '.iconPicker', function () { var iconEl = $(this).find('i'); - iconSelect.init(iconEl, function(el) { + iconSelect.init(iconEl, function (el) { var newIconClass = el.attr('value'); var index = iconEl.parents('[data-index]').attr('data-index'); $('#active-navigation [data-index="' + index + '"] i').attr('class', 'fa fa-fw ' + newIconClass); @@ -66,15 +66,15 @@ define('admin/general/navigation', ['translator', 'iconSelect'], function(transl data = id === 'custom' ? {iconClass: 'fa-navicon'} : available[id]; data.enabled = false; - data.index = parseInt($('#enabled').children().last().attr('data-index'), 10) + 1; + data.index = (parseInt($('#enabled').children().last().attr('data-index'), 10) || 0) + 1; - templates.parse('admin/general/navigation', 'navigation', {navigation: [data]}, function(li) { + templates.parse('admin/general/navigation', 'navigation', {navigation: [data]}, function (li) { li = $(translator.unescape(li)); el.after(li); el.remove(); }); - templates.parse('admin/general/navigation', 'enabled', {enabled: [data]}, function(li) { + templates.parse('admin/general/navigation', 'enabled', {enabled: [data]}, function (li) { li = $(translator.unescape(li)); $('#enabled').append(li); componentHandler.upgradeDom() @@ -85,17 +85,17 @@ define('admin/general/navigation', ['translator', 'iconSelect'], function(transl var nav = []; var indices = []; - $('#active-navigation li').each(function() { + $('#active-navigation li').each(function () { indices.push($(this).attr('data-index')); }); - indices.forEach(function(index) { + indices.forEach(function (index) { var el = $('#enabled').children('[data-index="' + index + '"]'); var form = el.find('form').serializeArray(), data = {}, properties = {}; - form.forEach(function(input) { + form.forEach(function (input) { if (input.name.slice(0, 9) === 'property:' && input.value === 'on') { properties[input.name.slice(9)] = true; } else { @@ -114,7 +114,7 @@ define('admin/general/navigation', ['translator', 'iconSelect'], function(transl nav.push(data); }); - socket.emit('admin.navigation.save', nav, function(err) { + socket.emit('admin.navigation.save', nav, function (err) { if (err) { app.alertError(err.message); } else { diff --git a/public/src/admin/general/social.js b/public/src/admin/general/social.js index 6816eb3909..9accf87417 100644 --- a/public/src/admin/general/social.js +++ b/public/src/admin/general/social.js @@ -1,19 +1,19 @@ "use strict"; /*global define, socket*/ -define('admin/general/social', [], function() { +define('admin/general/social', [], function () { var social = {}; - social.init = function() { - $('#save').on('click', function() { + social.init = function () { + $('#save').on('click', function () { var networks = []; - $('#postSharingNetworks input[type="checkbox"]').each(function() { + $('#postSharingNetworks input[type="checkbox"]').each(function () { if ($(this).prop('checked')) { networks.push($(this).attr('id')); } }); - socket.emit('admin.social.savePostSharingNetworks', networks, function(err) { + socket.emit('admin.social.savePostSharingNetworks', networks, function (err) { if (err) { return app.alertError(err); } diff --git a/public/src/admin/general/sounds.js b/public/src/admin/general/sounds.js index 73899144a0..0ea87f0917 100644 --- a/public/src/admin/general/sounds.js +++ b/public/src/admin/general/sounds.js @@ -1,12 +1,12 @@ "use strict"; /* global app, define, socket */ -define('admin/general/sounds', ['sounds', 'settings'], function(Sounds, Settings) { +define('admin/general/sounds', ['sounds', 'settings'], function (Sounds, Settings) { var SoundsAdmin = {}; - SoundsAdmin.init = function() { + SoundsAdmin.init = function () { // Sounds tab - $('.sounds').find('button[data-action="play"]').on('click', function(e) { + $('.sounds').find('button[data-action="play"]').on('click', function (e) { e.preventDefault(); var fileName = $(this).parent().parent().find('select').val(); @@ -18,8 +18,8 @@ define('admin/general/sounds', ['sounds', 'settings'], function(Sounds, Settings // Saving of Form Values var saveEl = $('#save'); - saveEl.on('click', function() { - Settings.save('sounds', $('.sounds form'), function() { + saveEl.on('click', function () { + Settings.save('sounds', $('.sounds form'), function () { socket.emit('admin.fireEvent', { name: 'event:sounds.reloadMapping' }); diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index b4a5f19601..4244fa41ab 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -1,11 +1,11 @@ "use strict"; /*global define, socket, app, bootbox, templates, ajaxify, Sortable */ -define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() { +define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-serializeobject.min', 'translator'], function (serialize, translator) { var Categories = {}, newCategoryId = -1, sortables; - Categories.init = function() { - socket.emit('admin.categories.getAll', function(error, payload){ + Categories.init = function () { + socket.emit('admin.categories.getAll', function (error, payload){ if(error){ return app.alertError(error.message); } @@ -16,13 +16,13 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri $('button[data-action="create"]').on('click', Categories.throwCreateModal); // Enable/Disable toggle events - $('.categories').on('click', 'button[data-action="toggle"]', function() { + $('.categories').on('click', 'button[data-action="toggle"]', function () { var $this = $(this), cid = $this.attr('data-cid'), parentEl = $this.parents('li[data-cid="' + cid + '"]'), disabled = parentEl.hasClass('disabled'); - var children = parentEl.find('li[data-cid]').map(function() { + var children = parentEl.find('li[data-cid]').map(function () { return $(this).attr('data-cid'); }).get(); @@ -31,15 +31,15 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri }); }; - Categories.throwCreateModal = function() { - socket.emit('admin.categories.getNames', {}, function(err, categories) { + Categories.throwCreateModal = function () { + socket.emit('admin.categories.getNames', {}, function (err, categories) { if (err) { return app.alertError(err.message); } templates.parse('admin/partials/categories/create', { categories: categories - }, function(html) { + }, function (html) { function submit() { var formData = modal.find('form').serializeObject(); formData.description = ''; @@ -67,8 +67,8 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri }); }; - Categories.create = function(payload) { - socket.emit('admin.categories.create', payload, function(err, data) { + Categories.create = function (payload) { + socket.emit('admin.categories.create', payload, function (err, data) { if (err) { return app.alertError(err.message); } @@ -85,7 +85,7 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri }); }; - Categories.render = function(categories){ + Categories.render = function (categories){ var container = $('.categories'); if (!categories || !categories.length) { @@ -99,16 +99,16 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri } }; - Categories.toggle = function(cids, disabled) { + Categories.toggle = function (cids, disabled) { var payload = {}; - cids.forEach(function(cid) { + cids.forEach(function (cid) { payload[cid] = { disabled: disabled ? 1 : 0 }; }); - socket.emit('admin.categories.update', payload, function(err) { + socket.emit('admin.categories.update', payload, function (err) { if (err) { return app.alertError(err.message); } @@ -154,8 +154,8 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri function renderList(categories, container, parentId){ // Translate category names if needed var count = 0; - categories.forEach(function(category, idx, parent) { - translator.translate(category.name, function(translated) { + categories.forEach(function (category, idx, parent) { + translator.translate(category.name, function (translated) { if (category.name !== translated) { category.name = translated; } @@ -167,15 +167,19 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri }); }); + if (!categories.length) { + continueRender(); + } + function continueRender() { templates.parse('admin/partials/categories/category-rows', { cid: parentId, categories: categories - }, function(html) { + }, function (html) { container.append(html); // Handle and children categories in this level have - for(var x=0,numCategories=categories.length;x<numCategories;x++) { + for(var x = 0,numCategories = categories.length;x < numCategories;x++) { renderList(categories[x].children, $('li[data-cid="' + categories[x].cid + '"]'), categories[x].cid); } diff --git a/public/src/admin/manage/category-analytics.js b/public/src/admin/manage/category-analytics.js index 52a24ecfeb..4bd80c305e 100644 --- a/public/src/admin/manage/category-analytics.js +++ b/public/src/admin/manage/category-analytics.js @@ -1,23 +1,23 @@ "use strict"; -/*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ +/*global define, ajaxify, utils */ -define('admin/manage/category-analytics', [], function() { +define('admin/manage/category-analytics', ['Chart'], function (Chart) { var CategoryAnalytics = {}; - CategoryAnalytics.init = function() { - var hourlyCanvas = document.getElementById('pageviews:hourly'), - dailyCanvas = document.getElementById('pageviews:daily'), - topicsCanvas = document.getElementById('topics:daily'), - postsCanvas = document.getElementById('posts:daily'), - hourlyLabels = utils.getHoursArray().map(function(text, idx) { + CategoryAnalytics.init = function () { + var hourlyCanvas = document.getElementById('pageviews:hourly'); + var dailyCanvas = document.getElementById('pageviews:daily'); + var topicsCanvas = document.getElementById('topics:daily'); + var postsCanvas = document.getElementById('posts:daily'); + var hourlyLabels = utils.getHoursArray().map(function (text, idx) { return idx % 3 ? '' : text; - }), - dailyLabels = utils.getDaysArray().map(function(text, idx) { + }); + var dailyLabels = utils.getDaysArray().map(function (text, idx) { return idx % 3 ? '' : text; }); if (utils.isMobile()) { - Chart.defaults.global.showTooltips = false; + Chart.defaults.global.tooltips.enabled = false; } var data = { @@ -26,12 +26,12 @@ define('admin/manage/category-analytics', [], function() { datasets: [ { label: "", - fillColor: "rgba(186,139,175,0.2)", - strokeColor: "rgba(186,139,175,1)", - pointColor: "rgba(186,139,175,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(186,139,175,1)", + backgroundColor: "rgba(186,139,175,0.2)", + borderColor: "rgba(186,139,175,1)", + pointBackgroundColor: "rgba(186,139,175,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(186,139,175,1)", data: ajaxify.data.analytics['pageviews:hourly'] } ] @@ -41,12 +41,12 @@ define('admin/manage/category-analytics', [], function() { datasets: [ { label: "", - fillColor: "rgba(151,187,205,0.2)", - strokeColor: "rgba(151,187,205,1)", - pointColor: "rgba(151,187,205,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(151,187,205,1)", + backgroundColor: "rgba(151,187,205,0.2)", + borderColor: "rgba(151,187,205,1)", + pointBackgroundColor: "rgba(151,187,205,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(151,187,205,1)", data: ajaxify.data.analytics['pageviews:daily'] } ] @@ -56,12 +56,12 @@ define('admin/manage/category-analytics', [], function() { datasets: [ { label: "", - fillColor: "rgba(171,70,66,0.2)", - strokeColor: "rgba(171,70,66,1)", - pointColor: "rgba(171,70,66,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(171,70,66,1)", + backgroundColor: "rgba(171,70,66,0.2)", + borderColor: "rgba(171,70,66,1)", + pointBackgroundColor: "rgba(171,70,66,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(171,70,66,1)", data: ajaxify.data.analytics['topics:daily'] } ] @@ -71,37 +71,97 @@ define('admin/manage/category-analytics', [], function() { datasets: [ { label: "", - fillColor: "rgba(161,181,108,0.2)", - strokeColor: "rgba(161,181,108,1)", - pointColor: "rgba(161,181,108,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(161,181,108,1)", + backgroundColor: "rgba(161,181,108,0.2)", + borderColor: "rgba(161,181,108,1)", + pointBackgroundColor: "rgba(161,181,108,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(161,181,108,1)", data: ajaxify.data.analytics['posts:daily'] } ] - }, + } }; hourlyCanvas.width = $(hourlyCanvas).parent().width(); dailyCanvas.width = $(dailyCanvas).parent().width(); topicsCanvas.width = $(topicsCanvas).parent().width(); postsCanvas.width = $(postsCanvas).parent().width(); - new Chart(hourlyCanvas.getContext('2d')).Line(data['pageviews:hourly'], { - responsive: true, - animation: false + + new Chart(hourlyCanvas.getContext('2d'), { + type: 'line', + data: data['pageviews:hourly'], + options: { + responsive: true, + animation: false, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } }); - new Chart(dailyCanvas.getContext('2d')).Line(data['pageviews:daily'], { - responsive: true, - animation: false + + new Chart(dailyCanvas.getContext('2d'), { + type: 'line', + data: data['pageviews:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } }); - new Chart(topicsCanvas.getContext('2d')).Line(data['topics:daily'], { - responsive: true, - animation: false + + new Chart(topicsCanvas.getContext('2d'), { + type: 'line', + data: data['topics:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } }); - new Chart(postsCanvas.getContext('2d')).Line(data['posts:daily'], { - responsive: true, - animation: false + + new Chart(postsCanvas.getContext('2d'), { + type: 'line', + data: data['posts:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } }); }; diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index d37fac64b0..a48c73f9ec 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -1,15 +1,15 @@ "use strict"; -/*global config, define, app, socket, ajaxify, bootbox, templates, Chart, utils */ +/*global config, define, app, socket, ajaxify, bootbox, templates */ define('admin/manage/category', [ 'uploader', 'iconSelect', 'admin/modules/colorpicker', 'autocomplete' -], function(uploader, iconSelect, colorpicker, autocomplete) { +], function (uploader, iconSelect, colorpicker, autocomplete) { var Category = {}; - Category.init = function() { + Category.init = function () { var modified_categories = {}; function modified(el) { @@ -18,6 +18,9 @@ define('admin/manage/category', [ if (cid) { modified_categories[cid] = modified_categories[cid] || {}; modified_categories[cid][$(el).attr('data-name')] = $(el).val(); + + app.flags = app.flags || {}; + app.flags._unsaved = true; } } @@ -25,12 +28,13 @@ define('admin/manage/category', [ e.preventDefault(); if(Object.keys(modified_categories).length) { - socket.emit('admin.categories.update', modified_categories, function(err, result) { + socket.emit('admin.categories.update', modified_categories, function (err, result) { if (err) { return app.alertError(err.message); } if (result && result.length) { + app.flags._unsaved = false; app.alert({ title: 'Updated Categories', message: 'Category IDs ' + result.join(', ') + ' was successfully updated.', @@ -43,7 +47,7 @@ define('admin/manage/category', [ } } - $('.blockclass, form.category select').each(function() { + $('.blockclass, form.category select').each(function () { var $this = $(this); $this.val($this.attr('data-value')); }); @@ -52,7 +56,7 @@ define('admin/manage/category', [ var $inputEl = $(inputEl), previewEl = $inputEl.parents('[data-cid]').find('.category-preview'); - colorpicker.enable($inputEl, function(hsb, hex) { + colorpicker.enable($inputEl, function (hsb, hex) { if ($inputEl.attr('data-name') === 'bgColor') { previewEl.css('background-color', '#' + hex); } else if ($inputEl.attr('data-name') === 'color') { @@ -65,31 +69,31 @@ define('admin/manage/category', [ $('form.category input, form.category select') - .on('change', function(ev) { + .on('change', function (ev) { modified(ev.target); }) - .on('keydown', function(ev) { + .on('keydown', function (ev) { if (ev.which === 13) { ev.preventDefault(); return false; } }); - $('[data-name="imageClass"]').on('change', function() { + $('[data-name="imageClass"]').on('change', function () { $('.category-preview').css('background-size', $(this).val()); }); $('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker); $('#save').on('click', save); - $('.purge').on('click', function(e) { + $('.purge').on('click', function (e) { e.preventDefault(); - bootbox.confirm('<p class="lead">Do you really want to purge this category "' + $('form.category').find('input[data-name="name"]').val() + '"?</p><h5><strong class="text-danger">Warning!</strong> All topics and posts in this category will be purged!</h5> <p class="help-block">Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category <em>temporarily</em>, you\'ll want to "disable" the category instead.</p>', function(confirm) { + bootbox.confirm('<p class="lead">Do you really want to purge this category "' + $('form.category').find('input[data-name="name"]').val() + '"?</p><h5><strong class="text-danger">Warning!</strong> All topics and posts in this category will be purged!</h5> <p class="help-block">Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category <em>temporarily</em>, you\'ll want to "disable" the category instead.</p>', function (confirm) { if (!confirm) { return; } - socket.emit('admin.categories.purge', ajaxify.data.category.cid, function(err) { + socket.emit('admin.categories.purge', ajaxify.data.category.cid, function (err) { if (err) { return app.alertError(err.message); } @@ -99,49 +103,20 @@ define('admin/manage/category', [ }); }); - $('.copy-settings').on('click', function(e) { - e.preventDefault(); - socket.emit('admin.categories.getNames', function(err, categories) { - if (err) { - return app.alertError(err.message); - } - - templates.parse('admin/partials/categories/select-category', { - categories: categories - }, function(html) { - function submit() { - var formData = modal.find('form').serializeObject(); - - socket.emit('admin.categories.copySettingsFrom', {fromCid: formData['select-cid'], toCid: ajaxify.data.category.cid}, function(err) { - if (err) { - return app.alertError(err.message); - } - app.alertSuccess('Settings Copied!'); - ajaxify.refresh(); - }); - - modal.modal('hide'); - return false; + $('.copy-settings').on('click', function () { + selectCategoryModal(function (cid) { + socket.emit('admin.categories.copySettingsFrom', {fromCid: cid, toCid: ajaxify.data.category.cid}, function (err) { + if (err) { + return app.alertError(err.message); } - - var modal = bootbox.dialog({ - title: 'Select a Category', - message: html, - buttons: { - save: { - label: 'Copy', - className: 'btn-primary', - callback: submit - } - } - }); - - modal.find('form').on('submit', submit); + app.alertSuccess('Settings Copied!'); + ajaxify.refresh(); }); }); + return false; }); - $('.upload-button').on('click', function() { + $('.upload-button').on('click', function () { var inputEl = $(this); var cid = inputEl.attr('data-cid'); @@ -149,7 +124,7 @@ define('admin/manage/category', [ title: 'Upload category image', route: config.relative_path + '/api/admin/category/uploadpicture', params: {cid: cid} - }, function(imageUrlOnServer) { + }, function (imageUrlOnServer) { $('#category-image').val(imageUrlOnServer); var previewBox = inputEl.parent().parent().siblings('.category-preview'); previewBox.css('background', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')'); @@ -158,11 +133,11 @@ define('admin/manage/category', [ }); }); - $('#category-image').on('change', function() { + $('#category-image').on('change', function () { $('.category-preview').css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : ''); }); - $('.delete-image').on('click', function(e) { + $('.delete-image').on('click', function (e) { e.preventDefault(); var inputEl = $('#category-image'); @@ -174,18 +149,18 @@ define('admin/manage/category', [ $(this).parent().addClass('hide').hide(); }); - $('.category-preview').on('click', function() { + $('.category-preview').on('click', function () { iconSelect.init($(this).find('i'), modified); }); $('button[data-action="setParent"], button[data-action="changeParent"]').on('click', Category.launchParentSelector); - $('button[data-action="removeParent"]').on('click', function() { - var payload= {}; + $('button[data-action="removeParent"]').on('click', function () { + var payload = {}; payload[ajaxify.data.category.cid] = { parentCid: 0 }; - socket.emit('admin.categories.update', payload, function(err) { + socket.emit('admin.categories.update', payload, function (err) { if (err) { return app.alertError(err.message); } @@ -198,8 +173,8 @@ define('admin/manage/category', [ Category.setupPrivilegeTable(); }; - Category.setupPrivilegeTable = function() { - $('.privilege-table-container').on('change', 'input[type="checkbox"]', function() { + Category.setupPrivilegeTable = function () { + $('.privilege-table-container').on('change', 'input[type="checkbox"]', function () { var checkboxEl = $(this), privilege = checkboxEl.parent().attr('data-privilege'), state = checkboxEl.prop('checked'), @@ -210,7 +185,7 @@ define('admin/manage/category', [ if (member) { if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { - bootbox.confirm('<strong>Are you sure you wish to grant the moderation privilege to this user group?</strong> This group is public, and any users can join at will.', function(confirm) { + bootbox.confirm('<strong>Are you sure you wish to grant the moderation privilege to this user group?</strong> This group is public, and any users can join at will.', function (confirm) { if (confirm) { Category.setPrivilege(member, privilege, state, checkboxEl); } else { @@ -228,40 +203,41 @@ define('admin/manage/category', [ $('.privilege-table-container').on('click', '[data-action="search.user"]', Category.addUserToPrivilegeTable); $('.privilege-table-container').on('click', '[data-action="search.group"]', Category.addGroupToPrivilegeTable); $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Category.copyPrivilegesToChildren); + $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Category.copyPrivilegesFromCategory); Category.exposeAssumedPrivileges(); }; - Category.refreshPrivilegeTable = function() { - socket.emit('admin.categories.getPrivilegeSettings', ajaxify.data.category.cid, function(err, privileges) { + Category.refreshPrivilegeTable = function () { + socket.emit('admin.categories.getPrivilegeSettings', ajaxify.data.category.cid, function (err, privileges) { if (err) { return app.alertError(err.message); } templates.parse('admin/partials/categories/privileges', { privileges: privileges - }, function(html) { + }, function (html) { $('.privilege-table-container').html(html); Category.exposeAssumedPrivileges(); }); }); }; - Category.exposeAssumedPrivileges = function() { + Category.exposeAssumedPrivileges = function () { /* If registered-users has a privilege enabled, then all users and groups of that privilege should be assumed to have that privilege as well, even if not set in the db, so reflect this arrangement in the table */ var privs = []; - $('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function(idx, el) { + $('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) { if ($(el).find('input').prop('checked')) { privs.push(el.getAttribute('data-privilege')); } }); - for(var x=0,numPrivs=privs.length;x<numPrivs;x++) { + for(var x = 0,numPrivs = privs.length;x < numPrivs;x++) { var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input'); - inputs.each(function(idx, el) { + inputs.each(function (idx, el) { if (!el.checked) { el.indeterminate = true; } @@ -269,13 +245,13 @@ define('admin/manage/category', [ } }; - Category.setPrivilege = function(member, privilege, state, checkboxEl) { + Category.setPrivilege = function (member, privilege, state, checkboxEl) { socket.emit('admin.categories.setPrivilege', { cid: ajaxify.data.category.cid, privilege: privilege, set: state, member: member - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -285,25 +261,25 @@ define('admin/manage/category', [ }); }; - Category.launchParentSelector = function() { - socket.emit('categories.get', function(err, categories) { + Category.launchParentSelector = function () { + socket.emit('categories.get', function (err, categories) { if (err) { return app.alertError(err.message); } - categories = categories.filter(function(category) { + categories = categories.filter(function (category) { return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); }); templates.parse('partials/category_list', { categories: categories - }, function(html) { + }, function (html) { var modal = bootbox.dialog({ message: html, title: 'Set Parent Category' }); - modal.find('li[data-cid]').on('click', function() { + modal.find('li[data-cid]').on('click', function () { var parentCid = $(this).attr('data-cid'), payload = {}; @@ -311,11 +287,11 @@ define('admin/manage/category', [ parentCid: parentCid }; - socket.emit('admin.categories.update', payload, function(err) { + socket.emit('admin.categories.update', payload, function (err) { if (err) { return app.alertError(err.message); } - var parent = categories.filter(function(category) { + var parent = categories.filter(function (category) { return category && parseInt(category.cid, 10) === parseInt(parentCid, 10); }); parent = parent[0]; @@ -331,23 +307,23 @@ define('admin/manage/category', [ }); }; - Category.addUserToPrivilegeTable = function() { + Category.addUserToPrivilegeTable = function () { var modal = bootbox.dialog({ title: 'Find a User', message: '<input class="form-control input-lg" placeholder="Search for a user here..." />', show: true }); - modal.on('shown.bs.modal', function() { + modal.on('shown.bs.modal', function () { var inputEl = modal.find('input'); - autocomplete.user(inputEl, function(ev, ui) { + autocomplete.user(inputEl, function (ev, ui) { socket.emit('admin.categories.setPrivilege', { cid: ajaxify.data.category.cid, - privilege: ['find', 'read'], + privilege: ['find', 'read', 'topics:read'], set: true, member: ui.item.user.uid - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -359,23 +335,23 @@ define('admin/manage/category', [ }); }; - Category.addGroupToPrivilegeTable = function() { + Category.addGroupToPrivilegeTable = function () { var modal = bootbox.dialog({ title: 'Find a Group', message: '<input class="form-control input-lg" placeholder="Search for a group here..." />', show: true }); - modal.on('shown.bs.modal', function() { + modal.on('shown.bs.modal', function () { var inputEl = modal.find('input'); - autocomplete.group(inputEl, function(ev, ui) { + autocomplete.group(inputEl, function (ev, ui) { socket.emit('admin.categories.setPrivilege', { cid: ajaxify.data.category.cid, - privilege: ['groups:find', 'groups:read'], + privilege: ['groups:find', 'groups:read', 'groups:topics:read'], set: true, member: ui.item.group.name - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -387,8 +363,8 @@ define('admin/manage/category', [ }); }; - Category.copyPrivilegesToChildren = function() { - socket.emit('admin.categories.copyPrivilegesToChildren', ajaxify.data.category.cid, function(err) { + Category.copyPrivilegesToChildren = function () { + socket.emit('admin.categories.copyPrivilegesToChildren', ajaxify.data.category.cid, function (err) { if (err) { return app.alertError(err.message); } @@ -396,5 +372,50 @@ define('admin/manage/category', [ }); }; + Category.copyPrivilegesFromCategory = function () { + selectCategoryModal(function (cid) { + socket.emit('admin.categories.copyPrivilegesFrom', {toCid: ajaxify.data.category.cid, fromCid: cid}, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); + }); + }); + }; + + function selectCategoryModal(callback) { + socket.emit('admin.categories.getNames', function (err, categories) { + if (err) { + return app.alertError(err.message); + } + + templates.parse('admin/partials/categories/select-category', { + categories: categories + }, function (html) { + function submit() { + var formData = modal.find('form').serializeObject(); + callback(formData['select-cid']); + modal.modal('hide'); + return false; + } + + var modal = bootbox.dialog({ + title: 'Select a Category', + message: html, + buttons: { + save: { + label: 'Copy', + className: 'btn-primary', + callback: submit + } + } + }); + + modal.find('form').on('submit', submit); + }); + }); + } + + return Category; }); \ No newline at end of file diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js index eaa839522a..fb8b32d602 100644 --- a/public/src/admin/manage/flags.js +++ b/public/src/admin/manage/flags.js @@ -1,60 +1,68 @@ "use strict"; -/*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/ +/*global define, socket, app, utils, bootbox, ajaxify*/ define('admin/manage/flags', [ - 'forum/infinitescroll', - 'admin/modules/selectable', - 'autocomplete' -], function(infinitescroll, selectable, autocomplete) { + 'autocomplete', + 'Chart', + 'components' +], function (autocomplete, Chart, components) { var Flags = {}; - Flags.init = function() { + Flags.init = function () { $('.post-container .content img:not(.not-responsive)').addClass('img-responsive'); - var params = utils.params(); - $('#flag-sort-by').val(params.sortBy); autocomplete.user($('#byUsername')); handleDismiss(); handleDismissAll(); handleDelete(); - handleInfiniteScroll(); + handleGraphs(); + + updateFlagDetails(ajaxify.data.posts); + + components.get('posts/flags').on('click', '[component="posts/flag/update"]', updateFlag); + + // Open flag as indicated in location bar + if (window.location.hash.startsWith('#flag-pid-')) { + $(window.location.hash).collapse('toggle'); + } }; function handleDismiss() { - $('.flags').on('click', '.dismiss', function() { + $('.flags').on('click', '.dismiss', function () { var btn = $(this); var pid = btn.parents('[data-pid]').attr('data-pid'); - socket.emit('posts.dismissFlag', pid, function(err) { + socket.emit('posts.dismissFlag', pid, function (err) { done(err, btn); }); }); } function handleDismissAll() { - $('#dismissAll').on('click', function() { - socket.emit('posts.dismissAllFlags', function(err) { + $('#dismissAll').on('click', function () { + socket.emit('posts.dismissAllFlags', function (err) { if (err) { return app.alertError(err.message); } ajaxify.refresh(); }); + return false; }); } function handleDelete() { - $('.flags').on('click', '.delete', function() { + $('.flags').on('click', '.delete', function () { var btn = $(this); - bootbox.confirm('Do you really want to delete this post?', function(confirm) { + bootbox.confirm('Do you really want to delete this post?', function (confirm) { if (!confirm) { return; } var pid = btn.parents('[data-pid]').attr('data-pid'); var tid = btn.parents('[data-pid]').attr('data-tid'); - socket.emit('posts.delete', {pid: pid, tid: tid}, function(err) { + socket.emit('posts.delete', {pid: pid, tid: tid}, function (err) { done(err, btn); }); }); @@ -65,7 +73,7 @@ define('admin/manage/flags', [ if (err) { return app.alertError(err.messaage); } - btn.parents('[data-pid]').fadeOut(function() { + btn.parents('[data-pid]').fadeOut(function () { $(this).remove(); if (!$('.flags [data-pid]').length) { $('.post-container').text('No flagged posts!'); @@ -73,31 +81,90 @@ define('admin/manage/flags', [ }); } - function handleInfiniteScroll() { - infinitescroll.init(function(direction) { - if (direction < 0 && !$('.flags').length) { - return; + function handleGraphs() { + var dailyCanvas = document.getElementById('flags:daily'); + var dailyLabels = utils.getDaysArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + + if (utils.isMobile()) { + Chart.defaults.global.tooltips.enabled = false; + } + var data = { + 'flags:daily': { + labels: dailyLabels, + datasets: [ + { + label: "", + backgroundColor: "rgba(151,187,205,0.2)", + borderColor: "rgba(151,187,205,1)", + pointBackgroundColor: "rgba(151,187,205,1)", + pointHoverBackgroundColor: "#fff", + pointBorderColor: "#fff", + pointHoverBorderColor: "rgba(151,187,205,1)", + data: ajaxify.data.analytics + } + ] } - var params = utils.params(); - var sortBy = params.sortBy || 'count'; - var byUsername = params.byUsername || ''; - - infinitescroll.loadMore('posts.getMoreFlags', { - byUsername: byUsername, - sortBy: sortBy, - after: $('[data-next]').attr('data-next') - }, function(data, done) { - if (data.posts && data.posts.length) { - app.parseAndTranslate('admin/manage/flags', 'posts', {posts: data.posts}, function(html) { - $('[data-next]').attr('data-next', data.next); - $('.post-container').append(html); - html.find('img:not(.not-responsive)').addClass('img-responsive'); - done(); - }); - } else { - done(); + }; + + dailyCanvas.width = $(dailyCanvas).parent().width(); + new Chart(dailyCanvas.getContext('2d'), { + type: 'line', + data: data['flags:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] } - }); + } + }); + } + + function updateFlagDetails(source) { + // As the flag details are returned in the API, update the form controls to show the correct data + + // Create reference hash for use in this method + source = source.reduce(function (memo, cur) { + memo[cur.pid] = cur.flagData; + return memo; + }, {}); + + components.get('posts/flag').each(function (idx, el) { + var pid = el.getAttribute('data-pid'); + var el = $(el); + + if (source[pid]) { + for(var prop in source[pid]) { + if (source[pid].hasOwnProperty(prop)) { + el.find('[name="' + prop + '"]').val(source[pid][prop]); + } + } + } + }); + } + + function updateFlag() { + var pid = $(this).parents('[component="posts/flag"]').attr('data-pid'); + var formData = $($(this).parents('form').get(0)).serializeArray(); + + socket.emit('posts.updateFlag', { + pid: pid, + data: formData + }, function (err) { + if (err) { + return app.alertError(err.message); + } else { + app.alertSuccess('[[topic:flag_manage_saved]]'); + } }); } diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index 06284e4e88..ce41436dac 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -6,10 +6,10 @@ define('admin/manage/group', [ 'iconSelect', 'admin/modules/colorpicker', 'translator' -], function(memberList, iconSelect, colorpicker, translator) { +], function (memberList, iconSelect, colorpicker, translator) { var Groups = {}; - Groups.init = function() { + Groups.init = function () { var groupDetailsSearch = $('#group-details-search'), groupDetailsSearchResults = $('#group-details-search-results'), groupIcon = $('#group-icon'), @@ -23,25 +23,25 @@ define('admin/manage/group', [ memberList.init(); - changeGroupUserTitle.keyup(function() { + changeGroupUserTitle.keyup(function () { groupLabelPreview.text(changeGroupUserTitle.val()); }); - changeGroupLabelColor.keyup(function() { + changeGroupLabelColor.keyup(function () { groupLabelPreview.css('background', changeGroupLabelColor.val() || '#000000'); }); - groupDetailsSearch.on('keyup', function() { + groupDetailsSearch.on('keyup', function () { if (searchDelay) { clearTimeout(searchDelay); } - searchDelay = setTimeout(function() { + searchDelay = setTimeout(function () { var searchText = groupDetailsSearch.val(), foundUser; - socket.emit('admin.user.search', {query: searchText}, function(err, results) { + socket.emit('admin.user.search', {query: searchText}, function (err, results) { if (!err && results && results.users.length > 0) { var numResults = results.users.length, x; if (numResults > 20) { @@ -75,14 +75,14 @@ define('admin/manage/group', [ }, 200); }); - groupDetailsSearchResults.on('click', 'li[data-uid]', function() { + groupDetailsSearchResults.on('click', 'li[data-uid]', function () { var userLabel = $(this), uid = parseInt(userLabel.attr('data-uid'), 10); socket.emit('admin.groups.join', { groupName: groupName, uid: uid - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -96,15 +96,15 @@ define('admin/manage/group', [ "icon:text": userLabel.attr('data-usericon-text') }; - templates.parse('partials/groups/memberlist', 'members', {group: {isOwner: ajaxify.data.group.isOwner, members: [member]}}, function(html) { - translator.translate(html, function(html) { + templates.parse('partials/groups/memberlist', 'members', {group: {isOwner: ajaxify.data.group.isOwner, members: [member]}}, function (html) { + translator.translate(html, function (html) { $('[component="groups/members"] tbody').prepend(html); }); }); }); }); - $('[component="groups/members"]').on('click', '[data-action]', function() { + $('[component="groups/members"]').on('click', '[data-action]', function () { var btnEl = $(this), userRow = btnEl.parents('[data-uid]'), ownerFlagEl = userRow.find('.member-name i'), @@ -117,7 +117,7 @@ define('admin/manage/group', [ socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { toUid: uid, groupName: groupName - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -126,14 +126,14 @@ define('admin/manage/group', [ break; case 'kick': - bootbox.confirm('Are you sure you want to remove this user?', function(confirm) { + bootbox.confirm('Are you sure you want to remove this user?', function (confirm) { if (!confirm) { return; } socket.emit('admin.groups.leave', { uid: uid, groupName: groupName - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -147,15 +147,15 @@ define('admin/manage/group', [ } }); - $('#group-icon').on('click', function() { + $('#group-icon').on('click', function () { iconSelect.init(groupIcon); }); - colorpicker.enable(changeGroupLabelColor, function(hsb, hex) { + colorpicker.enable(changeGroupLabelColor, function (hsb, hex) { groupLabelPreview.css('background-color', '#' + hex); }); - $('.save').on('click', function() { + $('.save').on('click', function () { socket.emit('admin.groups.update', { groupName: groupName, values: { @@ -169,11 +169,19 @@ define('admin/manage/group', [ hidden: $('#group-hidden').is(':checked'), disableJoinRequests: $('#group-disableJoinRequests').is(':checked') } - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } - app.alertSuccess('Changes saved!'); + + var newName = $('#change-group-name').val(); + + // If the group name changed, change url + if (groupName === newName) { + app.alertSuccess('Changes saved!'); + } else { + ajaxify.go('admin/manage/groups/' + encodeURIComponent(newName), undefined, true); + } }); return false; }); diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index 8c68837403..bbb0c08352 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -4,12 +4,12 @@ define('admin/manage/groups', [ 'translator', 'components' -], function(translator, components) { +], function (translator, components) { var Groups = {}; var intervalId = 0; - Groups.init = function() { + Groups.init = function () { var createModal = $('#create-modal'), createGroupName = $('#create-group-name'), createModalGo = $('#create-modal-go'), @@ -17,30 +17,30 @@ define('admin/manage/groups', [ handleSearch(); - createModal.on('keypress', function(e) { + createModal.on('keypress', function (e) { if (e.keyCode === 13) { createModalGo.click(); } }); - $('#create').on('click', function() { + $('#create').on('click', function () { createModal.modal('show'); - setTimeout(function() { + setTimeout(function () { createGroupName.focus(); }, 250); }); - createModalGo.on('click', function() { + createModalGo.on('click', function () { var submitObj = { name: createGroupName.val(), description: $('#create-group-desc').val() }, errorText; - socket.emit('admin.groups.create', submitObj, function(err) { + socket.emit('admin.groups.create', submitObj, function (err) { if (err) { if (err.hasOwnProperty('message') && utils.hasLanguageKey(err.message)) { - translator.translate(err.message, config.defaultLang, function(translated) { + translator.translate(err.message, config.defaultLang, function (translated) { createModalError.html(translated).removeClass('hide'); }); } else { @@ -49,7 +49,7 @@ define('admin/manage/groups', [ } else { createModalError.addClass('hide'); createGroupName.val(''); - createModal.on('hidden.bs.modal', function() { + createModal.on('hidden.bs.modal', function () { ajaxify.refresh(); }); createModal.modal('hide'); @@ -57,18 +57,18 @@ define('admin/manage/groups', [ }); }); - $('.groups-list').on('click', 'button[data-action]', function() { + $('.groups-list').on('click', 'button[data-action]', function () { var el = $(this), action = el.attr('data-action'), groupName = el.parents('tr[data-groupname]').attr('data-groupname'); switch (action) { case 'delete': - bootbox.confirm('Are you sure you wish to delete this group?', function(confirm) { + bootbox.confirm('Are you sure you wish to delete this group?', function (confirm) { if (confirm) { socket.emit('groups.delete', { groupName: groupName - }, function(err, data) { + }, function (err, data) { if(err) { return app.alertError(err.message); } @@ -94,10 +94,14 @@ define('admin/manage/groups', [ options: { sort: 'date' } - }, function(err, groups) { + }, function (err, groups) { + if (err) { + return app.alertError(err.message); + } + templates.parse('admin/manage/groups', 'groups', { groups: groups - }, function(html) { + }, function (html) { groupsEl.find('[data-groupname]').remove(); groupsEl.find('tr').after(html); }); @@ -106,7 +110,7 @@ define('admin/manage/groups', [ var queryEl = $('#group-search'); - queryEl.on('keyup', function() { + queryEl.on('keyup', function () { if (intervalId) { clearTimeout(intervalId); intervalId = 0; diff --git a/public/src/admin/manage/ip-blacklist.js b/public/src/admin/manage/ip-blacklist.js index 9769b3e3bb..03f4d8bb5c 100644 --- a/public/src/admin/manage/ip-blacklist.js +++ b/public/src/admin/manage/ip-blacklist.js @@ -1,19 +1,19 @@ 'use strict'; /* globals $, app, socket, templates, define, bootbox */ -define('admin/manage/ip-blacklist', [], function() { +define('admin/manage/ip-blacklist', [], function () { var Blacklist = {}; - Blacklist.init = function() { + Blacklist.init = function () { var blacklist = $('#blacklist-rules'); - blacklist.on('keyup', function() { + blacklist.on('keyup', function () { $('#blacklist-rules-holder').val(blacklist.val()); }); - $('[data-action="apply"]').on('click', function() { - socket.emit('blacklist.save', blacklist.val(), function(err) { + $('[data-action="apply"]').on('click', function () { + socket.emit('blacklist.save', blacklist.val(), function (err) { if (err) { return app.alertError(err.message); } @@ -25,11 +25,15 @@ define('admin/manage/ip-blacklist', [], function() { }); }); - $('[data-action="test"]').on('click', function() { + $('[data-action="test"]').on('click', function () { socket.emit('blacklist.validate', { rules: blacklist.val() - }, function(err, data) { - templates.parse('admin/partials/blacklist-validate', data, function(html) { + }, function (err, data) { + if (err) { + return app.alertError(err.message); + } + + templates.parse('admin/partials/blacklist-validate', data, function (html) { bootbox.alert(html); }); }); diff --git a/public/src/admin/manage/registration.js b/public/src/admin/manage/registration.js index 55578901e8..329a4edb96 100644 --- a/public/src/admin/manage/registration.js +++ b/public/src/admin/manage/registration.js @@ -2,18 +2,18 @@ /* global config, socket, define, templates, bootbox, app, ajaxify, */ -define('admin/manage/registration', function() { +define('admin/manage/registration', function () { var Registration = {}; - Registration.init = function() { + Registration.init = function () { - $('.users-list').on('click', '[data-action]', function(ev) { + $('.users-list').on('click', '[data-action]', function (ev) { var parent = $(this).parents('[data-username]'); var action = $(this).attr('data-action'); var username = parent.attr('data-username'); var method = action === 'accept' ? 'admin.user.acceptRegistration' : 'admin.user.rejectRegistration'; - socket.emit(method, {username: username}, function(err) { + socket.emit(method, {username: username}, function (err) { if (err) { return app.alertError(err.message); } @@ -22,7 +22,7 @@ define('admin/manage/registration', function() { return false; }); - $('.invites-list').on('click', '[data-action]', function(ev) { + $('.invites-list').on('click', '[data-action]', function (ev) { var parent = $(this).parents('[data-invitation-mail][data-invited-by]'); var email = parent.attr('data-invitation-mail'); var invitedBy = parent.attr('data-invited-by'); @@ -39,9 +39,9 @@ define('admin/manage/registration', function() { parent.remove(); }; if (action === 'delete') { - bootbox.confirm('Are you sure you wish to delete this invitation?', function(confirm) { + bootbox.confirm('Are you sure you wish to delete this invitation?', function (confirm) { if (confirm) { - socket.emit(method, {email: email, invitedBy: invitedBy}, function(err) { + socket.emit(method, {email: email, invitedBy: invitedBy}, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/admin/manage/tags.js b/public/src/admin/manage/tags.js index 01f755612b..108fce3797 100644 --- a/public/src/admin/manage/tags.js +++ b/public/src/admin/manage/tags.js @@ -1,36 +1,72 @@ "use strict"; -/*global define, socket, app, utils, bootbox*/ +/*global define, socket, app, utils, bootbox, ajaxify*/ define('admin/manage/tags', [ 'forum/infinitescroll', 'admin/modules/selectable', 'admin/modules/colorpicker' -], function(infinitescroll, selectable, colorpicker) { +], function (infinitescroll, selectable, colorpicker) { var Tags = {}, timeoutId = 0; - Tags.init = function() { + Tags.init = function () { selectable.enable('.tag-management', '.tag-row'); + handleCreate(); handleSearch(); handleModify(); handleDeleteSelected(); }; + function handleCreate() { + var createModal = $('#create-modal'); + var createTagName = $('#create-tag-name'); + var createModalGo = $('#create-modal-go'); + + createModal.on('keypress', function (e) { + if (e.keyCode === 13) { + createModalGo.click(); + } + }); + + $('#create').on('click', function () { + createModal.modal('show'); + setTimeout(function () { + createTagName.focus(); + }, 250); + }); + + createModalGo.on('click', function () { + socket.emit('admin.tags.create', { + tag: createTagName.val() + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + createTagName.val(''); + createModal.on('hidden.bs.modal', function () { + ajaxify.refresh(); + }); + createModal.modal('hide'); + }); + }); + } + function handleSearch() { - $('#tag-search').on('input propertychange', function() { + $('#tag-search').on('input propertychange', function () { if (timeoutId) { clearTimeout(timeoutId); timeoutId = 0; } - timeoutId = setTimeout(function() { - socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, result) { + timeoutId = setTimeout(function () { + socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function (err, result) { if (err) { return app.alertError(err.message); } - app.parseAndTranslate('admin/manage/tags', 'tags', {tags: result.tags}, function(html) { + app.parseAndTranslate('admin/manage/tags', 'tags', {tags: result.tags}, function (html) { $('.tag-list').html(html); utils.makeNumbersHumanReadable(html.find('.human-readable-number')); timeoutId = 0; @@ -43,8 +79,8 @@ define('admin/manage/tags', [ } function handleModify() { - $('#modify').on('click', function() { - var tagsToModify = $('.tag-row.selected'); + $('#modify').on('click', function () { + var tagsToModify = $('.tag-row.ui-selected'); if (!tagsToModify.length) { return; } @@ -59,12 +95,12 @@ define('admin/manage/tags', [ success: { label: "Save", className: "btn-primary save", - callback: function() { + callback: function () { var modal = $('.bootbox'), bgColor = modal.find('[data-name="bgColor"]').val(), color = modal.find('[data-name="color"]').val(); - tagsToModify.each(function(idx, tag) { + tagsToModify.each(function (idx, tag) { tag = $(tag); tag.find('[data-name="bgColor"]').val(bgColor); @@ -83,21 +119,21 @@ define('admin/manage/tags', [ } function handleDeleteSelected() { - $('#deleteSelected').on('click', function() { - var tagsToDelete = $('.tag-row.selected'); + $('#deleteSelected').on('click', function () { + var tagsToDelete = $('.tag-row.ui-selected'); if (!tagsToDelete.length) { return; } - bootbox.confirm('Do you want to delete the selected tags?', function(confirm) { + bootbox.confirm('Do you want to delete the selected tags?', function (confirm) { if (!confirm) { return; } var tags = []; - tagsToDelete.each(function(index, el) { + tagsToDelete.each(function (index, el) { tags.push($(el).attr('data-tag')); }); - socket.emit('admin.tags.deleteTags', {tags: tags}, function(err) { + socket.emit('admin.tags.deleteTags', {tags: tags}, function (err) { if (err) { return app.alertError(err.message); } @@ -123,7 +159,7 @@ define('admin/manage/tags', [ color : tag.find('[data-name="color"]').val() }; - socket.emit('admin.tags.update', data, function(err) { + socket.emit('admin.tags.update', data, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 09c9e3a01e..b8fb8d5de6 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -1,38 +1,47 @@ "use strict"; -/* global socket, define, templates, bootbox, app, ajaxify */ +/* global config, socket, define, templates, bootbox, app, ajaxify */ -define('admin/manage/users', ['admin/modules/selectable'], function(selectable) { +define('admin/manage/users', ['translator'], function (translator) { var Users = {}; - Users.init = function() { - selectable.enable('#users-container', '.user-selectable'); + Users.init = function () { + var navPills = $('.nav-pills li'); + var pathname = window.location.pathname; + if (!navPills.find('a[href="' + pathname + '"]').length) { + pathname = config.relative_path + '/admin/manage/users/latest'; + } + navPills.removeClass('active').find('a[href="' + pathname + '"]').parent().addClass('active'); function getSelectedUids() { var uids = []; - $('#users-container .users-box .selected').each(function() { - uids.push($(this).parents('[data-uid]').attr('data-uid')); + + $('.users-table [component="user/select/single"]').each(function () { + if ($(this).is(':checked')) { + uids.push($(this).attr('data-uid')); + } }); return uids; } function update(className, state) { - $('#users-container .users-box .selected').siblings('.labels').find(className).each(function() { - $(this).toggleClass('hide', !state); + $('.users-table [component="user/select/single"]:checked').parents('.user-row').find(className).each(function () { + $(this).toggleClass('hidden', !state); }); } function unselectAll() { - $('#users-container .users-box .selected').removeClass('selected'); + $('.users-table [component="user/select/single"]').prop('checked', false); + $('.users-table [component="user/select/all"]').prop('checked', false); } function removeSelected() { - $('#users-container .users-box .selected').parents('.users-box').remove(); + $('.users-table [component="user/select/single"]:checked').parents('.user-row').remove(); } function done(successMessage, className, flag) { - return function(err) { + return function (err) { if (err) { return app.alertError(err.message); } @@ -44,51 +53,91 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) }; } - $('.ban-user').on('click', function() { + $('[component="user/select/all"]').on('click', function () { + if ($(this).is(':checked')) { + $('.users-table [component="user/select/single"]').prop('checked', true); + } else { + $('.users-table [component="user/select/single"]').prop('checked', false); + } + }); + + $('.ban-user').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { - return false; + app.alertError('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open } - bootbox.confirm('Do you really want to ban?', function(confirm) { + bootbox.confirm('Do you really want to ban ' + (uids.length > 1 ? 'these users' : 'this user') + ' <strong>permanently</strong>?', function (confirm) { if (confirm) { - socket.emit('user.banUsers', uids, done('User(s) banned!', '.ban', true)); + socket.emit('user.banUsers', { uids: uids, reason: '' }, done('User(s) banned!', '.ban', true)); } }); - return false; }); - $('.unban-user').on('click', function() { + $('.ban-user-temporary').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { - return; + app.alertError('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open + } + + templates.parse('admin/partials/temporary-ban', {}, function (html) { + bootbox.dialog({ + className: 'ban-modal', + title: '[[user:ban_account]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link' + }, + submit: { + label: 'Ban ' + uids.length + (uids.length > 1 ? ' users' : ' user'), + callback: function () { + var formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + var until = formData.length ? (Date.now() + formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) : 0; + socket.emit('user.banUsers', { uids: uids, until: until, reason: formData.reason }, done('User(s) banned!', '.ban', true)); + } + } + } + }); + }); + }); + + $('.unban-user').on('click', function () { + var uids = getSelectedUids(); + if (!uids.length) { + app.alertError('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open } socket.emit('user.unbanUsers', uids, done('User(s) unbanned!', '.ban', false)); - return false; }); - $('.reset-lockout').on('click', function() { + $('.reset-lockout').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } socket.emit('admin.user.resetLockouts', uids, done('Lockout(s) reset!')); - return false; }); - $('.reset-flags').on('click', function() { + $('.reset-flags').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } socket.emit('admin.user.resetFlags', uids, done('Flags(s) reset!')); - return false; }); - $('.admin-user').on('click', function() { + $('.admin-user').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; @@ -99,10 +148,9 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) } else { socket.emit('admin.user.makeAdmins', uids, done('User(s) are now administrators.', '.administrator', true)); } - return false; }); - $('.remove-admin-user').on('click', function() { + $('.remove-admin-user').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; @@ -111,35 +159,42 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) if (uids.indexOf(app.user.uid.toString()) !== -1) { app.alertError('You can\'t remove yourself as Administrator!'); } else { - bootbox.confirm('Do you really want to remove admins?', function(confirm) { + bootbox.confirm('Do you really want to remove admins?', function (confirm) { if (confirm) { socket.emit('admin.user.removeAdmins', uids, done('User(s) are no longer administrators.', '.administrator', false)); } }); } - return false; }); - $('.validate-email').on('click', function() { + $('.validate-email').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } - bootbox.confirm('Do you want to validate email(s) of these user(s)?', function(confirm) { - if (confirm) { - socket.emit('admin.user.validateEmail', uids, done('Emails validated', '.notvalidated', false)); + bootbox.confirm('Do you want to validate email(s) of these user(s)?', function (confirm) { + if (!confirm) { + return; } + socket.emit('admin.user.validateEmail', uids, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('Emails validated'); + update('.notvalidated', false); + update('.validated', true); + unselectAll(); + }); }); - return false; }); - $('.send-validation-email').on('click', function() { + $('.send-validation-email').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } - socket.emit('admin.user.sendValidationEmail', uids, function(err) { + socket.emit('admin.user.sendValidationEmail', uids, function (err) { if (err) { return app.alertError(err.message); } @@ -147,29 +202,48 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) }); }); - $('.password-reset-email').on('click', function() { + $('.password-reset-email').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } - bootbox.confirm('Do you want to send password reset email(s) to these user(s)?', function(confirm) { + bootbox.confirm('Do you want to send password reset email(s) to these user(s)?', function (confirm) { if (confirm) { socket.emit('admin.user.sendPasswordResetEmail', uids, done('Emails sent')); } }); - return false; }); - $('.delete-user').on('click', function() { + $('.delete-user').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { return; } - bootbox.confirm('<b>Warning!</b><br/>Do you really want to delete user(s)?<br/> This action is not reversable, all user data and content will be erased!', function(confirm) { + bootbox.confirm('<b>Warning!</b><br/>Do you really want to delete user(s)?<br/> This action is not reversable, only the user account will be deleted, their posts and topics will not be deleled!', function (confirm) { if (confirm) { - socket.emit('admin.user.deleteUsers', uids, function(err) { + socket.emit('admin.user.deleteUsers', uids, function (err) { + if (err) { + return app.alertError(err.message); + } + + app.alertSuccess('User(s) Deleted!'); + removeSelected(); + unselectAll(); + }); + } + }); + }); + + $('.delete-user-and-content').on('click', function () { + var uids = getSelectedUids(); + if (!uids.length) { + return; + } + bootbox.confirm('<b>Warning!</b><br/>Do you really want to delete user(s) and their content?<br/> This action is not reversable, all user data and content will be erased!', function (confirm) { + if (confirm) { + socket.emit('admin.user.deleteUsersAndContent', uids, function (err) { if (err) { return app.alertError(err.message); } @@ -180,53 +254,71 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) }); } }); - return false; }); function handleUserCreate() { - var errorEl = $('#create-modal-error'); - $('#createUser').on('click', function() { - $('#create-modal').modal('show'); - $('#create-modal form')[0].reset(); - errorEl.addClass('hide'); + $('#createUser').on('click', function () { + templates.parse('admin/partials/create_user_modal', {}, function (html) { + translator.translate(html, function (html) { + bootbox.dialog({ + message: html, + title: 'Create User', + onEscape: true, + buttons: { + cancel: { + label: 'Cancel', + className: 'btn-link' + }, + create: { + label: 'Create', + className: 'btn-primary', + callback: function () { + createUser.call(this); + return false; + } + } + } + }); + }); + }); }); + } - $('#create-modal-go').on('click', function() { - var username = $('#create-user-name').val(), - email = $('#create-user-email').val(), - password = $('#create-user-password').val(), - passwordAgain = $('#create-user-password-again').val(); + function createUser() { + var modal = this; + var username = document.getElementById('create-user-name').value; + var email = document.getElementById('create-user-email').value; + var password = document.getElementById('create-user-password').value; + var passwordAgain = document.getElementById('create-user-password-again').value; + var errorEl = $('#create-modal-error'); - if (password !== passwordAgain) { - return errorEl.html('<strong>Error</strong><p>Passwords must match!</p>').removeClass('hide'); - } + if (password !== passwordAgain) { + return errorEl.html('<strong>Error</strong><p>Passwords must match!</p>').removeClass('hide'); + } - var user = { - username: username, - email: email, - password: password - }; + var user = { + username: username, + email: email, + password: password + }; - socket.emit('admin.user.createUser', user, function(err) { - if(err) { - return errorEl.translateHtml('<strong>Error</strong><p>' + err.message + '</p>').removeClass('hide'); - } - $('#create-modal').modal('hide'); - $('#create-modal').on('hidden.bs.modal', function() { - ajaxify.refresh(); - }); - app.alertSuccess('User created!'); - }); + socket.emit('admin.user.createUser', user, function (err) { + if(err) { + return errorEl.translateHtml('<strong>Error</strong><p>' + err.message + '</p>').removeClass('hide'); + } + modal.modal('hide'); + modal.on('hidden.bs.modal', function () { + ajaxify.refresh(); + }); + app.alertSuccess('User created!'); }); } var timeoutId = 0; - $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + '"]').parent().addClass('active'); - - $('#search-user-name, #search-user-email, #search-user-ip').on('keyup', function() { + $('#search-user-name, #search-user-email, #search-user-ip').on('keyup', function () { if (timeoutId !== 0) { clearTimeout(timeoutId); timeoutId = 0; @@ -235,32 +327,34 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) var $this = $(this); var type = $this.attr('data-search-type'); - timeoutId = setTimeout(function() { + timeoutId = setTimeout(function () { $('.fa-spinner').removeClass('hidden'); - socket.emit('admin.user.search', {searchBy: type, query: $this.val()}, function(err, data) { + socket.emit('admin.user.search', {searchBy: type, query: $this.val()}, function (err, data) { if (err) { return app.alertError(err.message); } - templates.parse('admin/manage/users', 'users', data, function(html) { - $('#users-container').html(html); - + templates.parse('admin/manage/users', 'users', data, function (html) { + html = $(html); + $('.users-table tr').not(':first').remove(); + $('.users-table tr').first().after(html); + html.find('.timeago').timeago(); $('.fa-spinner').addClass('hidden'); if (data && data.users.length === 0) { $('#user-notfound-notify').html('User not found!') - .show() + .removeClass('hide') .addClass('label-danger') .removeClass('label-success'); } else { $('#user-notfound-notify').html(data.users.length + ' user' + (data.users.length > 1 ? 's' : '') + ' found! Search took ' + data.timing + ' ms.') - .show() + .removeClass('hide') .addClass('label-success') .removeClass('label-danger'); } - selectable.enable('#users-container', '.user-selectable'); + }); }); }, 250); @@ -268,7 +362,27 @@ define('admin/manage/users', ['admin/modules/selectable'], function(selectable) handleUserCreate(); + handleInvite(); + }; + function handleInvite() { + $('[component="user/invite"]').on('click', function () { + bootbox.prompt('Email: ', function (email) { + if (!email) { + return; + } + + socket.emit('user.invite', email, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('An invitation email has been sent to ' + email); + }); + }); + }); + } + + return Users; }); diff --git a/public/src/admin/modules/colorpicker.js b/public/src/admin/modules/colorpicker.js index 6a0054ae85..47e129249a 100644 --- a/public/src/admin/modules/colorpicker.js +++ b/public/src/admin/modules/colorpicker.js @@ -2,22 +2,22 @@ /*globals define*/ -define('admin/modules/colorpicker', function() { +define('admin/modules/colorpicker', function () { var colorpicker = {}; - colorpicker.enable = function(inputEl, callback) { - (inputEl instanceof jQuery ? inputEl : $(inputEl)).each(function() { + colorpicker.enable = function (inputEl, callback) { + (inputEl instanceof jQuery ? inputEl : $(inputEl)).each(function () { var $this = $(this); $this.ColorPicker({ color: $this.val() || '#000', - onChange: function(hsb, hex) { + onChange: function (hsb, hex) { $this.val('#' + hex); if (typeof callback === 'function') { callback(hsb, hex); } }, - onShow: function(colpkr) { + onShow: function (colpkr) { $(colpkr).css('z-index', 1051); } }); diff --git a/public/src/admin/modules/instance.js b/public/src/admin/modules/instance.js index edad60b80e..d5c2164155 100644 --- a/public/src/admin/modules/instance.js +++ b/public/src/admin/modules/instance.js @@ -2,10 +2,10 @@ /*globals define, app, socket*/ -define('admin/modules/instance', function() { +define('admin/modules/instance', function () { var instance = {}; - instance.reload = function(callback) { + instance.reload = function (callback) { app.alert({ alert_id: 'instance_reload', type: 'info', @@ -14,7 +14,7 @@ define('admin/modules/instance', function() { timeout: 5000 }); - socket.emit('admin.reload', function(err) { + socket.emit('admin.reload', function (err) { if (!err) { app.alert({ alert_id: 'instance_reload', @@ -38,7 +38,7 @@ define('admin/modules/instance', function() { }); }; - instance.restart = function(callback) { + instance.restart = function (callback) { app.alert({ alert_id: 'instance_restart', type: 'info', @@ -47,7 +47,7 @@ define('admin/modules/instance', function() { timeout: 5000 }); - $(window).one('action:reconnected', function() { + $(window).one('action:reconnected', function () { app.alert({ alert_id: 'instance_restart', type: 'success', diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index ae15ad0064..b639034ebd 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -1,11 +1,11 @@ "use strict"; /*globals define, admin, ajaxify, RELATIVE_PATH*/ -define(function() { +define(function () { var search = {}, searchIndex; - search.init = function() { + search.init = function () { $.getJSON(RELATIVE_PATH + '/templates/indexed.json', function (data) { searchIndex = data; for (var file in searchIndex) { @@ -31,17 +31,17 @@ define(function() { input = $('#acp-search input'), firstResult = null; - input.on('keyup', function() { + input.on('keyup', function () { $('#acp-search .dropdown').addClass('open'); }); - $('#acp-search').parents('form').on('submit', function(ev) { + $('#acp-search').parents('form').on('submit', function (ev) { var input = $(this).find('input'), href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val(); ajaxify.go(href.replace(/^\//, '')); - setTimeout(function() { + setTimeout(function () { $('#acp-search .dropdown').removeClass('open'); $(input).blur(); }, 150); @@ -50,11 +50,11 @@ define(function() { return false; }); - $('#main-menu a').each(function(idx, link) { + $('#main-menu a').each(function (idx, link) { routes.push($(link).attr('href')); }); - input.on('keyup focus', function() { + input.on('keyup focus', function () { var $input = $(this), value = $input.val().toLowerCase(), menuItems = $('#acp-search .dropdown-menu').html(''); diff --git a/public/src/admin/modules/selectable.js b/public/src/admin/modules/selectable.js index 81b4fcf06f..6964f74924 100644 --- a/public/src/admin/modules/selectable.js +++ b/public/src/admin/modules/selectable.js @@ -2,77 +2,12 @@ /*globals define*/ -define('admin/modules/selectable', function() { +define('admin/modules/selectable', ['jqueryui'], function (jqueryui) { var selectable = {}; - // modified from http://threedubmedia.com/code/event/drop/demo/selection - selectable.enable = function(parentElement, elementsToSelect, events) { - function selected(element) { - var $element = $(element).toggleClass('selected'); - - if (events && typeof events.onSelected === 'function') { - events.onSelected($element); - } - } - - function unselected(element) { - var $element = $(element).removeClass('selected'); - - if (events && typeof events.onUnselected === 'function') { - events.onUnselected($element); - } - } - - parentElement = $(parentElement); - elementsToSelect = $(elementsToSelect).not('.selection'); - - var offset = parentElement.offset(); - - parentElement - .addClass('selectable') - .on('mousedown', function(ev) { - if (!ev.shiftKey) { - unselected(elementsToSelect); - } - }) - .drag('start',function(ev, dd) { - if (!ev.shiftKey) { - unselected(elementsToSelect); - } - - return $('<div class="selector" />') - .css('opacity', 0.65 ) - .appendTo(parentElement); - }) - .drag(function(ev, dd){ - $(dd.proxy).css({ - top: Math.min(ev.pageY - offset.top, dd.startY - offset.top), - left: Math.min(ev.pageX - offset.left, dd.startX - offset.left), - height: Math.abs(ev.pageY - dd.startY), - width: Math.abs(ev.pageX - dd.startX) - }); - }) - .drag('end',function(ev, dd){ - $(dd.proxy).remove(); - }); - - elementsToSelect - .addClass('selection') - .on('mouseup', function(ev) { - selected(this); - }) - .drop('start',function(){ - $(this).addClass('active'); - }) - .drop(function( ev, dd ){ - selected(this); - }) - .drop('end',function(){ - $(this).removeClass('active'); - }); - - $.drop({ - multi: true + selectable.enable = function (containerEl, targets) { + $(containerEl).selectable({ + filter: targets }); }; diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index c4b880f761..ce0a17933f 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -1,19 +1,16 @@ -"use strict"; -/*global define, app, socket, ajaxify, RELATIVE_PATH */ +'use strict'; +/*global define, app, socket, ajaxify */ -define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { +define('admin/settings', ['uploader'], function (uploader) { var Settings = {}; - Settings.init = function() { - if (!app.config) { - $(window).on('action:config.loaded', Settings.prepare); - } else { - Settings.prepare(); - } + Settings.init = function () { + console.warn('[deprecation] require(\'admin/settings\').init() has been deprecated, please call require(\'admin/settings\').prepare() directly instead.'); + Settings.prepare(); }; - Settings.populateTOC = function() { - $('.settings-header').each(function() { + Settings.populateTOC = function () { + $('.settings-header').each(function () { var header = $(this).text(), anchor = header.toLowerCase().replace(/ /g, '-').trim(); @@ -22,7 +19,7 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { }); }; - Settings.prepare = function(callback) { + Settings.prepare = function (callback) { // Populate the fields on the page from the config var fields = $('#content [data-field]'), numFields = fields.length, @@ -30,6 +27,12 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { revertBtn = $('#revert'), x, key, inputType, field; + // Handle unsaved changes + $(fields).on('change', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + for (x = 0; x < numFields; x++) { field = fields.eq(x); key = field.attr('data-field'); @@ -53,21 +56,21 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { } } } else if (field.is('textarea')) { - if (app.config[key]) { + if (app.config.hasOwnProperty(key)) { field.val(app.config[key]); } } else if (field.is('select')) { - if (app.config[key]) { + if (app.config.hasOwnProperty(key)) { field.val(app.config[key]); } } } - revertBtn.off('click').on('click', function(e) { + revertBtn.off('click').on('click', function () { ajaxify.refresh(); }); - saveBtn.off('click').on('click', function(e) { + saveBtn.off('click').on('click', function (e) { e.preventDefault(); saveFields(fields, function onFieldsSaved(err) { @@ -80,6 +83,9 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { type: 'danger' }); } + + app.flags._unsaved = false; + app.alert({ alert_id: 'config_status', timeout: 2500, @@ -87,13 +93,15 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { message: 'Your changes to the NodeBB configuration have been saved.', type: 'success' }); + + $(window).trigger('action:admin.settingsSaved'); }); }); handleUploads(); - $('#clear-sitemap-cache').off('click').on('click', function() { - socket.emit('admin.settings.clearSitemapCache', function() { + $('#clear-sitemap-cache').off('click').on('click', function () { + socket.emit('admin.settings.clearSitemapCache', function () { app.alertSuccess('Sitemap Cache Cleared!'); }); return false; @@ -103,13 +111,15 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { callback(); } - $(window).trigger('action:admin.settingsLoaded'); + setTimeout(function () { + $(window).trigger('action:admin.settingsLoaded'); + }, 0); }; function handleUploads() { - $('#content input[data-action="upload"]').each(function() { + $('#content input[data-action="upload"]').each(function () { var uploadBtn = $(this); - uploadBtn.on('click', function() { + uploadBtn.on('click', function () { uploader.show({ title: uploadBtn.attr('data-title'), description: uploadBtn.attr('data-description'), @@ -117,7 +127,7 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { params: {}, showHelp: uploadBtn.attr('data-help') ? uploadBtn.attr('data-help') === 1 : undefined, accept: uploadBtn.attr('data-accept') - }, function(image) { + }, function (image) { // need to move these into template, ex data-callback if (ajaxify.currentPage === 'admin/general/sounds') { ajaxify.refresh(); @@ -129,14 +139,14 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { }); } - Settings.remove = function(key) { + Settings.remove = function (key) { socket.emit('admin.config.remove', key); }; function saveFields(fields, callback) { var data = {}; - fields.each(function() { + fields.each(function () { var field = $(this); var key = field.attr('data-field'), value, inputType; @@ -163,16 +173,14 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { data[key] = value; }); - socket.emit('admin.config.setMultiple', data, function(err) { + socket.emit('admin.config.setMultiple', data, function (err) { if (err) { return callback(err); } - if (app.config) { - for(var field in data) { - if (data.hasOwnProperty(field)) { - app.config[field] = data[field]; - } + for(var field in data) { + if (data.hasOwnProperty(field)) { + app.config[field] = data[field]; } } diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js index a63546f0ba..d296c8aa25 100644 --- a/public/src/admin/settings/email.js +++ b/public/src/admin/settings/email.js @@ -1,18 +1,23 @@ "use strict"; /* global define, socket, app, ajaxify, ace */ -define('admin/settings/email', ['admin/settings'], function(settings) { +define('admin/settings/email', ['admin/settings'], function (settings) { var module = {}, emailEditor; - module.init = function() { + module.init = function () { configureEmailTester(); configureEmailEditor(); + + $(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange); + $(window).on('action:admin.settingsSaved', function () { + socket.emit('admin.user.restartJobs'); + }); }; function configureEmailTester() { - $('button[data-action="email.test"]').off('click').on('click', function() { - socket.emit('admin.email.test', {template: $('#test-email').val()}, function(err) { + $('button[data-action="email.test"]').off('click').on('click', function () { + socket.emit('admin.email.test', {template: $('#test-email').val()}, function (err) { if (err) { return app.alertError(err.message); } @@ -29,16 +34,23 @@ define('admin/settings/email', ['admin/settings'], function(settings) { emailEditor.setTheme("ace/theme/twilight"); emailEditor.getSession().setMode("ace/mode/html"); - emailEditor.on('change', function(e) { - $('#email-editor-holder').val(emailEditor.getValue()); + emailEditor.on('change', function () { + var emailPath = $('#email-editor-selector').val(); + var original; + ajaxify.data.emails.forEach(function (email) { + if (email.path === emailPath) { + original = email.original; + } + }); + var newEmail = emailEditor.getValue(); + $('#email-editor-holder').val(newEmail !== original ? newEmail : ''); }); - $('button[data-action="email.revert"]').off('click').on('click', function() { - ajaxify.data.emails.forEach(function(email) { + $('button[data-action="email.revert"]').off('click').on('click', function () { + ajaxify.data.emails.forEach(function (email) { if (email.path === $('#email-editor-selector').val()) { emailEditor.getSession().setValue(email.original); - $('#email-editor-holder') - .val(email.original); + $('#email-editor-holder').val(''); } }); }); @@ -47,15 +59,44 @@ define('admin/settings/email', ['admin/settings'], function(settings) { } function updateEmailEditor() { - ajaxify.data.emails.forEach(function(email) { + ajaxify.data.emails.forEach(function (email) { if (email.path === $('#email-editor-selector').val()) { emailEditor.getSession().setValue(email.text); $('#email-editor-holder') - .val(email.text) + .val(email.text !== email.original ? email.text : '') .attr('data-field', 'email:custom:' + email.path); } }); } + function handleDigestHourChange() { + var hour = parseInt($('#digestHour').val(), 10); + + if (isNaN(hour)) { + hour = 17; + } else if (hour > 23 || hour < 0) { + hour = 0; + } + + socket.emit('meta.getServerTime', {}, function (err, now) { + if (err) { + return app.alertError(err.message); + } + + now = new Date(now); + + $('#serverTime').text(now.toString()); + + now.setHours(parseInt(hour, 10), 0, 0, 0); + + // If adjusted time is in the past, move to next day + if (now.getTime() < Date.now()) { + now.setDate(now.getDate() + 1); + } + + $('#nextDigestTime').text(now.toString()); + }); + } + return module; -}); \ No newline at end of file +}); diff --git a/public/src/admin/settings/general.js b/public/src/admin/settings/general.js index 83ecff7907..fc0d7d540f 100644 --- a/public/src/admin/settings/general.js +++ b/public/src/admin/settings/general.js @@ -1,19 +1,22 @@ "use strict"; /* global define, socket */ -define('admin/settings/general', ['admin/settings'], function(Settings) { +define('admin/settings/general', ['admin/settings'], function (Settings) { var Module = {} - Module.init = function() { - $('button[data-action="removeLogo"]').on('click', function() { + Module.init = function () { + $('button[data-action="removeLogo"]').on('click', function () { $('input[data-field="brand:logo"]').val(''); }); - $('button[data-action="removeFavicon"]').on('click', function() { + $('button[data-action="removeFavicon"]').on('click', function () { $('input[data-field="brand:favicon"]').val(''); }); - $('button[data-action="removeTouchIcon"]').on('click', function() { + $('button[data-action="removeTouchIcon"]').on('click', function () { $('input[data-field="brand:touchIcon"]').val(''); }); + $('button[data-action="removeOgImage"]').on('click', function () { + $('input[data-field="removeOgImage"]').val(''); + }); }; return Module; diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index b72447a980..955acab7bb 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -1,21 +1,20 @@ "use strict"; +/*global app, bootbox, templates, socket, config, RELATIVE_PATH*/ var ajaxify = ajaxify || {}; -$(document).ready(function() { - - /*global app, templates, socket, config, RELATIVE_PATH*/ - +$(document).ready(function () { var location = document.location || window.location; var rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''); var apiXHR = null; var translator; var retry = true; + var previousBodyClass = ''; // Dumb hack to fool ajaxify into thinking translator is still a global // When ajaxify is migrated to a require.js module, then this can be merged into the "define" call - require(['translator'], function(_translator) { + require(['translator'], function (_translator) { translator = _translator; }); @@ -28,7 +27,7 @@ $(document).ready(function() { url: ev.state.returnPath }, ev.state.returnPath, config.relative_path + '/' + ev.state.returnPath); } else if (ev.state.url !== undefined) { - ajaxify.go(ev.state.url, function() { + ajaxify.go(ev.state.url, function () { $(window).trigger('action:popstate', {url: ev.state.url}); }, true); } @@ -42,7 +41,7 @@ $(document).ready(function() { if (ajaxify.reconnectAction) { $(window).off('action:reconnected', ajaxify.reconnectAction); } - ajaxify.reconnectAction = function(e) { + ajaxify.reconnectAction = function (e) { ajaxify.go(url, callback, quiet); $(window).off(e); }; @@ -61,19 +60,35 @@ $(document).ready(function() { apiXHR.abort(); } - url = ajaxify.start(url, quiet); + if (!window.location.pathname.match(/\/(403|404)$/g)) { + app.previousUrl = window.location.href; + } + + url = ajaxify.start(url); - $('body').removeClass(ajaxify.data.bodyClass); + // If any listeners alter url and set it to an empty string, abort the ajaxification + if (url === null) { + $(window).trigger('action:ajaxify.end', {url: url, tpl_url: ajaxify.data.template.name, title: ajaxify.data.title}); + return false; + } + + previousBodyClass = ajaxify.data.bodyClass; $('#footer, #content').removeClass('hide').addClass('ajaxifying'); - ajaxify.loadData(url, function(err, data) { + ajaxify.loadData(url, function (err, data) { + + if (!err || (err && err.data && (parseInt(err.data.status, 10) !== 302 && parseInt(err.data.status, 10) !== 308))) { + ajaxify.updateHistory(url, quiet); + } + if (err) { return onAjaxError(err, url, callback, quiet); } + retry = true; app.template = data.template.name; - require(['translator'], function(translator) { + require(['translator'], function (translator) { translator.load(config.defaultLang, data.template.name); renderTemplate(url, data.template.name, data, callback); }); @@ -82,11 +97,12 @@ $(document).ready(function() { return true; }; - ajaxify.handleRedirects = function(url) { + ajaxify.handleRedirects = function (url) { url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase(); - var isAdminRoute = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0; + var isClientToAdmin = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0; + var isAdminToClient = !url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') === 0; var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api'); - if (isAdminRoute || uploadsOrApi) { + if (isClientToAdmin || isAdminToClient || uploadsOrApi) { window.open(RELATIVE_PATH + '/' + url, '_top'); return true; } @@ -94,23 +110,25 @@ $(document).ready(function() { }; - ajaxify.start = function(url, quiet) { + ajaxify.start = function (url) { url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')); - $(window).trigger('action:ajaxify.start', {url: url}); - - if (!window.location.pathname.match(/\/(403|404)$/g)) { - app.previousUrl = window.location.href; + var payload = { + url: url } - ajaxify.currentPage = url.split(/[?#]/)[0]; + $(window).trigger('action:ajaxify.start', payload); + + return payload.url; + }; + ajaxify.updateHistory = function (url, quiet) { + ajaxify.currentPage = url.split(/[?#]/)[0]; if (window.history && window.history.pushState) { window.history[!quiet ? 'pushState' : 'replaceState']({ url: url }, url, RELATIVE_PATH + '/' + url); } - return url; }; function onAjaxError(err, url, callback, quiet) { @@ -136,9 +154,10 @@ $(document).ready(function() { } else if (status === 401) { app.alertError('[[global:please_log_in]]'); app.previousUrl = url; - return ajaxify.go('login'); + window.location.href = config.relative_path + '/login'; + return; } else if (status === 302 || status === 308) { - if (data.responseJSON.external) { + if (data.responseJSON && data.responseJSON.external) { window.location.href = data.responseJSON.external; } else if (typeof data.responseJSON === 'string') { ajaxify.go(data.responseJSON.slice(1), callback, quiet); @@ -152,10 +171,10 @@ $(document).ready(function() { function renderTemplate(url, tpl_url, data, callback) { $(window).trigger('action:ajaxify.loadingTemplates', {}); - templates.parse(tpl_url, data, function(template) { - translator.translate(template, function(translatedTemplate) { + templates.parse(tpl_url, data, function (template) { + translator.translate(template, function (translatedTemplate) { translatedTemplate = translator.unescape(translatedTemplate); - $('body').addClass(data.bodyClass); + $('body').removeClass(previousBodyClass).addClass(data.bodyClass); $('#content').html(translatedTemplate); ajaxify.end(url, tpl_url); @@ -171,7 +190,7 @@ $(document).ready(function() { }); } - ajaxify.end = function(url, tpl_url) { + ajaxify.end = function (url, tpl_url) { function done() { if (--count === 0) { $(window).trigger('action:ajaxify.end', {url: url, tpl_url: tpl_url, title: ajaxify.data.title}); @@ -179,8 +198,6 @@ $(document).ready(function() { } var count = 2; - ajaxify.variables.parse(); - ajaxify.loadScript(tpl_url, done); ajaxify.widgets.render(tpl_url, url, done); @@ -190,22 +207,26 @@ $(document).ready(function() { app.processPage(); }; - ajaxify.removeRelativePath = function(url) { + ajaxify.parseData = function () { + var dataEl = $('#ajaxify-data'); + if (dataEl.length) { + ajaxify.data = JSON.parse(dataEl.text()); + dataEl.remove(); + } + }; + + ajaxify.removeRelativePath = function (url) { if (url.startsWith(RELATIVE_PATH.slice(1))) { url = url.slice(RELATIVE_PATH.length); } return url; }; - ajaxify.refresh = function(e, callback) { - if (e && e instanceof jQuery.Event) { - e.preventDefault(); - } - + ajaxify.refresh = function (callback) { ajaxify.go(ajaxify.currentPage + window.location.search + window.location.hash, callback, true); }; - ajaxify.loadScript = function(tpl_url, callback) { + ajaxify.loadScript = function (tpl_url, callback) { var location = !app.inAdmin ? 'forum/' : ''; if (tpl_url.startsWith('admin')) { @@ -218,7 +239,7 @@ $(document).ready(function() { $(window).trigger('action:script.load', data); - require(data.scripts, function(script) { + require(data.scripts, function (script) { if (script && script.init) { script.init(); } @@ -229,7 +250,7 @@ $(document).ready(function() { }); }; - ajaxify.loadData = function(url, callback) { + ajaxify.loadData = function (url, callback) { url = ajaxify.removeRelativePath(url); $(window).trigger('action:ajaxify.loadingData', {url: url}); @@ -237,7 +258,7 @@ $(document).ready(function() { apiXHR = $.ajax({ url: RELATIVE_PATH + '/api/' + url, cache: false, - success: function(data) { + success: function (data) { if (!data) { return; } @@ -249,7 +270,7 @@ $(document).ready(function() { callback(null, data); }, - error: function(data, textStatus) { + error: function (data, textStatus) { if (data.status === 0 && textStatus === 'error') { data.status = 500; } @@ -261,17 +282,17 @@ $(document).ready(function() { }); }; - ajaxify.loadTemplate = function(template, callback) { + ajaxify.loadTemplate = function (template, callback) { if (templates.cache[template]) { callback(templates.cache[template]); } else { $.ajax({ url: RELATIVE_PATH + '/templates/' + template + '.tpl' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), type: 'GET', - success: function(data) { + success: function (data) { callback(data.toString()); }, - error: function(error) { + error: function (error) { throw new Error("Unable to load template: " + template + " (" + error.statusText + ")"); } }); @@ -283,15 +304,41 @@ $(document).ready(function() { return href === undefined || href === '' || href === 'javascript:;'; } + var contentEl = document.getElementById('content'); + // Enhancing all anchors to ajaxify... $(document.body).on('click', 'a', function (e) { + var _self = this; + var process = function () { + if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) { + if (internalLink) { + var pathname = this.href.replace(rootUrl + RELATIVE_PATH + '/', ''); + + // Special handling for urls with hashes + if (window.location.pathname === this.pathname && this.hash.length) { + window.location.hash = this.hash; + } else { + if (ajaxify.go(pathname)) { + e.preventDefault(); + } + } + } else if (window.location.pathname !== '/outgoing') { + if (config.openOutgoingLinksInNewTab && $.contains(contentEl, this)) { + window.open(this.href, '_blank'); + e.preventDefault(); + } else if (config.useOutgoingLinksPage) { + ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); + e.preventDefault(); + } + } + } + }; + if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) { return; } - var internalLink = this.host === '' || // Relative paths are always internal links - (this.host === window.location.host && this.protocol === window.location.protocol && // Otherwise need to check if protocol and host match - (RELATIVE_PATH.length > 0 ? this.pathname.indexOf(RELATIVE_PATH) === 0 : true)); // Subfolder installs need this additional check + var internalLink = utils.isInternalURI(this, window.location, RELATIVE_PATH); if ($(this).attr('data-ajaxify') === 'false') { if (!internalLink) { @@ -301,32 +348,28 @@ $(document).ready(function() { } } + // Default behaviour for rss feeds + if (internalLink && $(this).attr('href').endsWith('.rss')) { + return; + } + if (hrefEmpty(this.href) || this.protocol === 'javascript:' || $(this).attr('href') === '#') { return e.preventDefault(); } - if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) { - if (internalLink) { - var pathname = this.href.replace(rootUrl + RELATIVE_PATH + '/', ''); - - // Special handling for urls with hashes - if (window.location.pathname === this.pathname && this.hash.length) { - window.location.hash = this.hash; - } else { - if (ajaxify.go(pathname)) { - e.preventDefault(); + if (app.flags && app.flags.hasOwnProperty('_unsaved') && app.flags._unsaved === true) { + translator.translate('[[global:unsaved-changes]]', function (text) { + bootbox.confirm(text, function (navigate) { + if (navigate) { + app.flags._unsaved = false; + process.call(_self); } - } - } else if (window.location.pathname !== '/outgoing') { - if (config.openOutgoingLinksInNewTab) { - window.open(this.href, '_blank'); - e.preventDefault(); - } else if (config.useOutgoingLinksPage) { - ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); - e.preventDefault(); - } - } + }); + }); + return e.preventDefault(); } + + process.call(_self); }); } @@ -339,8 +382,9 @@ $(document).ready(function() { app.load(); - $('[data-template]').each(function() { - templates.cache[$(this).attr('data-template')] = $(this).html(); + $('[type="text/tpl"][data-template]').each(function () { + templates.cache[$(this).attr('data-template')] = $('<div/>').html($(this).html()).text(); + $(this).parent().remove(); }); }); \ No newline at end of file diff --git a/public/src/app.js b/public/src/app.js index 791aabd628..442e57f373 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -15,18 +15,16 @@ app.cacheBuster = null; app.cacheBuster = config['cache-buster']; - require(['csrf'], function(csrf) { - csrf.set(config.csrf_token); - }); - bootbox.setDefaults({ locale: config.userLang }); - app.load = function() { + app.load = function () { app.loadProgressiveStylesheet(); - var url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash, true); + var url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash); + ajaxify.updateHistory(url, true); + ajaxify.parseData(); ajaxify.end(url, app.template); handleStatusChange(); @@ -35,15 +33,15 @@ app.cacheBuster = null; app.handleSearch(); } - $('#content').on('click', '#new_topic', function(){ + $('body').on('click', '#new_topic', function (){ app.newTopic(); }); - require(['components'], function(components) { + require(['components'], function (components) { components.get('user/logout').on('click', app.logout); }); - Visibility.change(function(e, state){ + Visibility.change(function (event, state){ if (state === 'visible') { app.isFocused = true; app.alternatingTitle(''); @@ -58,15 +56,15 @@ app.cacheBuster = null; app.showEmailConfirmWarning(); socket.removeAllListeners('event:nodebb.ready'); - socket.on('event:nodebb.ready', function(data) { - if (!app.cacheBusters || app.cacheBusters['cache-buster'] !== data['cache-buster']) { - app.cacheBusters = data; + socket.on('event:nodebb.ready', function (data) { + if (!app.cacheBuster || app.cacheBuster !== data['cache-buster']) { + app.cacheBuster = data['cache-buster']; app.alert({ alert_id: 'forum_updated', title: '[[global:updated.title]]', message: '[[global:updated.message]]', - clickfn: function() { + clickfn: function () { window.location.reload(); }, type: 'warning' @@ -74,7 +72,7 @@ app.cacheBuster = null; } }); - require(['taskbar', 'helpers', 'forum/pagination'], function(taskbar, helpers, pagination) { + require(['taskbar', 'helpers', 'forum/pagination'], function (taskbar, helpers, pagination) { taskbar.init(); // templates.js helpers @@ -86,28 +84,32 @@ app.cacheBuster = null; }); }; - app.logout = function() { - require(['csrf'], function(csrf) { - $.ajax(config.relative_path + '/logout', { - type: 'POST', - headers: { - 'x-csrf-token': csrf.get() - }, - success: function() { - window.location.href = config.relative_path + '/'; - } - }); + app.logout = function () { + $(window).trigger('action:app.logout'); + $.ajax(config.relative_path + '/logout', { + type: 'POST', + headers: { + 'x-csrf-token': config.csrf_token + }, + success: function () { + var payload = { + next: config.relative_path + '/' + }; + + $(window).trigger('action:app.loggedOut', payload); + window.location.href = payload.next; + } }); }; app.alert = function (params) { - require(['alerts'], function(alerts) { + require(['alerts'], function (alerts) { alerts.alert(params); }); }; - app.removeAlert = function(id) { - require(['alerts'], function(alerts) { + app.removeAlert = function (id) { + require(['alerts'], function (alerts) { alerts.remove(id); }); }; @@ -117,39 +119,68 @@ app.cacheBuster = null; title: '[[global:alert.success]]', message: message, type: 'success', - timeout: timeout ? timeout : 2000 + timeout: timeout ? timeout : 5000 }); }; app.alertError = function (message, timeout) { + if (message === '[[error:invalid-session]]') { + return app.handleInvalidSession(); + } + app.alert({ title: '[[global:alert.error]]', message: message, type: 'danger', - timeout: timeout ? timeout : 5000 + timeout: timeout ? timeout : 10000 + }); + }; + + app.handleInvalidSession = function () { + if (app.flags && app.flags._sessionRefresh) { + return; + } + + app.flags = app.flags || {}; + app.flags._sessionRefresh = true; + + require(['translator'], function (translator) { + translator.translate('[[error:invalid-session-text]]', function (translated) { + bootbox.alert({ + title: '[[error:invalid-session]]', + message: translated, + closeButton: false, + callback: function () { + window.location.reload(); + } + }); + }); }); }; app.enterRoom = function (room, callback) { - callback = callback || function() {}; + callback = callback || function () {}; if (socket && app.user.uid && app.currentRoom !== room) { + var previousRoom = app.currentRoom; + app.currentRoom = room; socket.emit('meta.rooms.enter', { enter: room - }, function(err) { + }, function (err) { if (err) { + app.currentRoom = previousRoom; return app.alertError(err.message); } - app.currentRoom = room; + callback(); }); } }; - app.leaveCurrentRoom = function() { + app.leaveCurrentRoom = function () { if (!socket) { return; } - socket.emit('meta.rooms.leaveCurrent', function(err) { + socket.emit('meta.rooms.leaveCurrent', function (err) { if (err) { return app.alertError(err.message); } @@ -165,19 +196,19 @@ app.cacheBuster = null; } } - app.createUserTooltips = function(els) { + app.createUserTooltips = function (els, placement) { els = els || $('body'); - els.find('.avatar,img[title].teaser-pic,img[title].user-img,div.user-icon,span.user-icon').each(function() { + els.find('.avatar,img[title].teaser-pic,img[title].user-img,div.user-icon,span.user-icon').each(function () { if (!utils.isTouchDevice()) { $(this).tooltip({ - placement: 'top', + placement: placement || $(this).attr('title-placement') || 'top', title: $(this).attr('title') }); } }); }; - app.createStatusTooltips = function() { + app.createStatusTooltips = function () { if (!utils.isTouchDevice()) { $('body').tooltip({ selector:'.fa-circle.status', @@ -186,9 +217,9 @@ app.cacheBuster = null; } }; - app.replaceSelfLinks = function(selector) { + app.replaceSelfLinks = function (selector) { selector = selector || $('a'); - selector.each(function() { + selector.each(function () { var href = $(this).attr('href'); if (href && app.user.userslug && href.indexOf('user/_self_') !== -1) { $(this).attr('href', href.replace(/user\/_self_/g, 'user/' + app.user.userslug)); @@ -235,7 +266,7 @@ app.cacheBuster = null; } }; - app.openChat = function (roomId) { + app.openChat = function (roomId, uid) { if (!app.user.uid) { return app.alertError('[[error:not-logged-in]]'); } @@ -250,33 +281,38 @@ app.cacheBuster = null; if (chat.modalExists(roomId)) { loadAndCenter(chat.getModal(roomId)); } else { - socket.emit('modules.chats.loadRoom', {roomId: roomId}, function(err, roomData) { + socket.emit('modules.chats.loadRoom', {roomId: roomId, uid: uid || app.user.uid}, function (err, roomData) { if (err) { return app.alertError(err.message); } - roomData.users = roomData.users.filter(function(user) { + roomData.users = roomData.users.filter(function (user) { return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); }); + roomData.uid = uid || app.user.uid; chat.createModal(roomData, loadAndCenter); }); } }); }; - app.newChat = function (touid) { + app.newChat = function (touid, callback) { + callback = callback || function () {}; if (!app.user.uid) { return app.alertError('[[error:not-logged-in]]'); } - socket.emit('modules.chats.newRoom', {touid: touid}, function(err, roomId) { + socket.emit('modules.chats.newRoom', {touid: touid}, function (err, roomId) { if (err) { return app.alertError(err.message); } + if (!ajaxify.currentPage.startsWith('chats')) { app.openChat(roomId); } else { ajaxify.go('chats/' + roomId); } + + callback(false, roomId); }); }; @@ -296,14 +332,14 @@ app.cacheBuster = null; titleObj.titles[0] = window.document.title; } - require(['translator'], function(translator) { - translator.translate(title, function(translated) { + require(['translator'], function (translator) { + translator.translate(title, function (translated) { titleObj.titles[1] = translated; if (titleObj.interval) { clearInterval(titleObj.interval); } - titleObj.interval = setInterval(function() { + titleObj.interval = setInterval(function () { var title = titleObj.titles[titleObj.titles.indexOf(window.document.title) ^ 1]; if (title) { window.document.title = $('<div/>').html(title).text(); @@ -321,23 +357,23 @@ app.cacheBuster = null; } }; - app.refreshTitle = function(title) { + app.refreshTitle = function (title) { if (!title) { return; } - require(['translator'], function(translator) { + require(['translator'], function (translator) { title = config.titleLayout.replace(/{/g, '{').replace(/}/g, '}') - .replace('{pageTitle}', function() { return title; }) - .replace('{browserTitle}', function() { return config.browserTitle; }); + .replace('{pageTitle}', function () { return title; }) + .replace('{browserTitle}', function () { return config.browserTitle; }); - translator.translate(title, function(translated) { + translator.translate(title, function (translated) { titleObj.titles[0] = translated; app.alternatingTitle(''); }); }); }; - app.toggleNavbar = function(state) { + app.toggleNavbar = function (state) { var navbarEl = $('.navbar'); if (navbarEl) { navbarEl.toggleClass('hidden', !!!state); @@ -349,10 +385,11 @@ app.cacheBuster = null; if (env === 'xs' || env === 'sm') { return; } - $('#header-menu li a[title]').each(function() { + $('#header-menu li a[title]').each(function () { if (!utils.isTouchDevice()) { $(this).tooltip({ placement: 'bottom', + trigger: 'hover', title: $(this).attr('title') }); } @@ -361,6 +398,7 @@ app.cacheBuster = null; if (!utils.isTouchDevice()) { $('#search-form').parent().tooltip({ placement: 'bottom', + trigger: 'hover', title: $('#search-button i').attr('title') }); } @@ -368,6 +406,7 @@ app.cacheBuster = null; if (!utils.isTouchDevice()) { $('#user_dropdown').tooltip({ placement: 'bottom', + trigger: 'hover', title: $('#user_dropdown').attr('title') }); } @@ -378,7 +417,7 @@ app.cacheBuster = null; searchFields = $("#search-fields"), searchInput = $('#search-fields input'); - $('#search-form .advanced-search-link').on('mousedown', function() { + $('#search-form .advanced-search-link').on('mousedown', function () { ajaxify.go('/search'); }); @@ -390,7 +429,7 @@ app.cacheBuster = null; searchButton.removeClass('hidden'); } - searchButton.on('click', function(e) { + searchButton.on('click', function (e) { if (!config.loggedIn && !config.allowGuestSearching) { app.alert({ message:'[[error:search-requires-login]]', @@ -407,8 +446,10 @@ app.cacheBuster = null; $('#search-form').on('submit', function () { var input = $(this).find('input'); - require(['search'], function(search) { - search.query({term: input.val()}, function() { + require(['search'], function (search) { + var data = search.getSearchPreferences(); + data.term = input.val(); + search.query(data, function () { input.val(''); }); }); @@ -416,16 +457,16 @@ app.cacheBuster = null; }); }; - app.prepareSearch = function() { + app.prepareSearch = function () { $("#search-fields").removeClass('hidden'); $("#search-button").addClass('hidden'); $('#search-fields input').focus(); }; function handleStatusChange() { - $('[component="header/usercontrol"] [data-status]').off('click').on('click', function(e) { + $('[component="header/usercontrol"] [data-status]').off('click').on('click', function (e) { var status = $(this).attr('data-status'); - socket.emit('user.setStatus', status, function(err) { + socket.emit('user.setStatus', status, function (err) { if(err) { return app.alertError(err.message); } @@ -439,13 +480,13 @@ app.cacheBuster = null; }); } - app.updateUserStatus = function(el, status) { + app.updateUserStatus = function (el, status) { if (!el.length) { return; } - require(['translator'], function(translator) { - translator.translate('[[global:' + status + ']]', function(translated) { + require(['translator'], function (translator) { + translator.translate('[[global:' + status + ']]', function (translated) { el.removeClass('online offline dnd away') .addClass(status) .attr('title', translated) @@ -454,97 +495,89 @@ app.cacheBuster = null; }); }; - app.newTopic = function (cid) { - cid = cid || ajaxify.data.cid; - if (cid) { - $(window).trigger('action:composer.topic.new', { - cid: cid - }); - } else { - socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function(err, categories) { - if (err) { - return app.alertError(err.message); - } - categories = categories.filter(function(category) { - return !category.link && !parseInt(category.parentCid, 10); - }); - if (categories.length) { - $(window).trigger('action:composer.topic.new', { - cid: categories[0].cid - }); - } - }); - } + app.newTopic = function (cid, tags) { + $(window).trigger('action:composer.topic.new', { + cid: cid || ajaxify.data.cid || 0, + tags: tags || (ajaxify.data.tag ? [ajaxify.data.tag] : []) + }); }; - app.loadJQueryUI = function(callback) { + app.loadJQueryUI = function (callback) { if (typeof $().autocomplete === 'function') { return callback(); } - $.getScript(config.relative_path + '/vendor/jquery/js/jquery-ui-1.10.4.custom.js', callback); + var scriptEl = document.createElement('script'); + scriptEl.type = 'text/javascript'; + scriptEl.src = config.relative_path + '/vendor/jquery/js/jquery-ui.js' + (app.cacheBuster ? '?v=' + app.cacheBuster : ''); + scriptEl.onload = callback; + document.head.appendChild(scriptEl); }; - app.showEmailConfirmWarning = function(err) { + app.showEmailConfirmWarning = function (err) { if (!config.requireEmailConfirmation || !app.user.uid) { return; } + var msg = { + alert_id: 'email_confirm', + type: 'warning', + timeout: 0 + }; + 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: err ? err.message : '[[error:email-not-confirmed]]', - type: 'warning', - timeout: 0, - clickfn: function() { - app.removeAlert('email_confirm'); - socket.emit('user.emailConfirm', {}, function(err) { - if (err) { - return app.alertError(err.message); - } - app.alertSuccess('[[notifications:email-confirm-sent]]'); - }); - } - }); + msg.message = '[[error:no-email-to-confirm]]'; + msg.clickfn = function () { + app.removeAlert('email_confirm'); + ajaxify.go('user/' + app.user.userslug + '/edit'); + }; + app.alert(msg); + } else if (!app.user['email:confirmed'] && !app.user.isEmailConfirmSent) { + msg.message = err ? err.message : '[[error:email-not-confirmed]]'; + msg.clickfn = function () { + app.removeAlert('email_confirm'); + socket.emit('user.emailConfirm', {}, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[notifications:email-confirm-sent]]'); + }); + }; + + app.alert(msg); + } else if (!app.user['email:confirmed'] && app.user.isEmailConfirmSent) { + msg.message = '[[error:email-not-confirmed-email-sent]]'; + app.alert(msg); } }; - app.parseAndTranslate = function(template, blockName, data, callback) { - require(['translator'], function(translator) { + app.parseAndTranslate = function (template, blockName, data, callback) { + require(['translator'], function (translator) { + function translate(html, callback) { + translator.translate(html, function (translatedHTML) { + translatedHTML = translator.unescape(translatedHTML); + callback($(translatedHTML)); + }); + } + if (typeof blockName === 'string') { - templates.parse(template, blockName, data, function(html) { - translator.translate(html, function(translatedHTML) { - translatedHTML = translator.unescape(translatedHTML); - callback($(translatedHTML)); - }); + templates.parse(template, blockName, data, function (html) { + translate(html, callback); }); } else { - callback = data, data = blockName; - templates.parse(template, data, function(html) { - translator.translate(html, function(translatedHTML) { - translatedHTML = translator.unescape(translatedHTML); - callback($(translatedHTML)); - }); + callback = data; + data = blockName; + templates.parse(template, data, function (html) { + translate(html, callback); }); } }); }; - app.loadProgressiveStylesheet = function() { + app.loadProgressiveStylesheet = function () { var linkEl = document.createElement('link'); linkEl.rel = 'stylesheet'; linkEl.href = config.relative_path + '/js-enabled.css'; document.head.appendChild(linkEl); - } + }; }()); diff --git a/public/src/client/account/best.js b/public/src/client/account/best.js index 27384f9638..abc88620bf 100644 --- a/public/src/client/account/best.js +++ b/public/src/client/account/best.js @@ -2,10 +2,10 @@ /* globals define */ -define('forum/account/best', ['forum/account/header', 'forum/account/posts'], function(header, posts) { +define('forum/account/best', ['forum/account/header', 'forum/account/posts'], function (header, posts) { var Best = {}; - Best.init = function() { + Best.init = function () { header.init(); $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); diff --git a/public/src/client/account/bookmarks.js b/public/src/client/account/bookmarks.js new file mode 100644 index 0000000000..3f9b848b75 --- /dev/null +++ b/public/src/client/account/bookmarks.js @@ -0,0 +1,17 @@ +'use strict'; + +/* globals define */ + +define('forum/account/bookmarks', ['forum/account/header', 'forum/account/posts'], function (header, posts) { + var Bookmarks = {}; + + Bookmarks.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('posts.loadMoreBookmarks', 'account/bookmarks'); + }; + + return Bookmarks; +}); diff --git a/public/src/client/account/downvoted.js b/public/src/client/account/downvoted.js index 1a3758e215..b84e5da6bf 100644 --- a/public/src/client/account/downvoted.js +++ b/public/src/client/account/downvoted.js @@ -2,10 +2,10 @@ /* globals define */ -define('forum/account/downvoted', ['forum/account/header', 'forum/account/posts'], function(header, posts) { +define('forum/account/downvoted', ['forum/account/header', 'forum/account/posts'], function (header, posts) { var Downvoted = {}; - Downvoted.init = function() { + Downvoted.init = function () { header.init(); $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index de5b01e809..915e2b5079 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -2,18 +2,16 @@ /* globals define, ajaxify, socket, app, config, templates, bootbox */ -define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], function(header, uploader, translator) { - var AccountEdit = {}, - uploadedPicture = ''; +define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', 'components'], function (header, uploader, translator, components) { + var AccountEdit = {}; - AccountEdit.init = function() { - uploadedPicture = ajaxify.data.uploadedpicture; + AccountEdit.init = function () { header.init(); $('#submitBtn').on('click', updateProfile); - app.loadJQueryUI(function() { + app.loadJQueryUI(function () { $('#inputBirthday').datepicker({ changeMonth: true, changeYear: true, @@ -36,11 +34,12 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], website: $('#inputWebsite').val(), birthday: $('#inputBirthday').val(), location: $('#inputLocation').val(), + groupTitle: $('#groupTitle').val(), signature: $('#inputSignature').val(), aboutme: $('#inputAboutMe').val() }; - socket.emit('user.updateProfile', userData, function(err, data) { + socket.emit('user.updateProfile', userData, function (err, data) { if (err) { return app.alertError(err.message); } @@ -58,29 +57,27 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], } function updateHeader(picture) { - require(['components'], function(components) { - if (parseInt(ajaxify.data.theirid, 10) !== parseInt(ajaxify.data.yourid, 10)) { - return; - } + if (parseInt(ajaxify.data.theirid, 10) !== parseInt(ajaxify.data.yourid, 10)) { + return; + } - components.get('header/userpicture')[picture ? 'show' : 'hide'](); - components.get('header/usericon')[!picture ? 'show' : 'hide'](); - if (picture) { - components.get('header/userpicture').attr('src', picture); - } - }); + components.get('header/userpicture')[picture ? 'show' : 'hide'](); + components.get('header/usericon')[!picture ? 'show' : 'hide'](); + if (picture) { + components.get('header/userpicture').attr('src', picture); + } } function handleImageChange() { - $('#changePictureBtn').on('click', function() { - socket.emit('user.getProfilePictures', {uid: ajaxify.data.uid}, function(err, pictures) { + $('#changePictureBtn').on('click', function () { + socket.emit('user.getProfilePictures', {uid: ajaxify.data.uid}, function (err, pictures) { if (err) { return app.alertError(err.message); } // boolean to signify whether an uploaded picture is present in the pictures list - var uploaded = pictures.reduce(function(memo, cur) { + var uploaded = pictures.reduce(function (memo, cur) { return memo || cur.type === 'uploaded'; }, false); @@ -88,8 +85,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], pictures: pictures, uploaded: uploaded, allowProfileImageUploads: ajaxify.data.allowProfileImageUploads - }, function(html) { - translator.translate(html, function(html) { + }, function (html) { + translator.translate(html, function (html) { var modal = bootbox.dialog({ className: 'picture-switcher', title: '[[user:change_picture]]', @@ -127,7 +124,7 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], if (!ajaxify.data.picture) { modal.find('.list-group-item .user-icon').parents('.list-group-item').addClass('active'); } else { - modal.find('.list-group-item img').each(function() { + modal.find('.list-group-item img').each(function () { if (this.getAttribute('src') === ajaxify.data.picture) { $(this).parents('.list-group-item').addClass('active'); } @@ -136,14 +133,14 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], } function saveSelection() { - var type = modal.find('.list-group-item.active').attr('data-type'), - src = modal.find('.list-group-item.active img').attr('src'); - changeUserPicture(type, function(err) { + var type = modal.find('.list-group-item.active').attr('data-type'); + + changeUserPicture(type, function (err) { if (err) { return app.alertError(err.message); } - updateHeader(type === 'default' ? '' : src); + updateHeader(type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src')); ajaxify.refresh(); }); } @@ -160,9 +157,9 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], } function handleAccountDelete() { - $('#deleteAccountBtn').on('click', function() { - translator.translate('[[user:delete_account_confirm]]', function(translated) { - var modal = bootbox.confirm(translated + '<p><input type="text" class="form-control" id="confirm-username" /></p>', function(confirm) { + $('#deleteAccountBtn').on('click', function () { + translator.translate('[[user:delete_account_confirm]]', function (translated) { + var modal = bootbox.confirm(translated + '<p><input type="text" class="form-control" id="confirm-username" /></p>', function (confirm) { if (!confirm) { return; } @@ -171,16 +168,17 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], app.alertError('[[error:invalid-username]]'); return false; } else { - socket.emit('user.deleteAccount', {}, function(err) { + socket.emit('user.deleteAccount', {}, function (err) { if (err) { - app.alertError(err.message); + return app.alertError(err.message); } + window.location.href = config.relative_path + '/'; }); } }); - modal.on('shown.bs.modal', function() { + modal.on('shown.bs.modal', function () { modal.find('input').focus(); }); }); @@ -190,15 +188,17 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], function handleImageUpload(modal) { function onUploadComplete(urlOnServer) { - urlOnServer = urlOnServer + '?' + new Date().getTime(); + urlOnServer = urlOnServer + '?' + Date.now(); updateHeader(urlOnServer); if (ajaxify.data.picture.length) { $('#user-current-picture, img.avatar').attr('src', urlOnServer); - uploadedPicture = urlOnServer; + ajaxify.data.uploadedpicture = urlOnServer; } else { - ajaxify.refresh(); + ajaxify.refresh(function () { + $('#user-current-picture, img.avatar').attr('src', urlOnServer); + }); } } @@ -209,7 +209,7 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], } } - modal.find('[data-action="upload"]').on('click', function() { + modal.find('[data-action="upload"]').on('click', function () { modal.modal('hide'); uploader.show({ @@ -219,26 +219,26 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], title: '[[user:upload_picture]]', description: '[[user:upload_a_picture]]', accept: '.png,.jpg,.bmp' - }, function(imageUrlOnServer) { + }, function (imageUrlOnServer) { onUploadComplete(imageUrlOnServer); }); return false; }); - modal.find('[data-action="upload-url"]').on('click', function() { + modal.find('[data-action="upload-url"]').on('click', function () { modal.modal('hide'); - templates.parse('partials/modals/upload_picture_from_url_modal', {}, function(html) { - translator.translate(html, function(html) { + templates.parse('partials/modals/upload_picture_from_url_modal', {}, function (html) { + translator.translate(html, function (html) { var uploadModal = $(html); uploadModal.modal('show'); - uploadModal.find('.upload-btn').on('click', function() { + uploadModal.find('.upload-btn').on('click', function () { var url = uploadModal.find('#uploadFromUrl').val(); if (!url) { return; } - socket.emit('user.uploadProfileImageFromUrl', {url: url, uid: ajaxify.data.theirid}, function(err, imageUrlOnServer) { + socket.emit('user.uploadProfileImageFromUrl', {url: url, uid: ajaxify.data.theirid}, function (err, imageUrlOnServer) { if (err) { return app.alertError(err.message); } @@ -255,8 +255,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], return false; }); - modal.find('[data-action="remove-uploaded"]').on('click', function() { - socket.emit('user.removeUploadedPicture', {uid: ajaxify.data.theirid}, function(err) { + modal.find('[data-action="remove-uploaded"]').on('click', function () { + socket.emit('user.removeUploadedPicture', {uid: ajaxify.data.theirid}, function (err) { modal.modal('hide'); if (err) { return app.alertError(err.message); @@ -267,9 +267,9 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], } function handleEmailConfirm() { - $('#confirm-email').on('click', function() { + $('#confirm-email').on('click', function () { var btn = $(this).attr('disabled', true); - socket.emit('user.emailConfirm', {}, function(err) { + socket.emit('user.emailConfirm', {}, function (err) { btn.removeAttr('disabled'); if (err) { return app.alertError(err.message); @@ -294,7 +294,7 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], var el = $('#inputSignature'); $('#signatureCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumSignatureLength)); - el.on('keyup change', function() { + el.on('keyup change', function () { $('#signatureCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumSignatureLength)); }); } @@ -303,7 +303,7 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], var el = $('#inputAboutMe'); $('#aboutMeCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumAboutMeLength)); - el.on('keyup change', function() { + el.on('keyup change', function () { $('#aboutMeCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumAboutMeLength)); }); } diff --git a/public/src/client/account/edit/email.js b/public/src/client/account/edit/email.js index 1772f76c49..df998bead0 100644 --- a/public/src/client/account/edit/email.js +++ b/public/src/client/account/edit/email.js @@ -2,10 +2,10 @@ /* globals define, ajaxify, socket, app */ -define('forum/account/edit/email', ['forum/account/header'], function(header) { +define('forum/account/edit/email', ['forum/account/header'], function (header) { var AccountEditEmail = {}; - AccountEditEmail.init = function() { + AccountEditEmail.init = function () { header.init(); $('#submitBtn').on('click', function () { @@ -26,7 +26,7 @@ define('forum/account/edit/email', ['forum/account/header'], function(header) { var btn = $(this); btn.addClass('disabled').find('i').removeClass('hide'); - socket.emit('user.changeUsernameEmail', userData, function(err) { + socket.emit('user.changeUsernameEmail', userData, function (err) { btn.removeClass('disabled').find('i').addClass('hide'); if (err) { return app.alertError(err.message); diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js index 3dc36c25b2..a123959804 100644 --- a/public/src/client/account/edit/password.js +++ b/public/src/client/account/edit/password.js @@ -2,10 +2,10 @@ /* globals define, ajaxify, socket, app, utils */ -define('forum/account/edit/password', ['forum/account/header', 'translator'], function(header, translator) { +define('forum/account/edit/password', ['forum/account/header', 'translator'], function (header, translator) { var AccountEditPassword = {}; - AccountEditPassword.init = function() { + AccountEditPassword.init = function () { header.init(); handlePasswordChange(); @@ -56,7 +56,7 @@ define('forum/account/edit/password', ['forum/account/header', 'translator'], fu password.on('blur', onPasswordChanged); password_confirm.on('blur', onPasswordConfirmChanged); - $('#changePasswordBtn').on('click', function() { + $('#changePasswordBtn').on('click', function () { onPasswordChanged(); onPasswordConfirmChanged(); @@ -67,7 +67,7 @@ define('forum/account/edit/password', ['forum/account/header', 'translator'], fu 'currentPassword': currentPassword.val(), 'newPassword': password.val(), 'uid': ajaxify.data.theirid - }, function(err) { + }, function (err) { btn.removeClass('disabled').find('i').addClass('hide'); currentPassword.val(''); password.val(''); @@ -97,22 +97,20 @@ define('forum/account/edit/password', ['forum/account/header', 'translator'], fu } function showError(element, msg) { - translator.translate(msg, function(msg) { - element.find('.error').html(msg).removeClass('hide').siblings().addClass('hide'); + translator.translate(msg, function (msg) { + element.html(msg); element.parent() - .removeClass('alert-success') - .addClass('alert-danger'); - element.show(); + .removeClass('show-success') + .addClass('show-danger'); }); } function showSuccess(element) { - element.find('.success').removeClass('hide').siblings().addClass('hide'); + element.html(''); element.parent() - .removeClass('alert-danger') - .addClass('alert-success'); - element.show(); + .removeClass('show-danger') + .addClass('show-success'); } return AccountEditPassword; diff --git a/public/src/client/account/edit/username.js b/public/src/client/account/edit/username.js index 64f9baa0bc..cb924a3f53 100644 --- a/public/src/client/account/edit/username.js +++ b/public/src/client/account/edit/username.js @@ -2,10 +2,10 @@ /* globals define, ajaxify, socket, app, utils, config */ -define('forum/account/edit/username', ['forum/account/header'], function(header) { +define('forum/account/edit/username', ['forum/account/header'], function (header) { var AccountEditUsername = {}; - AccountEditUsername.init = function() { + AccountEditUsername.init = function () { header.init(); $('#submitBtn').on('click', function updateUsername() { @@ -25,7 +25,7 @@ define('forum/account/edit/username', ['forum/account/header'], function(header) var btn = $(this); btn.addClass('disabled').find('i').removeClass('hide'); - socket.emit('user.changeUsernameEmail', userData, function(err, data) { + socket.emit('user.changeUsernameEmail', userData, function (err, data) { btn.removeClass('disabled').find('i').addClass('hide'); if (err) { return app.alertError(err.message); diff --git a/public/src/client/account/favourites.js b/public/src/client/account/favourites.js deleted file mode 100644 index 9d732418c4..0000000000 --- a/public/src/client/account/favourites.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -/* globals define, app, utils */ - -define('forum/account/favourites', ['forum/account/header', 'forum/account/posts'], function(header, posts) { - var Favourites = {}; - - Favourites.init = function() { - header.init(); - - $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); - - posts.handleInfiniteScroll('posts.loadMoreFavourites', 'account/favourites'); - }; - - return Favourites; -}); diff --git a/public/src/client/account/followers.js b/public/src/client/account/followers.js index 91d3f4daaa..22970327fb 100644 --- a/public/src/client/account/followers.js +++ b/public/src/client/account/followers.js @@ -1,43 +1,13 @@ 'use strict'; -/* globals define, socket, utils */ +/* globals define */ -define('forum/account/followers', ['forum/account/header', 'forum/infinitescroll'], function(header, infinitescroll) { +define('forum/account/followers', ['forum/account/header'], function (header) { var Followers = {}; - Followers.init = function() { + Followers.init = function () { header.init(); - - infinitescroll.init(function(direction) { - Followers.loadMore(direction, 'account/followers', 'followers:' + ajaxify.data.uid); - }); }; - Followers.loadMore = function(direction, tpl, set) { - if (direction < 0) { - return; - } - - infinitescroll.loadMore('user.loadMore', { - set: set, - after: $('#users-container').attr('data-nextstart') - }, function(data, done) { - if (data.users && data.users.length) { - onUsersLoaded(tpl, data.users, done); - $('#users-container').attr('data-nextstart', data.nextStart); - } else { - done(); - } - }); - }; - - function onUsersLoaded(tpl, users, callback) { - app.parseAndTranslate(tpl, 'users', {users: users}, function(html) { - $('#users-container').append(html); - utils.addCommasToNumbers(html.find('.formatted-number')); - callback(); - }); - } - return Followers; }); diff --git a/public/src/client/account/following.js b/public/src/client/account/following.js index 8e421b1892..dff2f92709 100644 --- a/public/src/client/account/following.js +++ b/public/src/client/account/following.js @@ -2,15 +2,11 @@ /* globals define */ -define('forum/account/following', ['forum/account/header', 'forum/infinitescroll', 'forum/account/followers'], function(header, infinitescroll, followers) { +define('forum/account/following', ['forum/account/header'], function (header) { var Following = {}; - Following.init = function() { + Following.init = function () { header.init(); - - infinitescroll.init(function(direction) { - followers.loadMore(direction, 'account/following', 'following:' + ajaxify.data.uid); - }); }; return Following; diff --git a/public/src/client/account/groups.js b/public/src/client/account/groups.js index e4c932161d..f8fe55fdd9 100644 --- a/public/src/client/account/groups.js +++ b/public/src/client/account/groups.js @@ -2,15 +2,15 @@ /* globals ajaxify, define, app, socket, utils */ -define('forum/account/groups', ['forum/account/header'], function(header) { +define('forum/account/groups', ['forum/account/header'], function (header) { var AccountTopics = {}; - AccountTopics.init = function() { + AccountTopics.init = function () { header.init(); var groupsEl = $('#groups-list'); - groupsEl.on('click', '.list-cover', function() { + groupsEl.on('click', '.list-cover', function () { var groupSlug = $(this).parents('[data-slug]').attr('data-slug'); ajaxify.go('groups/' + groupSlug); diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index 520224d9ef..d29b58c05b 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -1,43 +1,42 @@ 'use strict'; -/* globals define, app, config, ajaxify, socket, bootbox, translator */ +/* globals define, app, config, ajaxify, socket, bootbox, templates */ define('forum/account/header', [ 'coverPhoto', 'uploader', - 'components' -], function(coverPhoto, uploader, components) { - var AccountHeader = {}, - yourid, - theirid; + 'components', + 'translator' +], function (coverPhoto, uploader, components, translator) { + var AccountHeader = {}; + var isAdminOrSelfOrGlobalMod; - AccountHeader.init = function() { - yourid = ajaxify.data.yourid; - theirid = ajaxify.data.theirid; + AccountHeader.init = function () { + isAdminOrSelfOrGlobalMod = ajaxify.data.isAdmin || ajaxify.data.isSelf || ajaxify.data.isGlobalModerator; hidePrivateLinks(); selectActivePill(); - if (parseInt(yourid, 10) === parseInt(theirid, 10)) { + if (isAdminOrSelfOrGlobalMod) { setupCoverPhoto(); } - components.get('account/follow').on('click', function() { + components.get('account/follow').on('click', function () { toggleFollow('follow'); }); - components.get('account/unfollow').on('click', function() { + components.get('account/unfollow').on('click', function () { toggleFollow('unfollow'); }); - components.get('account/chat').on('click', function() { - socket.emit('modules.chats.hasPrivateChat', theirid, function(err, roomId) { + components.get('account/chat').on('click', function () { + socket.emit('modules.chats.hasPrivateChat', ajaxify.data.uid, function (err, roomId) { if (err) { return app.alertError(err.message); } if (roomId) { app.openChat(roomId); } else { - app.newChat(theirid); + app.newChat(ajaxify.data.uid); } }); }); @@ -54,7 +53,7 @@ define('forum/account/header', [ } function selectActivePill() { - $('.account-sub-links li').removeClass('active').each(function() { + $('.account-sub-links li').removeClass('active').each(function () { var href = $(this).find('a').attr('href'); if (decodeURIComponent(href) === decodeURIComponent(window.location.pathname)) { @@ -66,20 +65,20 @@ define('forum/account/header', [ function setupCoverPhoto() { coverPhoto.init(components.get('account/cover'), - function(imageData, position, callback) { + function (imageData, position, callback) { socket.emit('user.updateCover', { - uid: yourid, + uid: ajaxify.data.uid, imageData: imageData, position: position }, callback); }, - function() { + function () { uploader.show({ title: '[[user:upload_cover_picture]]', route: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/uploadcover', - params: {uid: yourid }, + params: {uid: ajaxify.data.uid }, accept: '.png,.jpg,.bmp' - }, function(imageUrlOnServer) { + }, function (imageUrlOnServer) { components.get('account/cover').css('background-image', 'url(' + imageUrlOnServer + '?v=' + Date.now() + ')'); }); }, @@ -89,8 +88,8 @@ define('forum/account/header', [ function toggleFollow(type) { socket.emit('user.' + type, { - uid: theirid - }, function(err) { + uid: ajaxify.data.uid + }, function (err) { if (err) { return app.alertError(err.message); } @@ -103,41 +102,56 @@ define('forum/account/header', [ } function banAccount() { - translator.translate('[[user:ban_account_confirm]]', function(translated) { - bootbox.confirm(translated, function(confirm) { - if (!confirm) { - return; - } - socket.emit('user.banUsers', [ajaxify.data.theirid], function(err) { - if (err) { - return app.alertError(err.message); + templates.parse('admin/partials/temporary-ban', {}, function (html) { + bootbox.dialog({ + className: 'ban-modal', + title: '[[user:ban_account]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link' + }, + submit: { + label: '[[user:ban_account]]', + callback: function () { + var formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + var until = formData.length ? (Date.now() + formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) : 0; + + socket.emit('user.banUsers', { uids: [ajaxify.data.theirid], until: until, reason: formData.reason || '' }, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); + }); + } } - components.get('account/ban').parent().addClass('hide'); - components.get('account/unban').parent().removeClass('hide'); - }); + } }); }); } function unbanAccount() { - socket.emit('user.unbanUsers', [ajaxify.data.theirid], function(err) { + socket.emit('user.unbanUsers', [ajaxify.data.theirid], function (err) { if (err) { return app.alertError(err.message); } - - components.get('account/ban').parent().removeClass('hide'); - components.get('account/unban').parent().addClass('hide'); + ajaxify.refresh(); }); } function deleteAccount() { - translator.translate('[[user:delete_this_account_confirm]]', function(translated) { - bootbox.confirm(translated, function(confirm) { + translator.translate('[[user:delete_this_account_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { if (!confirm) { return; } - socket.emit('admin.user.deleteUsers', [ajaxify.data.theirid], function(err) { + socket.emit('admin.user.deleteUsersAndContent', [ajaxify.data.theirid], function (err) { if (err) { return app.alertError(err.message); } @@ -151,7 +165,7 @@ define('forum/account/header', [ function removeCover() { socket.emit('user.removeCover', { uid: ajaxify.data.uid - }, function(err) { + }, function (err) { if (!err) { ajaxify.refresh(); } else { diff --git a/public/src/client/account/info.js b/public/src/client/account/info.js new file mode 100644 index 0000000000..4ee76b21a3 --- /dev/null +++ b/public/src/client/account/info.js @@ -0,0 +1,26 @@ +'use strict'; + +/* globals define, socket, ajaxify, app */ + +define('forum/account/info', ['forum/account/header'], function (header) { + var Info = {}; + + Info.init = function () { + header.init(); + handleModerationNote(); + }; + + function handleModerationNote() { + $('[component="account/save-moderation-note"]').on('click', function () { + var note = $('[component="account/moderation-note"]').val(); + socket.emit('user.setModerationNote', {uid: ajaxify.data.uid, note: note}, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[user:info.moderation-note.success]]'); + }); + }); + } + + return Info; +}); diff --git a/public/src/client/account/posts.js b/public/src/client/account/posts.js index 35e60e890b..9e2082e211 100644 --- a/public/src/client/account/posts.js +++ b/public/src/client/account/posts.js @@ -2,11 +2,11 @@ /* globals define, app, socket, utils, config, ajaxify */ -define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'], function(header, infinitescroll) { +define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'], function (header, infinitescroll) { var AccountPosts = {}; var method, template; - AccountPosts.init = function() { + AccountPosts.init = function () { header.init(); $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); @@ -14,7 +14,7 @@ define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'], AccountPosts.handleInfiniteScroll('posts.loadMoreUserPosts', 'account/posts'); }; - AccountPosts.handleInfiniteScroll = function(_method, _template) { + AccountPosts.handleInfiniteScroll = function (_method, _template) { method = _method; template = _template; if (!config.usePagination) { @@ -30,7 +30,7 @@ define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'], infinitescroll.loadMore(method, { uid: ajaxify.data.theirid, after: $('[component="posts"]').attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (data.posts && data.posts.length) { onPostsLoaded(data.posts, done); } else { @@ -41,7 +41,7 @@ define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'], } function onPostsLoaded(posts, callback) { - app.parseAndTranslate(template, 'posts', {posts: posts}, function(html) { + app.parseAndTranslate(template, 'posts', {posts: posts}, function (html) { $('[component="posts"]').append(html); html.find('img:not(.not-responsive)').addClass('img-responsive'); html.find('.timeago').timeago(); diff --git a/public/src/client/account/profile.js b/public/src/client/account/profile.js index 0d59a96df1..f1d41db46f 100644 --- a/public/src/client/account/profile.js +++ b/public/src/client/account/profile.js @@ -7,11 +7,11 @@ define('forum/account/profile', [ 'forum/infinitescroll', 'translator', 'components' -], function(header, infinitescroll, translator) { +], function (header, infinitescroll, translator) { var Account = {}, theirid; - Account.init = function() { + Account.init = function () { header.init(); theirid = ajaxify.data.theirid; @@ -48,7 +48,7 @@ define('forum/account/profile', [ infinitescroll.loadMore('posts.loadMoreUserPosts', { after: $('[component="posts"]').attr('data-nextstart'), uid: theirid - }, function(data, done) { + }, function (data, done) { if (data.posts && data.posts.length) { onPostsLoaded(data.posts, done); } else { @@ -60,7 +60,7 @@ define('forum/account/profile', [ } function onPostsLoaded(posts, callback) { - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return !$('[component="posts"] [data-pid=' + post.pid + ']').length; }); @@ -68,7 +68,7 @@ define('forum/account/profile', [ return callback(); } - app.parseAndTranslate('account/profile', 'posts', {posts: posts}, function(html) { + app.parseAndTranslate('account/profile', 'posts', {posts: posts}, function (html) { $('[component="posts"]').append(html); html.find('.timeago').timeago(); diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index 04312f2823..6410267795 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -2,109 +2,146 @@ /*global define, socket, app, ajaxify, config*/ -define('forum/account/settings', ['forum/account/header', 'components', 'csrf'], function(header, components, csrf) { +define('forum/account/settings', ['forum/account/header', 'components', 'sounds'], function (header, components, sounds) { var AccountSettings = {}; - AccountSettings.init = function() { + AccountSettings.init = function () { header.init(); - $('#submitBtn').on('click', function() { - var settings = {}; - - $('.account').find('input, textarea, select').each(function(id, input) { - input = $(input); - var setting = input.attr('data-property'); - if (input.is('select')) { - settings[setting] = input.val(); - return; - } - - switch (input.attr('type')) { - case 'text': - case 'textarea': - settings[setting] = input.val(); - break; - case 'checkbox': - settings[setting] = input.is(':checked') ? 1 : 0; - break; - } - }); - - socket.emit('user.saveSettings', {uid: ajaxify.data.theirid, settings: settings}, function(err, newSettings) { - if (err) { - return app.alertError(err.message); - } - - app.alertSuccess('[[success:settings-saved]]'); - var requireReload = false; - for (var key in newSettings) { - if (newSettings.hasOwnProperty(key)) { - if (key === 'userLang' && config.userLang !== newSettings.userLang) { - requireReload = true; - } - config[key] = newSettings[key]; - } - } - - if (requireReload && parseInt(app.user.uid, 10) === parseInt(ajaxify.data.theirid, 10)) { - app.alert({ - id: 'setting-change', - message: '[[user:settings-require-reload]]', - type: 'warning', - timeout: 5000, - clickfn: function() { - ajaxify.refresh(); - } - }); - } - }); + $('#submitBtn').on('click', function () { + var settings = loadSettings(); + + if (settings.homePageRoute === 'custom' && settings.homePageCustom) { + $.get(config.relative_path + '/' + settings.homePageCustom, function () { + saveSettings(settings); + }).fail(function () { + app.alertError('[[error:invalid-home-page-route]]'); + }); + } else { + saveSettings(settings); + } return false; }); - $('#bootswatchSkin').on('change', function() { - var css = $('#bootswatchCSS'), - val = $(this).val() === 'default' ? config['theme:src'] : 'http://maxcdn.bootstrapcdn.com/bootswatch/latest/' + $(this).val() + '/bootstrap.min.css'; + $('#bootswatchSkin').on('change', function () { + var css = $('#bootswatchCSS'); + var val = $(this).val() === 'default' ? config['theme:src'] : '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + $(this).val() + '/bootstrap.min.css'; css.attr('href', val); }); $('[data-property="homePageRoute"]').on('change', toggleCustomRoute); + $('.account').find('button[data-action="play"]').on('click', function (e) { + e.preventDefault(); + + var fileName = $(this).parent().parent().find('select').val(); + sounds.playFile(fileName); + }); + toggleCustomRoute(); components.get('user/sessions').find('.timeago').timeago(); prepareSessionRevoking(); }; + function loadSettings() { + var settings = {}; + + $('.account').find('input, textarea, select').each(function (id, input) { + input = $(input); + var setting = input.attr('data-property'); + if (input.is('select')) { + settings[setting] = input.val(); + return; + } + + switch (input.attr('type')) { + case 'text': + case 'textarea': + settings[setting] = input.val(); + break; + case 'checkbox': + settings[setting] = input.is(':checked') ? 1 : 0; + break; + } + }); + + return settings; + } + + function saveSettings(settings) { + socket.emit('user.saveSettings', {uid: ajaxify.data.theirid, settings: settings}, function (err, newSettings) { + if (err) { + return app.alertError(err.message); + } + + app.alertSuccess('[[success:settings-saved]]'); + var requireReload = false; + for (var key in newSettings) { + if (newSettings.hasOwnProperty(key)) { + if (key === 'userLang' && config.userLang !== newSettings.userLang) { + requireReload = true; + } + if (config.hasOwnProperty(key)) { + config[key] = newSettings[key]; + } + } + } + + sounds.reloadMapping(); + + if (requireReload && parseInt(app.user.uid, 10) === parseInt(ajaxify.data.theirid, 10)) { + app.alert({ + id: 'setting-change', + message: '[[user:settings-require-reload]]', + type: 'warning', + timeout: 5000, + clickfn: function () { + ajaxify.refresh(); + } + }); + } + }); + } + function toggleCustomRoute() { - $('[data-property="homePageCustom"]').val(''); if ($('[data-property="homePageRoute"]').val() === 'custom') { $('#homePageCustom').show(); - }else{ + } else { $('#homePageCustom').hide(); + $('[data-property="homePageCustom"]').val(''); } } function prepareSessionRevoking() { - components.get('user/sessions').on('click', '[data-action]', function() { - var parentEl = $(this).parents('[data-uuid]'), - uuid = parentEl.attr('data-uuid'); + components.get('user/sessions').on('click', '[data-action]', function () { + var parentEl = $(this).parents('[data-uuid]'); + var uuid = parentEl.attr('data-uuid'); if (uuid) { // This is done via DELETE because a user shouldn't be able to // revoke his own session! This is what logout is for $.ajax({ - url: config.relative_path + '/user/' + ajaxify.data.userslug + '/session/' + uuid, + url: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/session/' + uuid, method: 'delete', headers: { - 'x-csrf-token': csrf.get() + 'x-csrf-token': config.csrf_token } - }).done(function() { + }).done(function () { parentEl.remove(); - }).fail(function(err) { - app.alertError(err.responseText); - }) + }).fail(function (err) { + try { + var errorObj = JSON.parse(err.responseText); + if (errorObj.loggedIn === false) { + window.location.href = config.relative_path + '/login?error=' + errorObj.title; + } + app.alertError(errorObj.title); + } catch (e) { + app.alertError('[[error:invalid-data]]'); + } + }); } }); } diff --git a/public/src/client/account/topics.js b/public/src/client/account/topics.js index 1fefc727d1..63658f0cbf 100644 --- a/public/src/client/account/topics.js +++ b/public/src/client/account/topics.js @@ -2,17 +2,17 @@ /* globals define, app, socket, utils, config, ajaxify */ -define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'], function(header, infinitescroll) { +define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'], function (header, infinitescroll) { var AccountTopics = {}; var template, set; - AccountTopics.init = function() { + AccountTopics.init = function () { header.init(); AccountTopics.handleInfiniteScroll('account/topics', 'uid:' + ajaxify.data.theirid + ':topics'); }; - AccountTopics.handleInfiniteScroll = function(_template, _set) { + AccountTopics.handleInfiniteScroll = function (_template, _set) { template = _template; set = _set; @@ -29,7 +29,7 @@ define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'], infinitescroll.loadMore('topics.loadMoreFromSet', { set: set, after: $('[component="category"]').attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (data.topics && data.topics.length) { onTopicsLoaded(data.topics, done); } else { @@ -41,7 +41,7 @@ define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'], } function onTopicsLoaded(topics, callback) { - app.parseAndTranslate('account/topics', 'topics', {topics: topics}, function(html) { + app.parseAndTranslate('account/topics', 'topics', {topics: topics}, function (html) { $('[component="category"]').append(html); html.find('.timeago').timeago(); app.createUserTooltips(); diff --git a/public/src/client/account/upvoted.js b/public/src/client/account/upvoted.js index 1d0ef86294..76bf001b77 100644 --- a/public/src/client/account/upvoted.js +++ b/public/src/client/account/upvoted.js @@ -2,10 +2,10 @@ /* globals define */ -define('forum/account/upvoted', ['forum/account/header', 'forum/account/posts'], function(header, posts) { +define('forum/account/upvoted', ['forum/account/header', 'forum/account/posts'], function (header, posts) { var Upvoted = {}; - Upvoted.init = function() { + Upvoted.init = function () { header.init(); $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); diff --git a/public/src/client/account/watched.js b/public/src/client/account/watched.js index dae195df2d..4e1ca01cfe 100644 --- a/public/src/client/account/watched.js +++ b/public/src/client/account/watched.js @@ -1,10 +1,10 @@ 'use strict'; /* globals define, app, socket, utils */ -define('forum/account/watched', ['forum/account/header', 'forum/account/topics'], function(header, topics) { +define('forum/account/watched', ['forum/account/header', 'forum/account/topics'], function (header, topics) { var AccountWatched = {}; - AccountWatched.init = function() { + AccountWatched.init = function () { header.init(); topics.handleInfiniteScroll('account/watched', 'uid:' + ajaxify.data.theirid + ':followed_tids'); diff --git a/public/src/client/categories.js b/public/src/client/categories.js index a73ed617bb..672fa38cf9 100644 --- a/public/src/client/categories.js +++ b/public/src/client/categories.js @@ -2,16 +2,16 @@ /* globals define, socket, app, templates, ajaxify*/ -define('forum/categories', ['components', 'translator'], function(components, translator) { +define('forum/categories', ['components', 'translator'], function (components, translator) { var categories = {}; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { if (ajaxify.currentPage !== data.url) { socket.removeListener('event:new_post', categories.onNewPost); } }); - categories.init = function() { + categories.init = function () { app.enterRoom('categories'); socket.removeListener('event:new_post', categories.onNewPost); @@ -22,7 +22,7 @@ define('forum/categories', ['components', 'translator'], function(components, tr }); }; - categories.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]); } @@ -38,7 +38,7 @@ define('forum/categories', ['components', 'translator'], function(components, tr var recentPosts = category.find('[component="category/posts"]'); var insertBefore = recentPosts.first(); - parseAndTranslate([post], function(html) { + parseAndTranslate([post], function (html) { html.hide(); if(recentPosts.length === 0) { html.appendTo(category); @@ -60,8 +60,8 @@ define('forum/categories', ['components', 'translator'], function(components, tr } function parseAndTranslate(posts, callback) { - templates.parse('categories', '(categories.)?posts', {categories: {posts: posts}}, function(html) { - translator.translate(html, function(translatedHTML) { + templates.parse('categories', '(categories.)?posts', {categories: {posts: posts}}, function (html) { + translator.translate(html, function (translatedHTML) { translatedHTML = $(translatedHTML); translatedHTML.find('img:not(.not-responsive)').addClass('img-responsive'); diff --git a/public/src/client/category.js b/public/src/client/category.js index a441ff5d08..7f64fd4f7f 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -11,10 +11,10 @@ define('forum/category', [ 'translator', 'topicSelect', 'forum/pagination' -], function(infinitescroll, share, navigator, categoryTools, sort, components, translator, topicSelect, pagination) { +], function (infinitescroll, share, navigator, categoryTools, sort, components, translator, topicSelect, pagination) { var Category = {}; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { if (ajaxify.currentPage !== data.url) { navigator.disable(); @@ -27,7 +27,7 @@ define('forum/category', [ categoryTools.removeListeners(); } - Category.init = function() { + Category.init = function () { var cid = ajaxify.data.cid; app.enterRoom('category_' + cid); @@ -47,9 +47,9 @@ define('forum/category', [ enableInfiniteLoadingOrPagination(); - $('[component="category"]').on('click', '[component="topic/header"]', function() { + $('[component="category"]').on('click', '[component="topic/header"]', function () { var clickedIndex = $(this).parents('[data-index]').attr('data-index'); - $('[component="category/topic"]').each(function(index, el) { + $('[component="category/topic"]').each(function (index, el) { if ($(el).offset().top - $(window).scrollTop() > 0) { localStorage.setItem('category:' + cid + ':bookmark', $(el).attr('data-index')); localStorage.setItem('category:' + cid + ':bookmark:clicked', clickedIndex); @@ -59,41 +59,51 @@ define('forum/category', [ }); handleIgnoreWatch(cid); + + $(window).trigger('action:topics.loaded', {topics: ajaxify.data.topics}); + $(window).trigger('action:category.loaded', {cid: ajaxify.data.cid}); }; function handleIgnoreWatch(cid) { - $('.watch, .ignore').on('click', function() { + $('[component="category/watching"], [component="category/ignoring"]').on('click', function () { var $this = $(this); - var command = $this.hasClass('watch') ? 'watch' : 'ignore'; + var command = $this.attr('component') === 'category/watching' ? 'watch' : 'ignore'; - socket.emit('categories.' + command, cid, function(err) { + socket.emit('categories.' + command, cid, function (err) { if (err) { return app.alertError(err.message); } - $('.watch').toggleClass('hidden', command === 'watch'); - $('.ignore').toggleClass('hidden', command === 'ignore'); + $('[component="category/watching/menu"]').toggleClass('hidden', command !== 'watch'); + $('[component="category/watching/check"]').toggleClass('fa-check', command === 'watch'); + + $('[component="category/ignoring/menu"]').toggleClass('hidden', command !== 'ignore'); + $('[component="category/ignoring/check"]').toggleClass('fa-check', command === 'ignore'); app.alertSuccess('[[category:' + command + '.message]]'); }); }); } - Category.toTop = function() { + Category.toTop = function () { navigator.scrollTop(0); }; - Category.toBottom = function() { - socket.emit('categories.getTopicCount', ajaxify.data.cid, function(err, count) { + Category.toBottom = function () { + socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, count) { + if (err) { + return app.alertError(err.message); + } + navigator.scrollBottom(count - 1); }); }; - Category.navigatorCallback = function(topIndex, bottomIndex, elementCount) { + Category.navigatorCallback = function (topIndex, bottomIndex, elementCount) { return bottomIndex; }; - $(window).on('action:popstate', function(ev, data) { + $(window).on('action:popstate', function (ev, data) { if (data.url.startsWith('category/')) { var cid = data.url.match(/^category\/(\d+)/); if (cid && cid[1]) { @@ -115,7 +125,7 @@ define('forum/category', [ if (config.usePagination) { var page = Math.ceil((parseInt(bookmarkIndex, 10) + 1) / config.topicsPerPage); if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { - pagination.loadPage(page, function() { + pagination.loadPage(page, function () { Category.scrollToTopic(bookmarkIndex, clickedIndex, 400); }); } else { @@ -129,25 +139,25 @@ define('forum/category', [ $('[component="category"]').empty(); - loadTopicsAfter(Math.max(0, bookmarkIndex - 1), 1, function() { + loadTopicsAfter(Math.max(0, bookmarkIndex - 1), 1, function () { Category.scrollToTopic(bookmarkIndex, clickedIndex, 0); }); } } }); - Category.highlightTopic = function(topicIndex) { + Category.highlightTopic = function (topicIndex) { var highlight = components.get('category/topic', 'index', topicIndex); if (highlight.length && !highlight.hasClass('highlight')) { highlight.addClass('highlight'); - setTimeout(function() { + setTimeout(function () { highlight.removeClass('highlight'); }, 5000); } }; - Category.scrollToTopic = function(bookmarkIndex, clickedIndex, duration, offset) { + Category.scrollToTopic = function (bookmarkIndex, clickedIndex, duration, offset) { if (!bookmarkIndex) { return; } @@ -162,7 +172,7 @@ define('forum/category', [ if (scrollTo.length && cid) { $('html, body').animate({ scrollTop: (scrollTo.offset().top - offset) + 'px' - }, duration !== undefined ? duration : 400, function() { + }, duration !== undefined ? duration : 400, function () { Category.highlightTopic(clickedIndex); navigator.update(); }); @@ -177,7 +187,7 @@ define('forum/category', [ } } - Category.onNewTopic = function(topic) { + Category.onNewTopic = function (topic) { var cid = ajaxify.data.cid; if (!topic || parseInt(topic.cid, 10) !== parseInt(cid, 10)) { return; @@ -190,9 +200,10 @@ define('forum/category', [ templates.parse('category', 'topics', { privileges: {editable: editable}, showSelect: editable, - topics: [topic] - }, function(html) { - translator.translate(html, function(translatedHTML) { + topics: [topic], + template: {category: true} + }, function (html) { + translator.translate(html, function (translatedHTML) { var topic = $(translatedHTML), container = $('[component="category"]'), topics = $('[component="category/topic"]'), @@ -235,7 +246,7 @@ define('forum/category', [ }; function updateTopicCount() { - socket.emit('categories.getTopicCount', ajaxify.data.cid, function(err, topicCount) { + socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, topicCount) { if(err) { return app.alertError(err.message); } @@ -243,7 +254,7 @@ define('forum/category', [ }); } - Category.loadMoreTopics = function(direction) { + Category.loadMoreTopics = function (direction) { if (!$('[component="category"]').length || !$('[component="category"]').children().length) { return; } @@ -256,17 +267,20 @@ define('forum/category', [ }; function loadTopicsAfter(after, direction, callback) { - callback = callback || function() {}; + callback = callback || function () {}; if (!utils.isNumber(after) || (after === 0 && components.get('category/topic', 'index', 0).length)) { return callback(); } $(window).trigger('action:categories.loading'); + var params = utils.params(); infinitescroll.loadMore('categories.loadMore', { cid: ajaxify.data.cid, after: after, direction: direction, - author: utils.params().author + author: params.author, + tag: params.tag, + categoryTopicSort: config.categoryTopicSort }, function (data, done) { if (data.topics && data.topics.length) { Category.onTopicsLoaded(data, direction, done); @@ -280,13 +294,13 @@ define('forum/category', [ } - Category.onTopicsLoaded = function(data, direction, callback) { + Category.onTopicsLoaded = function (data, direction, callback) { if (!data || !data.topics.length) { return callback(); } function removeAlreadyAddedTopics(topics) { - return topics.filter(function(topic) { + return topics.filter(function (topic) { return components.get('category/topic', 'tid', topic.tid).length === 0; }); } @@ -307,7 +321,7 @@ define('forum/category', [ before = topics.first(); } - app.parseAndTranslate('category', 'topics', data, function(html) { + app.parseAndTranslate('category', 'topics', data, function (html) { $('[component="category"]').removeClass('hidden'); $('.category-sidebar').removeClass('hidden'); @@ -341,4 +355,4 @@ define('forum/category', [ }; return Category; -}); \ No newline at end of file +}); diff --git a/public/src/client/categoryTools.js b/public/src/client/categoryTools.js index 348ff684f8..548386ffc6 100644 --- a/public/src/client/categoryTools.js +++ b/public/src/client/categoryTools.js @@ -4,31 +4,31 @@ /* globals define, app, socket, bootbox, ajaxify */ -define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', 'translator'], function(move, topicSelect, components, translator) { +define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', 'translator'], function (move, topicSelect, components, translator) { var CategoryTools = {}; - CategoryTools.init = function(cid) { + CategoryTools.init = function (cid) { CategoryTools.cid = cid; topicSelect.init(updateDropdownOptions); - components.get('topic/delete').on('click', function() { + components.get('topic/delete').on('click', function () { categoryCommand('delete', topicSelect.getSelectedTids()); return false; }); - components.get('topic/restore').on('click', function() { + components.get('topic/restore').on('click', function () { categoryCommand('restore', topicSelect.getSelectedTids()); return false; }); - components.get('topic/purge').on('click', function() { + components.get('topic/purge').on('click', function () { categoryCommand('purge', topicSelect.getSelectedTids()); return false; }); - components.get('topic/lock').on('click', function() { + components.get('topic/lock').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { socket.emit('topics.lock', {tids: tids, cid: CategoryTools.cid}, onCommandComplete); @@ -36,7 +36,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/unlock').on('click', function() { + components.get('topic/unlock').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { socket.emit('topics.unlock', {tids: tids, cid: CategoryTools.cid}, onCommandComplete); @@ -44,7 +44,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/pin').on('click', function() { + components.get('topic/pin').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { socket.emit('topics.pin', {tids: tids, cid: CategoryTools.cid}, onCommandComplete); @@ -52,7 +52,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/unpin').on('click', function() { + components.get('topic/unpin').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { socket.emit('topics.unpin', {tids: tids, cid: CategoryTools.cid}, onCommandComplete); @@ -60,15 +60,17 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/mark-unread-for-all').on('click', function() { + components.get('topic/mark-unread-for-all').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { - socket.emit('topics.markAsUnreadForAll', tids, function(err) { + socket.emit('topics.markAsUnreadForAll', tids, function (err) { if (err) { return app.alertError(err.message); } app.alertSuccess('[[topic:markAsUnreadForAll.success]]'); - + tids.forEach(function (tid) { + $('[component="category/topic"][data-tid="' + tid + '"]').addClass('unread'); + }); onCommandComplete(); }); } @@ -76,7 +78,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/move').on('click', function() { + components.get('topic/move').on('click', function () { var tids = topicSelect.getSelectedTids(); if (tids.length) { @@ -85,8 +87,12 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return false; }); - components.get('topic/move-all').on('click', function() { - move.init(null, cid, function(err) { + components.get('topic/move-all').on('click', function () { + move.init(null, cid, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); }); }); @@ -107,8 +113,8 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', return; } - translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function(msg) { - bootbox.confirm(msg, function(confirm) { + translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function (msg) { + bootbox.confirm(msg, function (confirm) { if (!confirm) { return; } @@ -118,7 +124,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', }); } - CategoryTools.removeListeners = function() { + CategoryTools.removeListeners = function () { socket.removeListener('event:topic_deleted', setDeleteState); socket.removeListener('event:topic_restored', setDeleteState); socket.removeListener('event:topic_purged', onTopicPurged); @@ -169,7 +175,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', } function isAny(method, tids) { - for(var i=0; i<tids.length; ++i) { + for(var i = 0; i < tids.length; ++i) { if(method(tids[i])) { return true; } @@ -178,7 +184,7 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', } function areAll(method, tids) { - for(var i=0; i<tids.length; ++i) { + for(var i = 0; i < tids.length; ++i) { if(!method(tids[i])) { return false; } diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 9046318212..abab76f965 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -1,15 +1,22 @@ 'use strict'; -/* globals define, config, app, ajaxify, utils, socket, templates, Mousetrap, bootbox */ - -define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', 'translator'], function(components, S, sounds, infinitescroll, translator) { +/* globals define, app, ajaxify, utils, socket, templates */ + +define('forum/chats', [ + 'components', + 'translator', + 'mousetrap', + 'forum/chats/recent', + 'forum/chats/search', + 'forum/chats/messages' +], function (components, translator, mousetrap, recentChats, search, messages) { var Chats = { initialised: false }; var newMessage = false; - Chats.init = function() { + Chats.init = function () { var env = utils.findBootstrapEnvironment(); if (!Chats.initialised) { @@ -26,73 +33,103 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.addHotkeys(); } - Chats.scrollToBottom($('.expanded-chat ul')); + messages.scrollToBottom($('.expanded-chat ul')); Chats.initialised = true; - Chats.handleSearch(); + search.init(); if (ajaxify.data.hasOwnProperty('roomId')) { components.get('chat/input').focus(); } }; - Chats.addEventListeners = function() { - $('[component="chat/recent"]').on('click', '[component="chat/leave"]', function() { + Chats.addEventListeners = function () { + $('[component="chat/recent"]').on('click', '[component="chat/leave"]', function () { Chats.leave($(this).parents('[data-roomid]')); return false; }); - $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function() { + $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { Chats.switchChat($(this).attr('data-roomid')); }); Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]')); - $('[data-action="pop-out"]').on('click', function() { + $('[data-action="pop-out"]').on('click', function () { var text = components.get('chat/input').val(); var roomId = ajaxify.data.roomId; if (app.previousUrl && app.previousUrl.match(/chats/)) { - ajaxify.go('chats', function() { - app.openChat(roomId); + ajaxify.go('user/' + ajaxify.data.userslug + '/chats', function () { + app.openChat(roomId, ajaxify.data.uid); }, true); } else { window.history.go(-1); - app.openChat(roomId); + app.openChat(roomId, ajaxify.data.uid); } - $(window).one('action:chat.loaded', function() { + $(window).one('action:chat.loaded', function () { components.get('chat/input').val(text); }); }); - components.get('chat/messages') - .on('click', '[data-action="edit"]', function() { - var messageId = $(this).parents('[data-mid]').attr('data-mid'); - var inputEl = components.get('chat/input'); - Chats.prepEdit(inputEl, messageId, ajaxify.data.roomId); - }) - .on('click', '[data-action="delete"]', function() { - var messageId = $(this).parents('[data-mid]').attr('data-mid'); - Chats.delete(messageId, ajaxify.data.roomId); - }); + Chats.addEditDeleteHandler(components.get('chat/messages'), ajaxify.data.roomId); - $('[component="chat/recent"]').on('scroll', function() { - var $this = $(this); - var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; - if ($this.scrollTop() > bottom) { - loadMoreRecentChats(); + recentChats.init(); + + Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]')); + Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); + }; + + Chats.addScrollHandler = function (roomId, uid, el) { + var loading = false; + el.off('scroll').on('scroll', function () { + if (loading) { + return; } + + var top = (el[0].scrollHeight - el.height()) * 0.1; + if (el.scrollTop() >= top) { + return; + } + loading = true; + var start = parseInt($('.chat-content').children('[data-index]').first().attr('data-index'), 10) + 1; + socket.emit('modules.chats.getMessages', {roomId: roomId, uid: uid, start: start}, function (err, data) { + if (err) { + return app.alertError(err.message); + } + if (!data) { + return; + } + messages.parseMessage(data, function (html) { + var currentScrollTop = el.scrollTop(); + var previousHeight = el[0].scrollHeight; + html = $(html); + el.prepend(html); + html.find('.timeago').timeago(); + html.find('img:not(.not-responsive)').addClass('img-responsive'); + el.scrollTop((el[0].scrollHeight - previousHeight) + currentScrollTop); + loading = false; + }); + }); }); + }; - Chats.addSinceHandler(ajaxify.data.roomId, $('.expanded-chat .chat-content'), $('.expanded-chat [data-since]')); - Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]')); + Chats.addEditDeleteHandler = function (element, roomId) { + element.on('click', '[data-action="edit"]', function () { + var messageId = $(this).parents('[data-mid]').attr('data-mid'); + var inputEl = components.get('chat/input'); + messages.prepEdit(inputEl, messageId, roomId); + }).on('click', '[data-action="delete"]', function () { + var messageId = $(this).parents('[data-mid]').attr('data-mid'); + messages.delete(messageId, roomId); + }); }; - Chats.addHotkeys = function() { - Mousetrap.bind('ctrl+up', function() { + Chats.addHotkeys = function () { + mousetrap.bind('ctrl+up', function () { var activeContact = $('.chats-list .bg-primary'), prev = activeContact.prev(); @@ -100,7 +137,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.switchChat(prev.attr('data-roomid')); } }); - Mousetrap.bind('ctrl+down', function() { + mousetrap.bind('ctrl+down', function () { var activeContact = $('.chats-list .bg-primary'), next = activeContact.next(); @@ -108,69 +145,21 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.switchChat(next.attr('data-roomid')); } }); - Mousetrap.bind('up', function(e) { + mousetrap.bind('up', function (e) { if (e.target === components.get('chat/input').get(0)) { // Retrieve message id from messages list var message = components.get('chat/messages').find('.chat-message[data-self="1"]').last(); var lastMid = message.attr('data-mid'); var inputEl = components.get('chat/input'); - Chats.prepEdit(inputEl, lastMid, ajaxify.data.roomId); + messages.prepEdit(inputEl, lastMid, ajaxify.data.roomId); } }); }; - Chats.prepEdit = function(inputEl, messageId, roomId) { - socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function(err, raw) { - if (err) { - return app.alertError(err.message); - } - // Populate the input field with the raw message content - if (inputEl.val().length === 0) { - // By setting the `data-mid` attribute, I tell the chat code that I am editing a - // message, instead of posting a new one. - inputEl.attr('data-mid', messageId).addClass('editing'); - inputEl.val(raw); - } - }); - }; - - Chats.delete = function(messageId, roomId) { - translator.translate('[[modules:chat.delete_message_confirm]]', function(translated) { - bootbox.confirm(translated, function(ok) { - if (!ok) { - return; - } - - socket.emit('modules.chats.delete', { - messageId: messageId, - roomId: roomId - }, function(err) { - if (err) { - return app.alertError(err.message); - } - - components.get('chat/message', messageId).slideUp('slow', function() { - $(this).remove(); - }); - }); - }); - }); - }; - - Chats.addSinceHandler = function(roomId, chatContentEl, sinceEl) { - sinceEl.on('click', function() { - var since = $(this).attr('data-since'); - sinceEl.removeClass('selected'); - $(this).addClass('selected'); - Chats.loadChatSince(roomId, chatContentEl, since); - return false; - }); - }; - - Chats.addRenameHandler = function(roomId, inputEl) { + Chats.addRenameHandler = function (roomId, inputEl) { var oldName = inputEl.val(); - inputEl.on('blur keypress', function(ev) { + inputEl.on('blur keypress', function (ev) { if (ev.type === 'keypress' && ev.keyCode !== 13) { return; } @@ -179,7 +168,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', if (oldName === newName) { return; } - socket.emit('modules.chats.renameRoom', {roomId: roomId, newName: newName}, function(err) { + socket.emit('modules.chats.renameRoom', {roomId: roomId, newName: newName}, function (err) { if (err) { return app.alertError(err.message); } @@ -189,38 +178,28 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }); }; - Chats.addSendHandlers = function(roomId, inputEl, sendEl) { - - inputEl.off('keypress').on('keypress', function(e) { + Chats.addSendHandlers = function (roomId, inputEl, sendEl) { + inputEl.off('keypress').on('keypress', function (e) { if (e.which === 13 && !e.shiftKey) { - Chats.sendMessage(roomId, inputEl); + messages.sendMessage(roomId, inputEl); return false; } }); - inputEl.off('keyup').on('keyup', function() { - var val = !!$(this).val(); - if ((val && $(this).attr('data-typing') === 'true') || (!val && $(this).attr('data-typing') === 'false')) { - return; - } - - $(this).attr('data-typing', val); - }); - - sendEl.off('click').on('click', function() { - Chats.sendMessage(roomId, inputEl); + sendEl.off('click').on('click', function () { + messages.sendMessage(roomId, inputEl); inputEl.focus(); return false; }); }; - Chats.createAutoComplete = function(element) { + Chats.createAutoComplete = function (element) { var data = { element: element, strategies: [], options: { zIndex: 20000, - listPosition: function(position) { + listPosition: function (position) { this.$el.css(this._applyPlacement(position)); this.$el.css('position', 'absolute'); return this; @@ -234,27 +213,27 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', } }; - Chats.createTagsInput = function(tagEl, data) { + Chats.createTagsInput = function (tagEl, data) { tagEl.tagsinput({ confirmKeys: [13, 44], trimValue: true }); if (data.users && data.users.length) { - data.users.forEach(function(user) { + data.users.forEach(function (user) { tagEl.tagsinput('add', $('<div/>').html(user.username).text()); }); } - tagEl.on('beforeItemAdd', function(event) { + tagEl.on('beforeItemAdd', function (event) { event.cancel = event.item === app.user.username; }); - tagEl.on('itemAdded', function(event) { + tagEl.on('itemAdded', function (event) { if (event.item === app.user.username) { return; } - socket.emit('modules.chats.addUserToRoom', {roomId: data.roomId, username: event.item}, function(err) { + socket.emit('modules.chats.addUserToRoom', {roomId: data.roomId, username: event.item}, function (err) { if (err) { app.alertError(err.message); tagEl.tagsinput('remove', event.item, {nouser: true}); @@ -262,7 +241,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }); }); - tagEl.on('beforeItemRemove', function(event) { + tagEl.on('beforeItemRemove', function (event) { if (event.options && event.options.nouser) { return; } @@ -277,11 +256,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', } }); - tagEl.on('itemRemoved', function(event) { + tagEl.on('itemRemoved', function (event) { if (event.options && event.options.nouser) { return; } - socket.emit('modules.chats.removeUserFromRoom', {roomId: data.roomId, username: event.item}, function(err) { + socket.emit('modules.chats.removeUserFromRoom', {roomId: data.roomId, username: event.item}, function (err) { if (err) { return app.alertError(err.message); } @@ -290,47 +269,32 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', var input = $('.users-tag-container').find('.bootstrap-tagsinput input'); - require(['autocomplete'], function(autocomplete) { + require(['autocomplete'], function (autocomplete) { autocomplete.user(input); }); }; - Chats.leave = function(el) { + Chats.leave = function (el) { var roomId = el.attr('data-roomid'); - socket.emit('modules.chats.leave', roomId, function(err) { + socket.emit('modules.chats.leave', roomId, function (err) { if (err) { return app.alertError(err.message); } if (parseInt(roomId, 10) === ajaxify.data.roomId) { - ajaxify.go('chats'); + ajaxify.go('user/' + ajaxify.data.userslug + '/chats'); } else { el.remove(); } }); }; - Chats.switchChat = function(roomid) { - ajaxify.go('chats/' + roomid); + Chats.switchChat = function (roomid) { + ajaxify.go('user/' + ajaxify.data.userslug + '/chats/' + roomid); }; - Chats.loadChatSince = function(roomId, chatContentEl, since) { - if (!roomId) { - return; - } - socket.emit('modules.chats.get', {roomId: roomId, since: since}, function(err, messages) { - if (err) { - return app.alertError(err.message); - } - - chatContentEl.find('[component="chat/message"]').remove(); - - Chats.appendChatMessage(chatContentEl, messages); - }); - }; - - Chats.addGlobalEventListeners = function() { + Chats.addGlobalEventListeners = function () { $(window).on('resize', Chats.resizeMainWindow); - $(window).on('mousemove keypress click', function() { + $(window).on('mousemove keypress click', function () { if (newMessage && ajaxify.data.roomId) { socket.emit('modules.chats.markRead', ajaxify.data.roomId); newMessage = false; @@ -338,134 +302,63 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }); }; - Chats.appendChatMessage = function(chatContentEl, data) { - - var lastSpeaker = parseInt(chatContentEl.find('.chat-message').last().attr('data-uid'), 10); - if (!Array.isArray(data)) { - data.newSet = lastSpeaker !== data.fromuid; - } - - Chats.parseMessage(data, function(html) { - onMessagesParsed(chatContentEl, html); - }); - }; - - function onMessagesParsed(chatContentEl, html) { - var newMessage = $(html); - - newMessage.appendTo(chatContentEl); - newMessage.find('.timeago').timeago(); - newMessage.find('img:not(.not-responsive)').addClass('img-responsive'); - Chats.scrollToBottom(chatContentEl); - } - - Chats.addSocketListeners = function() { - socket.on('event:chats.receive', function(data) { + Chats.addSocketListeners = function () { + socket.on('event:chats.receive', function (data) { if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) { newMessage = data.self === 0; data.message.self = data.self; - Chats.appendChatMessage($('.expanded-chat .chat-content'), data.message); + messages.appendChatMessage($('.expanded-chat .chat-content'), data.message); + } else { + if (ajaxify.currentPage.startsWith("chats")) { + var roomEl = $('[data-roomid=' + data.roomId + ']'); + + if (roomEl.length > 0) { + roomEl.addClass("unread"); + } else { + var recentEl = components.get('chat/recent'); + templates.parse('partials/chat_recent_room', { + rooms: { "roomId": data.roomId, "lastUser": data.message.fromUser, "usernames": data.message.fromUser.username, "unread": true } + }, function (html) { + translator.translate(html, function (translated) { + recentEl.prepend(translated); + }); + }); + } + } } }); - socket.on('event:user_status_change', function(data) { + socket.on('event:user_status_change', function (data) { app.updateUserStatus($('.chats-list [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); }); - Chats.onChatEdit(); + messages.onChatMessageEdit(); - socket.on('event:chats.roomRename', function(data) { + socket.on('event:chats.roomRename', function (data) { $('[component="chat/room/name"]').val($('<div/>').html(data.newName).text()); }); }; - Chats.onChatEdit = function() { - socket.on('event:chats.edit', function(data) { - data.messages.forEach(function(message) { - var self = parseInt(message.fromuid, 10) === parseInt(app.user.uid); - message.self = self ? 1 : 0; - Chats.parseMessage(message, function(html) { - var body = components.get('chat/message', message.messageId); - if (body.length) { - body.replaceWith(html); - components.get('chat/message', message.messageId).find('.timeago').timeago(); - } - }); - }); - }); - }; - - Chats.resizeMainWindow = function() { + Chats.resizeMainWindow = function () { var messagesList = $('.expanded-chat .chat-content'); if (messagesList.length) { - var margin = $('.expanded-chat ul').outerHeight(true) - $('.expanded-chat ul').height(), - inputHeight = $('.chat-input').outerHeight(true), - fromTop = messagesList.offset().top, - searchHeight = $('.chat-search').height(), - searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height(); + var margin = $('.expanded-chat ul').outerHeight(true) - $('.expanded-chat ul').height(); + var inputHeight = $('.chat-input').outerHeight(true); + var fromTop = messagesList.offset().top; + var searchHeight = $('.chat-search').height(); + var searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height(); messagesList.height($(window).height() - (fromTop + inputHeight + (margin * 4))); components.get('chat/recent').height($('.expanded-chat').height() - (searchHeight + searchListHeight)); - $('[component="chat/search/list"]').css('max-height', components.get('chat/recent').height()/2 + 'px'); + $('[component="chat/search/list"]').css('max-height', components.get('chat/recent').height() / 2 + 'px'); } Chats.setActive(); }; - Chats.sendMessage = function(roomId, inputEl) { - var msg = inputEl.val(); - var mid = inputEl.attr('data-mid'); - - if (msg.length > ajaxify.data.maximumChatMessageLength) { - return app.alertError('[[error:chat-message-too-long]]'); - } - - if (!msg.length) { - return; - } - - inputEl.val(''); - inputEl.removeAttr('data-mid'); - - if (!mid) { - socket.emit('modules.chats.send', { - roomId: roomId, - message: msg - }, function(err) { - if (err) { - if (err.message === '[[error:email-not-confirmed-chat]]') { - return app.showEmailConfirmWarning(err); - } - return app.alertError(err.message); - } - - sounds.play('chat-outgoing'); - }); - } else { - socket.emit('modules.chats.edit', { - roomId: roomId, - mid: mid, - message: msg - }, function(err) { - if (err) { - return app.alertError(err.message); - } - - }); - } - }; - - Chats.scrollToBottom = function(containerEl) { - if (containerEl.length) { - containerEl.scrollTop( - containerEl[0].scrollHeight - containerEl.height() - ); - } - }; - - Chats.setActive = function() { + Chats.setActive = function () { if (ajaxify.data.roomId) { socket.emit('modules.chats.markRead', ajaxify.data.roomId); $('.expanded-chat input').focus(); @@ -474,113 +367,6 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-primary'); }; - Chats.parseMessage = function(data, callback) { - templates.parse('partials/chat_message' + (Array.isArray(data) ? 's' : ''), { - messages: data - }, function(html) { - translator.translate(html, callback); - }); - }; - function loadMoreRecentChats() { - var recentChats = $('[component="chat/recent"]'); - if (recentChats.attr('loading')) { - return; - } - recentChats.attr('loading', 1); - socket.emit('modules.chats.getRecentChats', { - after: recentChats.attr('data-nextstart') - }, function(err, data) { - if (err) { - return app.alertError(err.message); - } - - if (data && data.rooms.length) { - onRecentChatsLoaded(data, function() { - recentChats.removeAttr('loading'); - recentChats.attr('data-nextstart', data.nextStart); - }); - } else { - recentChats.removeAttr('loading'); - } - }); - } - - function onRecentChatsLoaded(data, callback) { - if (!data.rooms.length) { - return callback(); - } - - app.parseAndTranslate('chats', 'rooms', data, function(html) { - $('[component="chat/recent"]').append(html); - html.find('.timeago').timeago(); - callback(); - }); - } - - Chats.handleSearch = function() { - var timeoutId = 0; - - components.get('chat/search').on('keyup', function() { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = 0; - } - - timeoutId = setTimeout(doSearch, 250); - }); - - function doSearch() { - var username = components.get('chat/search').val(); - var chatsListEl = $('[component="chat/search/list"]'); - - if (!username) { - return chatsListEl.empty(); - } - - socket.emit('user.search', { - query: username, - searchBy: 'username' - }, function(err, data) { - if (err) { - return app.alertError(err.message); - } - - chatsListEl.empty(); - - if (data.users.length === 0) { - return chatsListEl.translateHtml('<li><div><span>[[users:no-users-found]]</span></div></li>'); - } - - data.users.forEach(function(userObj) { - function createUserImage() { - return (userObj.picture ? - '<img src="' + userObj.picture + '" title="' + userObj.username +'" />' : - '<div class="user-icon" style="background-color: ' + userObj['icon:bgColor'] + '">' + userObj['icon:text'] + '</div>') + - '<i class="fa fa-circle status ' + userObj.status + '"></i> ' + userObj.username; - } - - var chatEl = $('<li component="chat/search/user" />') - .attr('data-uid', userObj.uid) - .appendTo(chatsListEl); - - chatEl.append(createUserImage()); - - chatEl.on('click', function() { - socket.emit('modules.chats.hasPrivateChat', userObj.uid, function(err, roomId) { - if (err) { - return app.alertError(err.message); - } - if (roomId) { - ajaxify.go('chats/' + roomId); - } else { - app.newChat(userObj.uid); - } - }); - }); - }); - }); - } - }; return Chats; }); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js new file mode 100644 index 0000000000..9d91910493 --- /dev/null +++ b/public/src/client/chats/messages.js @@ -0,0 +1,145 @@ +'use strict'; + +/* globals define, socket, app, ajaxify, templates, bootbox */ + +define('forum/chats/messages', ['components', 'sounds', 'translator'], function (components, sounds, translator) { + + var messages = {}; + + messages.sendMessage = function (roomId, inputEl) { + var msg = inputEl.val(); + var mid = inputEl.attr('data-mid'); + + if (msg.length > ajaxify.data.maximumChatMessageLength) { + return app.alertError('[[error:chat-message-too-long]]'); + } + + if (!msg.length) { + return; + } + + inputEl.val(''); + inputEl.removeAttr('data-mid'); + + if (!mid) { + socket.emit('modules.chats.send', { + roomId: roomId, + message: msg + }, function (err) { + if (err) { + if (err.message === '[[error:email-not-confirmed-chat]]') { + return app.showEmailConfirmWarning(err); + } + return app.alertError(err.message); + } + + sounds.play('chat-outgoing'); + }); + } else { + socket.emit('modules.chats.edit', { + roomId: roomId, + mid: mid, + message: msg + }, function (err) { + if (err) { + return app.alertError(err.message); + } + }); + } + }; + + messages.appendChatMessage = function (chatContentEl, data) { + + var lastSpeaker = parseInt(chatContentEl.find('.chat-message').last().attr('data-uid'), 10); + if (!Array.isArray(data)) { + data.newSet = lastSpeaker !== data.fromuid; + } + + messages.parseMessage(data, function (html) { + onMessagesParsed(chatContentEl, html); + }); + }; + + function onMessagesParsed(chatContentEl, html) { + var newMessage = $(html); + + newMessage.appendTo(chatContentEl); + newMessage.find('.timeago').timeago(); + newMessage.find('img:not(.not-responsive)').addClass('img-responsive'); + messages.scrollToBottom(chatContentEl); + } + + + messages.parseMessage = function (data, callback) { + templates.parse('partials/chat_message' + (Array.isArray(data) ? 's' : ''), { + messages: data + }, function (html) { + translator.translate(html, callback); + }); + }; + + + messages.scrollToBottom = function (containerEl) { + if (containerEl.length) { + containerEl.scrollTop( + containerEl[0].scrollHeight - containerEl.height() + ); + } + }; + + messages.prepEdit = function (inputEl, messageId, roomId) { + socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function (err, raw) { + if (err) { + return app.alertError(err.message); + } + // Populate the input field with the raw message content + if (inputEl.val().length === 0) { + // By setting the `data-mid` attribute, I tell the chat code that I am editing a + // message, instead of posting a new one. + inputEl.attr('data-mid', messageId).addClass('editing'); + inputEl.val(raw); + } + }); + }; + + messages.onChatMessageEdit = function () { + socket.on('event:chats.edit', function (data) { + data.messages.forEach(function (message) { + var self = parseInt(message.fromuid, 10) === parseInt(app.user.uid); + message.self = self ? 1 : 0; + messages.parseMessage(message, function (html) { + var body = components.get('chat/message', message.messageId); + if (body.length) { + body.replaceWith(html); + components.get('chat/message', message.messageId).find('.timeago').timeago(); + } + }); + }); + }); + }; + + messages.delete = function (messageId, roomId) { + translator.translate('[[modules:chat.delete_message_confirm]]', function (translated) { + bootbox.confirm(translated, function (ok) { + if (!ok) { + return; + } + + socket.emit('modules.chats.delete', { + messageId: messageId, + roomId: roomId + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + components.get('chat/message', messageId).slideUp('slow', function () { + $(this).remove(); + }); + }); + }); + }); + }; + + return messages; +}); \ No newline at end of file diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js new file mode 100644 index 0000000000..b4b9ba115b --- /dev/null +++ b/public/src/client/chats/recent.js @@ -0,0 +1,58 @@ +'use strict'; + +/* globals define, socket, app */ + +define('forum/chats/recent', function () { + + var recent = {}; + + recent.init = function () { + $('[component="chat/recent"]').on('scroll', function () { + var $this = $(this); + var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; + if ($this.scrollTop() > bottom) { + loadMoreRecentChats(); + } + }); + }; + + function loadMoreRecentChats() { + var recentChats = $('[component="chat/recent"]'); + if (recentChats.attr('loading')) { + return; + } + recentChats.attr('loading', 1); + socket.emit('modules.chats.getRecentChats', { + uid: ajaxify.data.uid, + after: recentChats.attr('data-nextstart') + }, function (err, data) { + if (err) { + return app.alertError(err.message); + } + + if (data && data.rooms.length) { + onRecentChatsLoaded(data, function () { + recentChats.removeAttr('loading'); + recentChats.attr('data-nextstart', data.nextStart); + }); + } else { + recentChats.removeAttr('loading'); + } + }); + } + + function onRecentChatsLoaded(data, callback) { + if (!data.rooms.length) { + return callback(); + } + + app.parseAndTranslate('chats', 'rooms', data, function (html) { + $('[component="chat/recent"]').append(html); + html.find('.timeago').timeago(); + callback(); + }); + } + + + return recent; +}); \ No newline at end of file diff --git a/public/src/client/chats/search.js b/public/src/client/chats/search.js new file mode 100644 index 0000000000..82b3923d2d --- /dev/null +++ b/public/src/client/chats/search.js @@ -0,0 +1,89 @@ +'use strict'; + +/* globals define, socket, app */ + +define('forum/chats/search', ['components'], function (components) { + + var search = {}; + + search.init = function () { + var timeoutId = 0; + + components.get('chat/search').on('keyup', function () { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = 0; + } + + timeoutId = setTimeout(doSearch, 250); + }); + }; + + function doSearch() { + var username = components.get('chat/search').val(); + var chatsListEl = $('[component="chat/search/list"]'); + + if (!username) { + return chatsListEl.empty(); + } + + socket.emit('user.search', { + query: username, + searchBy: 'username' + }, function (err, data) { + if (err) { + return app.alertError(err.message); + } + + displayResults(chatsListEl, data); + }); + } + + function displayResults(chatsListEl, data) { + chatsListEl.empty(); + + if (!data.users.length) { + return chatsListEl.translateHtml('<li><div><span>[[users:no-users-found]]</span></div></li>'); + } + + data.users.forEach(function (userObj) { + var chatEl = displayUser(chatsListEl, userObj); + onUserClick(chatEl, userObj); + }); + } + + function displayUser(chatsListEl, userObj) { + function createUserImage() { + return (userObj.picture ? + '<img src="' + userObj.picture + '" title="' + userObj.username + '" />' : + '<div class="user-icon" style="background-color: ' + userObj['icon:bgColor'] + '">' + userObj['icon:text'] + '</div>') + + '<i class="fa fa-circle status ' + userObj.status + '"></i> ' + userObj.username; + } + + var chatEl = $('<li component="chat/search/user" />') + .attr('data-uid', userObj.uid) + .appendTo(chatsListEl); + + chatEl.append(createUserImage()); + return chatEl; + } + + function onUserClick(chatEl, userObj) { + chatEl.on('click', function () { + socket.emit('modules.chats.hasPrivateChat', userObj.uid, function (err, roomId) { + if (err) { + return app.alertError(err.message); + } + if (roomId) { + require(['forum/chats'], function (chats) { + chats.switchChat(roomId); + }); + } else { + app.newChat(userObj.uid); + } + }); + }); + } + + return search; +}); diff --git a/public/src/client/compose.js b/public/src/client/compose.js index c50ffe9945..4b2bb87b4e 100644 --- a/public/src/client/compose.js +++ b/public/src/client/compose.js @@ -2,10 +2,10 @@ /* globals define */ -define('forum/compose', [], function() { +define('forum/compose', [], function () { var Compose = {}; - Compose.init = function() { + Compose.init = function () { var container = $('.composer'); if (container.length) { diff --git a/public/src/client/footer.js b/public/src/client/footer.js index d8e54d4fc3..d10f47e317 100644 --- a/public/src/client/footer.js +++ b/public/src/client/footer.js @@ -1,7 +1,7 @@ "use strict"; /*globals define, app, socket*/ -define('forum/footer', ['notifications', 'chat', 'components', 'translator'], function(Notifications, Chat, components, translator) { +define('forum/footer', ['notifications', 'chat', 'components', 'translator'], function (Notifications, Chat, components, translator) { Notifications.prepareDOM(); Chat.prepareDOM(); @@ -13,6 +13,12 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu .attr('data-content', count > 99 ? '99+' : count); } + function updateUnreadNewTopicCount(count) { + $('#unread-new-count i') + .toggleClass('unread-count', count > 0) + .attr('data-content', count > 99 ? '99+' : count); + } + function updateUnreadChatCount(count) { components.get('chat/icon') .toggleClass('unread-count', count > 0) @@ -43,11 +49,13 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu $('[data-tid="' + tid + '"]').addClass('unread'); } - $(window).on('action:ajaxify.end', function(ev, data) { - var tid = data.url.match(/^topic\/(\d+)/); + $(window).on('action:ajaxify.end', function (ev, data) { + if (data.url) { + var tid = data.url.match(/^topic\/(\d+)/); - if (tid && tid[1]) { - delete unreadTopics[tid[1]]; + if (tid && tid[1]) { + delete unreadTopics[tid[1]]; + } } }); @@ -55,12 +63,13 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu } if (app.user.uid) { - socket.emit('user.getUnreadCounts', function(err, data) { + socket.emit('user.getUnreadCounts', function (err, data) { if (err) { return app.alert(err.message); } updateUnreadTopicCount(data.unreadTopicCount); + updateUnreadNewTopicCount(data.unreadNewTopicCount); updateUnreadChatCount(data.unreadChatCount); Notifications.updateNotifCount(data.unreadNotificationCount); }); diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 49f1d221f6..e8c449f5f6 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,5 +1,5 @@ "use strict"; -/* globals define, socket, ajaxify, app, bootbox, utils, RELATIVE_PATH */ +/* globals define, socket, ajaxify, app, bootbox, utils, config */ define('forum/groups/details', [ 'forum/groups/memberlist', @@ -8,12 +8,12 @@ define('forum/groups/details', [ 'coverPhoto', 'uploader', 'vendor/colorpicker/colorpicker' -], function(memberList, iconSelect, components, coverPhoto, uploader) { +], function (memberList, iconSelect, components, coverPhoto, uploader) { var Details = {}; var groupName; - Details.init = function() { + Details.init = function () { var detailsPage = components.get('groups/container'); groupName = ajaxify.data.group.name; @@ -22,19 +22,19 @@ define('forum/groups/details', [ Details.prepareSettings(); coverPhoto.init(components.get('groups/cover'), - function(imageData, position, callback) { + function (imageData, position, callback) { socket.emit('groups.cover.update', { groupName: groupName, imageData: imageData, position: position }, callback); }, - function() { + function () { uploader.show({ title: '[[groups:upload-group-cover]]', route: config.relative_path + '/api/groups/uploadpicture', params: {groupName: groupName} - }, function(imageUrlOnServer) { + }, function (imageUrlOnServer) { components.get('groups/cover').css('background-image', 'url(' + imageUrlOnServer + ')'); }); }, @@ -48,10 +48,10 @@ define('forum/groups/details', [ components.get('groups/activity').find('.content img:not(.not-responsive)').addClass('img-responsive'); - detailsPage.on('click', '[data-action]', function() { + detailsPage.on('click', '[data-action]', function () { var btnEl = $(this), userRow = btnEl.parents('[data-uid]'), - ownerFlagEl = userRow.find('.member-name i'), + ownerFlagEl = userRow.find('.member-name > i'), isOwner = !ownerFlagEl.hasClass('invisible') ? true : false, uid = userRow.attr('data-uid'), action = btnEl.attr('data-action'); @@ -61,7 +61,7 @@ define('forum/groups/details', [ socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { toUid: uid, groupName: groupName - }, function(err) { + }, function (err) { if (!err) { ownerFlagEl.toggleClass('invisible'); } else { @@ -74,7 +74,7 @@ define('forum/groups/details', [ socket.emit('groups.kick', { uid: uid, groupName: groupName - }, function(err) { + }, function (err) { if (!err) { userRow.slideUp().remove(); } else { @@ -104,7 +104,7 @@ define('forum/groups/details', [ socket.emit('groups.' + action, { toUid: uid, groupName: groupName - }, function(err) { + }, function (err) { if (!err) { ajaxify.refresh(); } else { @@ -116,7 +116,7 @@ define('forum/groups/details', [ }); }; - Details.prepareSettings = function() { + Details.prepareSettings = function () { var settingsFormEl = components.get('groups/settings'), colorBtn = settingsFormEl.find('[data-action="color-select"]'), colorValueEl = settingsFormEl.find('[name="labelColor"]'), @@ -130,31 +130,31 @@ define('forum/groups/details', [ // Add color picker to settings form colorBtn.ColorPicker({ color: colorValueEl.val() || '#000', - onChange: function(hsb, hex) { + onChange: function (hsb, hex) { colorValueEl.val('#' + hex); previewEl.css('background-color', '#' + hex); }, - onShow: function(colpkr) { + onShow: function (colpkr) { $(colpkr).css('z-index', 1051); } }); // Add icon selection interface - iconBtn.on('click', function() { - iconSelect.init(previewIcon, function() { + iconBtn.on('click', function () { + iconSelect.init(previewIcon, function () { iconValueEl.val(previewIcon.val()); }); }); // If the user title changes, update that too - userTitleEl.on('keyup', function() { + userTitleEl.on('keyup', function () { var icon = previewIcon.detach(); previewEl.text(' ' + (this.value || settingsFormEl.find('#name').val())); previewEl.prepend(icon); }); // Disable user title customisation options if the the user title itself is disabled - userTitleEnabledEl.on('change', function() { + userTitleEnabledEl.on('change', function () { var customOpts = components.get('groups/userTitleOption'); if (this.checked) { @@ -167,16 +167,16 @@ define('forum/groups/details', [ }); }; - Details.update = function() { + Details.update = function () { var settingsFormEl = components.get('groups/settings'), checkboxes = settingsFormEl.find('input[type="checkbox"][name]'); if (settingsFormEl.length) { - require(['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() { + require(['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function () { var settings = settingsFormEl.serializeObject(); // Fix checkbox values - checkboxes.each(function(idx, inputEl) { + checkboxes.each(function (idx, inputEl) { inputEl = $(inputEl); if (inputEl.length) { settings[inputEl.attr('name')] = inputEl.prop('checked'); @@ -186,7 +186,7 @@ define('forum/groups/details', [ socket.emit('groups.update', { groupName: groupName, values: settings - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } @@ -205,14 +205,14 @@ define('forum/groups/details', [ } }; - Details.deleteGroup = function() { - bootbox.confirm('Are you sure you want to delete the group: ' + utils.escapeHTML(groupName), function(confirm) { + Details.deleteGroup = function () { + bootbox.confirm('Are you sure you want to delete the group: ' + utils.escapeHTML(groupName), function (confirm) { if (confirm) { - bootbox.prompt('Please enter the name of this group in order to delete it:', function(response) { + bootbox.prompt('Please enter the name of this group in order to delete it:', function (response) { if (response === groupName) { socket.emit('groups.delete', { groupName: groupName - }, function(err) { + }, function (err) { if (!err) { app.alertSuccess('[[groups:event.deleted, ' + utils.escapeHTML(groupName) + ']]'); ajaxify.go('groups'); @@ -227,29 +227,47 @@ define('forum/groups/details', [ }; function handleMemberInvitations() { - if (ajaxify.data.group.isOwner) { - var searchInput = $('[component="groups/members/invite"]'); - require(['autocomplete'], function(autocomplete) { - autocomplete.user(searchInput, function(e, selected) { - socket.emit('groups.issueInvite', { - toUid: selected.item.user.uid, - groupName: ajaxify.data.group.name - }, function(err) { - if (!err) { - ajaxify.refresh(); - } else { - app.alertError(err.message); - } - }); + if (!ajaxify.data.group.isOwner) { + return; + } + + var searchInput = $('[component="groups/members/invite"]'); + require(['autocomplete'], function (autocomplete) { + autocomplete.user(searchInput, function (event, selected) { + socket.emit('groups.issueInvite', { + toUid: selected.item.user.uid, + groupName: ajaxify.data.group.name + }, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); }); }); - } + }); + + $('[component="groups/members/bulk-invite-button"]').on('click', function () { + var usernames = $('[component="groups/members/bulk-invite"]').val(); + if (!usernames) { + return false; + } + socket.emit('groups.issueMassInvite', { + usernames: usernames, + groupName: ajaxify.data.group.name + }, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); + }); + return false; + }); } function removeCover() { socket.emit('groups.cover.remove', { groupName: ajaxify.data.group.name - }, function(err) { + }, function (err) { if (!err) { ajaxify.refresh(); } else { @@ -259,4 +277,4 @@ define('forum/groups/details', [ } return Details; -}); \ No newline at end of file +}); diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index 606a460dff..55ac232eb1 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -1,21 +1,21 @@ "use strict"; /* globals app, define, ajaxify, socket, bootbox, utils, templates */ -define('forum/groups/list', ['forum/infinitescroll'], function(infinitescroll) { +define('forum/groups/list', ['forum/infinitescroll'], function (infinitescroll) { var Groups = {}; - Groups.init = function() { + Groups.init = function () { var groupsEl = $('#groups-list'); infinitescroll.init(Groups.loadMoreGroups); // Group creation - $('button[data-action="new"]').on('click', function() { - bootbox.prompt('[[groups:new-group.group_name]]', function(name) { + $('button[data-action="new"]').on('click', function () { + bootbox.prompt('[[groups:new-group.group_name]]', function (name) { if (name && name.length) { socket.emit('groups.create', { name: name - }, function(err) { + }, function (err) { if (!err) { ajaxify.go('groups/' + utils.slugify(name)); } else { @@ -31,12 +31,12 @@ define('forum/groups/list', ['forum/infinitescroll'], function(infinitescroll) { // Group searching $('#search-text').on('keyup', Groups.search); $('#search-button').on('click', Groups.search); - $('#search-sort').on('change', function() { + $('#search-sort').on('change', function () { ajaxify.go('groups?sort=' + $('#search-sort').val()); }); }; - Groups.loadMoreGroups = function(direction) { + Groups.loadMoreGroups = function (direction) { if (direction < 0) { return; } @@ -44,11 +44,11 @@ define('forum/groups/list', ['forum/infinitescroll'], function(infinitescroll) { infinitescroll.loadMore('groups.loadMore', { sort: $('#search-sort').val(), after: $('[component="groups/container"]').attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (data && data.groups.length) { templates.parse('partials/groups/list', { groups: data.groups - }, function(html) { + }, function (html) { $('#groups-list').append(html); done(); }); @@ -62,7 +62,7 @@ define('forum/groups/list', ['forum/infinitescroll'], function(infinitescroll) { }); } - Groups.search = function() { + Groups.search = function () { var groupsEl = $('#groups-list'), queryEl = $('#search-text'), sortEl = $('#search-sort'); @@ -73,16 +73,16 @@ define('forum/groups/list', ['forum/infinitescroll'], function(infinitescroll) { sort: sortEl.val(), filterHidden: true } - }, function(err, groups) { + }, function (err, groups) { if (err) { return app.alertError(err.message); } - groups = groups.filter(function(group) { + groups = groups.filter(function (group) { return group.name !== 'registered-users' && group.name !== 'guests'; }); templates.parse('partials/groups/list', { groups: groups - }, function(html) { + }, function (html) { groupsEl.empty().append(html); }); }); diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js index 7fc3bb6781..e7e6d8b1f1 100644 --- a/public/src/client/groups/memberlist.js +++ b/public/src/client/groups/memberlist.js @@ -1,13 +1,13 @@ "use strict"; /* globals define, socket, ajaxify, app */ -define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], function(components, infinitescroll) { +define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], function (components, infinitescroll) { var MemberList = {}; var searchInterval; var groupName; - MemberList.init = function() { + MemberList.init = function () { groupName = ajaxify.data.group.name; handleMemberSearch(); @@ -15,19 +15,19 @@ define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], functi }; function handleMemberSearch() { - $('[component="groups/members/search"]').on('keyup', function() { + $('[component="groups/members/search"]').on('keyup', function () { var query = $(this).val(); if (searchInterval) { clearInterval(searchInterval); searchInterval = 0; } - searchInterval = setTimeout(function() { - socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function(err, results) { + searchInterval = setTimeout(function () { + socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function (err, results) { if (err) { return app.alertError(err.message); } - parseAndTranslate(results.users, function(html) { + parseAndTranslate(results.users, function (html) { $('[component="groups/members"] tbody').html(html); $('[component="groups/members"]').attr('data-nextstart', 20); }); @@ -37,7 +37,7 @@ define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], functi } function handleMemberInfiniteScroll() { - $('[component="groups/members"] tbody').on('scroll', function() { + $('[component="groups/members"] tbody').on('scroll', function () { var $this = $(this); var bottom = ($this[0].scrollHeight - $this.innerHeight()) * 0.9; @@ -57,13 +57,13 @@ define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], functi socket.emit('groups.loadMoreMembers', { groupName: groupName, after: members.attr('data-nextstart') - }, function(err, data) { + }, function (err, data) { if (err) { return app.alertError(err.message); } if (data && data.users.length) { - onMembersLoaded(data.users, function() { + onMembersLoaded(data.users, function () { members.removeAttr('loading'); members.attr('data-nextstart', data.nextStart); }); @@ -74,11 +74,11 @@ define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], functi } function onMembersLoaded(users, callback) { - users = users.filter(function(user) { + users = users.filter(function (user) { return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length; }); - parseAndTranslate(users, function(html) { + parseAndTranslate(users, function (html) { $('[component="groups/members"] tbody').append(html); callback(); }); diff --git a/public/src/client/infinitescroll.js b/public/src/client/infinitescroll.js index 42443aa27e..6ae9b5cbbc 100644 --- a/public/src/client/infinitescroll.js +++ b/public/src/client/infinitescroll.js @@ -1,8 +1,8 @@ 'use strict'; -/* globals define, socket, ajaxify, templates, app */ +/* globals define, socket, app */ -define('forum/infinitescroll', ['translator'], function(translator) { +define('forum/infinitescroll', function () { var scroll = {}; var callback; @@ -10,13 +10,13 @@ define('forum/infinitescroll', ['translator'], function(translator) { var loadingMore = false; var container; - scroll.init = function(el, cb) { + scroll.init = function (el, cb) { if (typeof el === 'function') { callback = el; - container = $(document); + container = $('body'); } else { callback = cb; - container = el || $(document); + container = el || $('body'); } $(window).off('scroll', onScroll).on('scroll', onScroll); @@ -47,23 +47,27 @@ define('forum/infinitescroll', ['translator'], function(translator) { previousScrollTop = currentScrollTop; } - scroll.loadMore = function(method, data, callback) { + scroll.loadMore = function (method, data, callback) { if (loadingMore) { return; } loadingMore = true; - socket.emit(method, data, function(err, data) { + + var hookData = {method: method, data: data}; + $(window).trigger('action:infinitescroll.loadmore', hookData); + + socket.emit(hookData.method, hookData.data, function (err, data) { if (err) { loadingMore = false; return app.alertError(err.message); } - callback(data, function() { + callback(data, function () { loadingMore = false; }); }); }; - scroll.removeExtra = function(els, direction, count) { + scroll.removeExtra = function (els, direction, count) { if (els.length <= count) { return; } diff --git a/public/src/client/login.js b/public/src/client/login.js index ecbd607f05..254c90d583 100644 --- a/public/src/client/login.js +++ b/public/src/client/login.js @@ -1,15 +1,15 @@ "use strict"; /* global define, app, config, RELATIVE_PATH */ -define('forum/login', ['csrf', 'translator'], function(csrf, translator) { +define('forum/login', ['translator'], function (translator) { var Login = {}; - Login.init = function() { + Login.init = function () { var errorEl = $('#login-error-notify'), submitEl = $('#login'), formEl = $('#login-form'); - submitEl.on('click', function(e) { + submitEl.on('click', function (e) { e.preventDefault(); if (!$('#username').val() || !$('#password').val()) { @@ -25,33 +25,48 @@ define('forum/login', ['csrf', 'translator'], function(csrf, translator) { submitEl.addClass('disabled'); formEl.ajaxSubmit({ headers: { - 'x-csrf-token': csrf.get() + 'x-csrf-token': config.csrf_token }, - success: function(data, status) { + success: function (data, status) { window.location.href = data + '?loggedin'; }, - error: function(data, status) { - errorEl.find('p').translateText(data.responseText); - errorEl.show(); - submitEl.removeClass('disabled'); + error: function (data, status) { + if (data.status === 403 && data.responseText === 'Forbidden') { + window.location.href = config.relative_path + '/login?error=csrf-invalid'; + } else { + errorEl.find('p').translateText(data.responseText); + errorEl.show(); + submitEl.removeClass('disabled'); + + // Select the entire password if that field has focus + if ($('#password:focus').size()) { + $('#password').select(); + } + } } }); } }); - $('#login-error-notify button').on('click', function(e) { + $('#login-error-notify button').on('click', function (e) { e.preventDefault(); errorEl.hide(); return false; }); - $('#content #username').focus(); + if ($('#content #username').attr('readonly')) { + $('#content #password').val('').focus(); + } else { + $('#content #username').focus(); + } + // Add "returnTo" data if present - if (app.previousUrl) { + if (app.previousUrl && $('#returnTo').length === 0) { var returnToEl = document.createElement('input'); returnToEl.type = 'hidden'; returnToEl.name = 'returnTo'; + returnToEl.id = 'returnTo'; returnToEl.value = app.previousUrl; $(returnToEl).appendTo(formEl); } diff --git a/public/src/client/notifications.js b/public/src/client/notifications.js index 6d353ddc21..baeaa262cd 100644 --- a/public/src/client/notifications.js +++ b/public/src/client/notifications.js @@ -2,14 +2,14 @@ /* globals define, socket, app */ -define('forum/notifications', ['components', 'notifications', 'forum/infinitescroll'], function(components, notifs, infinitescroll) { +define('forum/notifications', ['components', 'notifications', 'forum/infinitescroll'], function (components, notifs, infinitescroll) { var Notifications = {}; - Notifications.init = function() { + Notifications.init = function () { var listEl = $('.notifications-list'); - listEl.on('click', '[component="notifications/item/link"]', function() { + listEl.on('click', '[component="notifications/item/link"]', function () { var nid = $(this).parents('[data-nid]').attr('data-nid'); - socket.emit('notifications.markRead', nid, function(err) { + socket.emit('notifications.markRead', nid, function (err) { if (err) { return app.alertError(err); } @@ -18,8 +18,8 @@ define('forum/notifications', ['components', 'notifications', 'forum/infinitescr $('.timeago').timeago(); - components.get('notifications/mark_all').on('click', function() { - socket.emit('notifications.markAllRead', function(err) { + components.get('notifications/mark_all').on('click', function () { + socket.emit('notifications.markAllRead', function (err) { if (err) { return app.alertError(err.message); } @@ -39,7 +39,7 @@ define('forum/notifications', ['components', 'notifications', 'forum/infinitescr var notifList = $('.notifications-list'); infinitescroll.loadMore('notifications.loadMore', { after: notifList.attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (!data) { return done(); } @@ -47,7 +47,7 @@ define('forum/notifications', ['components', 'notifications', 'forum/infinitescr if (!data.notifications || !data.notifications.length) { return done(); } - app.parseAndTranslate('notifications', 'notifications', {notifications: data.notifications}, function(html) { + app.parseAndTranslate('notifications', 'notifications', {notifications: data.notifications}, function (html) { notifList.append(html); html.find('.timeago').timeago(); done(); diff --git a/public/src/client/pagination.js b/public/src/client/pagination.js index 3bac155dc5..6cbd7102f0 100644 --- a/public/src/client/pagination.js +++ b/public/src/client/pagination.js @@ -1,20 +1,20 @@ 'use strict'; /*global define, utils, ajaxify, bootbox*/ -define('forum/pagination', function() { +define('forum/pagination', function () { var pagination = {}; - pagination.init = function() { - $('body').on('click', '[component="pagination/select-page"]', function() { - bootbox.prompt('[[global:enter_page_number]]', function(pageNum) { + pagination.init = function () { + $('body').on('click', '[component="pagination/select-page"]', function () { + bootbox.prompt('[[global:enter_page_number]]', function (pageNum) { pagination.loadPage(pageNum); }); return false; }); }; - pagination.loadPage = function(page, callback) { - callback = callback || function() {}; + pagination.loadPage = function (page, callback) { + callback = callback || function () {}; page = parseInt(page, 10); if (!utils.isNumber(page) || page < 1 || page > ajaxify.data.pagination.pageCount) { return; diff --git a/public/src/client/popular.js b/public/src/client/popular.js index f7f0cd568e..00fb6937e2 100644 --- a/public/src/client/popular.js +++ b/public/src/client/popular.js @@ -2,10 +2,10 @@ /* globals define, app, socket*/ -define('forum/popular', ['components'], function(components) { +define('forum/popular', ['components'], function (components) { var Popular = {}; - Popular.init = function() { + Popular.init = function () { app.enterRoom('popular_topics'); components.get('popular/tab') diff --git a/public/src/client/recent.js b/public/src/client/recent.js index e4223d519a..15b37eaf19 100644 --- a/public/src/client/recent.js +++ b/public/src/client/recent.js @@ -1,29 +1,33 @@ 'use strict'; -/* globals define, app, socket, utils */ +/* globals define, app, socket, utils, ajaxify, config */ -define('forum/recent', ['forum/infinitescroll', 'components'], function(infinitescroll, components) { +define('forum/recent', ['forum/infinitescroll', 'components'], function (infinitescroll, components) { var Recent = {}; - var newTopicCount = 0, - newPostCount = 0; + var newTopicCount = 0; + var newPostCount = 0; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { if (ajaxify.currentPage !== data.url) { Recent.removeListeners(); } }); - Recent.init = function() { + Recent.init = function () { app.enterRoom('recent_topics'); Recent.watchForNewPosts(); - $('#new-topics-alert').on('click', function() { + $('#new-topics-alert').on('click', function () { $(this).addClass('hide'); }); - infinitescroll.init(Recent.loadMoreTopics); + if (!config.usePagination) { + infinitescroll.init(Recent.loadMoreTopics); + } + + $(window).trigger('action:topics.loaded', {topics: ajaxify.data.topics}); }; Recent.watchForNewPosts = function () { @@ -35,21 +39,61 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite }; function onNewTopic(data) { + if (ajaxify.data.selectedCategory && parseInt(ajaxify.data.selectedCategory.cid, 10) !== parseInt(data.cid, 10)) { + return; + } + + if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.url === 'unread/watched') { + return; + } + ++newTopicCount; Recent.updateAlertText(); } function onNewPost(data) { - ++newPostCount; - Recent.updateAlertText(); + function showAlert() { + ++newPostCount; + Recent.updateAlertText(); + } + + var post = data.posts[0]; + if (!post || !post.topic) { + return; + } + if (parseInt(post.topic.mainPid, 10) === parseInt(post.pid, 10)) { + return; + } + + if (ajaxify.data.selectedCategory && parseInt(ajaxify.data.selectedCategory.cid, 10) !== parseInt(post.topic.cid, 10)) { + return; + } + + if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.url === 'unread/new') { + return; + } + + if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.url === 'unread/watched') { + socket.emit('topics.isFollowed', post.tid, function (err, isFollowed) { + if (err) { + app.alertError(err.message); + } + if (isFollowed) { + showAlert(); + } + }); + return; + } + + showAlert(); } - Recent.removeListeners = function() { + Recent.removeListeners = function () { socket.removeListener('event:new_topic', onNewTopic); socket.removeListener('event:new_post', onNewPost); }; - Recent.updateAlertText = function() { + Recent.updateAlertText = function () { var text = ''; if (newTopicCount === 0) { @@ -64,7 +108,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite } else if (newPostCount === 1) { text = '[[recent:there-is-a-new-topic-and-a-new-post]]'; } else if (newPostCount > 1) { - text = '[[recent:there-is-a-new-topic-and-new-posts, ' + newPostCount +']]'; + text = '[[recent:there-is-a-new-topic-and-new-posts, ' + newPostCount + ']]'; } } else if (newTopicCount > 1) { if (newPostCount === 0) { @@ -72,7 +116,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite } else if (newPostCount === 1) { text = '[[recent:there-are-new-topics-and-a-new-post, ' + newTopicCount + ']]'; } else if (newPostCount > 1) { - text = '[[recent:there-are-new-topics-and-new-posts, ' + newTopicCount + ', ' + newPostCount +']]'; + text = '[[recent:there-are-new-topics-and-new-posts, ' + newTopicCount + ', ' + newPostCount + ']]'; } } @@ -82,15 +126,15 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite $('#category-no-topics').addClass('hide'); }; - Recent.loadMoreTopics = function(direction) { + Recent.loadMoreTopics = function (direction) { if(direction < 0 || !$('[component="category"]').length) { return; } infinitescroll.loadMore('topics.loadMoreFromSet', { after: $('[component="category"]').attr('data-nextstart'), - set: 'topics:recent' - }, function(data, done) { + set: $('[component="category"]').attr('data-set') ? $('[component="category"]').attr('data-set') : 'topics:recent' + }, function (data, done) { if (data.topics && data.topics.length) { Recent.onTopicsLoaded('recent', data.topics, false, done); } else { @@ -100,9 +144,9 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite }); }; - Recent.onTopicsLoaded = function(templateName, topics, showSelect, callback) { + Recent.onTopicsLoaded = function (templateName, topics, showSelect, callback) { - topics = topics.filter(function(topic) { + topics = topics.filter(function (topic) { return !components.get('category/topic', 'tid', topic.tid).length; }); @@ -110,7 +154,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function(infinite return callback(); } - app.parseAndTranslate(templateName, 'topics', {topics: topics, showSelect: showSelect}, function(html) { + app.parseAndTranslate(templateName, 'topics', {topics: topics, showSelect: showSelect}, function (html) { $('#category-no-topics').remove(); $('[component="category"]').append(html); diff --git a/public/src/client/register.js b/public/src/client/register.js index da86c0231d..01dfa4dfc1 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -3,24 +3,23 @@ /* globals define, app, utils, socket, config, ajaxify, bootbox */ -define('forum/register', ['csrf', 'translator'], function(csrf, translator) { +define('forum/register', ['translator'], function (translator) { var Register = {}, validationError = false, - successIcon = '<i class="fa fa-check"></i>'; + successIcon = ''; - Register.init = function() { + Register.init = function () { var email = $('#email'), username = $('#username'), password = $('#password'), password_confirm = $('#password-confirm'), - register = $('#register'), - agreeTerms = $('#agree-terms'); + register = $('#register'); handleLanguageOverride(); $('#referrer').val(app.previousUrl); - email.on('blur', function() { + email.on('blur', function () { if (email.val().length) { validateEmail(email.val()); } @@ -33,23 +32,23 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { } // Update the "others can mention you via" text - username.on('keyup', function() { + username.on('keyup', function () { $('#yourUsername').text(this.value.length > 0 ? utils.slugify(this.value) : 'username'); }); - username.on('blur', function() { + username.on('blur', function () { if (username.val().length) { validateUsername(username.val()); } }); - password.on('blur', function() { + password.on('blur', function () { if (password.val().length) { validatePassword(password.val(), password_confirm.val()); } }); - password_confirm.on('blur', function() { + password_confirm.on('blur', function () { if (password_confirm.val().length) { validatePasswordConfirm(password.val(), password_confirm.val()); } @@ -60,17 +59,17 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { validatePassword(password.val(), password_confirm.val()); validatePasswordConfirm(password.val(), password_confirm.val()); - validateEmail(email.val(), function() { + validateEmail(email.val(), function () { validateUsername(username.val(), callback); }); } - register.on('click', function(e) { + register.on('click', function (e) { var registerBtn = $(this); var errorEl = $('#register-error-notify'); errorEl.addClass('hidden'); e.preventDefault(); - validateForm(function() { + validateForm(function () { if (validationError) { return; } @@ -79,9 +78,9 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { registerBtn.parents('form').ajaxSubmit({ headers: { - 'x-csrf-token': csrf.get() + 'x-csrf-token': config.csrf_token }, - success: function(data) { + success: function (data) { registerBtn.removeClass('disabled'); if (!data) { return; @@ -89,40 +88,32 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { if (data.referrer) { window.location.href = data.referrer; } else if (data.message) { - require(['translator'], function(translator) { - translator.translate(data.message, function(msg) { + require(['translator'], function (translator) { + translator.translate(data.message, function (msg) { bootbox.alert(msg); ajaxify.go('/'); }); }); } }, - error: function(data) { - translator.translate(data.responseText, config.defaultLang, function(translated) { - errorEl.find('p').text(translated); - errorEl.removeClass('hidden'); - registerBtn.removeClass('disabled'); + error: function (data) { + translator.translate(data.responseText, config.defaultLang, function (translated) { + if (data.status === 403 && data.responseText === 'Forbidden') { + window.location.href = config.relative_path + '/register?error=csrf-invalid'; + } else { + errorEl.find('p').text(translated); + errorEl.removeClass('hidden'); + registerBtn.removeClass('disabled'); + } }); } }); }); }); - - if (agreeTerms.length) { - agreeTerms.on('click', function() { - if ($(this).prop('checked')) { - register.removeAttr('disabled'); - } else { - register.attr('disabled', 'disabled'); - } - }); - - register.attr('disabled', 'disabled'); - } }; function validateEmail(email, callback) { - callback = callback || function() {}; + callback = callback || function () {}; var email_notify = $('#email-notify'); if (!utils.isEmailValid(email)) { @@ -132,7 +123,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { socket.emit('user.emailExists', { email: email - }, function(err, exists) { + }, function (err, exists) { if (err) { app.alertError(err.message); return callback(); @@ -149,7 +140,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { } function validateUsername(username, callback) { - callback = callback || function() {}; + callback = callback || function () {}; var username_notify = $('#username-notify'); @@ -162,7 +153,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { } else { socket.emit('user.exists', { username: username - }, function(err, exists) { + }, function (err, exists) { if (err) { return app.alertError(err.message); } @@ -217,22 +208,22 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { } function showError(element, msg) { - translator.translate(msg, function(msg) { + translator.translate(msg, function (msg) { element.html(msg); element.parent() - .removeClass('alert-success') - .addClass('alert-danger'); + .removeClass('register-success') + .addClass('register-danger'); element.show(); }); validationError = true; } function showSuccess(element, msg) { - translator.translate(msg, function(msg) { + translator.translate(msg, function (msg) { element.html(msg); element.parent() - .removeClass('alert-danger') - .addClass('alert-success'); + .removeClass('register-danger') + .addClass('register-success'); element.show(); }); } diff --git a/public/src/client/reset.js b/public/src/client/reset.js index ef6856555a..3e0220957b 100644 --- a/public/src/client/reset.js +++ b/public/src/client/reset.js @@ -1,17 +1,17 @@ "use strict"; /*globals define, app, socket*/ -define('forum/reset', function() { +define('forum/reset', function () { var ResetPassword = {}; - ResetPassword.init = function() { + ResetPassword.init = function () { var inputEl = $('#email'), errorEl = $('#error'), successEl = $('#success'); - $('#reset').on('click', function() { + $('#reset').on('click', function () { if (inputEl.val() && inputEl.val().indexOf('@') !== -1) { - socket.emit('user.reset.send', inputEl.val(), function(err) { + socket.emit('user.reset.send', inputEl.val(), function (err) { if(err) { return app.alertError(err.message); } diff --git a/public/src/client/reset_code.js b/public/src/client/reset_code.js index 2efd14b1a6..044602144c 100644 --- a/public/src/client/reset_code.js +++ b/public/src/client/reset_code.js @@ -1,17 +1,17 @@ "use strict"; /*globals define, app, ajaxify, socket, config*/ -define('forum/reset_code', function() { +define('forum/reset_code', function () { var ResetCode = {}; - ResetCode.init = function() { + ResetCode.init = function () { var reset_code = ajaxify.data.code; var resetEl = $('#reset'); var password = $('#password'); var repeat = $('#repeat'); - resetEl.on('click', function() { + resetEl.on('click', function () { if (password.val().length < ajaxify.data.minimumPasswordLength) { app.alertError('[[reset_password:password_too_short]]'); } else if (password.val() !== repeat.val()) { @@ -21,7 +21,7 @@ define('forum/reset_code', function() { socket.emit('user.reset.commit', { code: reset_code, password: password.val() - }, function(err) { + }, function (err) { if (err) { ajaxify.refresh(); return app.alertError(err.message); diff --git a/public/src/client/search.js b/public/src/client/search.js index 6ca5b8dc41..20c85c13c2 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -1,11 +1,11 @@ 'use strict'; -/* globals app, define, utils, socket*/ +/* globals app, define, utils*/ -define('forum/search', ['search', 'autocomplete'], function(searchModule, autocomplete) { +define('forum/search', ['search', 'autocomplete'], function (searchModule, autocomplete) { var Search = {}; - Search.init = function() { + Search.init = function () { var searchQuery = $('#results').attr('data-search-query'); $('#search-input').val(searchQuery); @@ -14,23 +14,18 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco fillOutForm(); - searchIn.on('change', function() { + searchIn.on('change', function () { updateFormItemVisiblity(searchIn.val()); }); highlightMatches(searchQuery); - $('#advanced-search').off('submit').on('submit', function(e) { + $('#advanced-search').off('submit').on('submit', function (e) { e.preventDefault(); - - var input = $('#search-input'); - - var searchData = getSearchData(); - searchData.term = input.val(); - - searchModule.query(searchData, function() { - input.val(''); + searchModule.query(getSearchData(), function () { + $('#search-input').val(''); }); + return false; }); handleSavePreferences(); @@ -43,7 +38,7 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco var searchData = { in: $('#search-in').val() }; - + searchData.term = $('#search-input').val(); 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(); @@ -67,46 +62,50 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco function fillOutForm() { var params = utils.params(); - var searchData = getSearchPreferences(); - params = utils.merge(searchData, params); + var searchData = searchModule.getSearchPreferences(); + var formData = utils.merge(searchData, params); - if (params) { - if (params.in) { - $('#search-in').val(params.in); - updateFormItemVisiblity(params.in); + if (formData) { + if (params.term) { + $('#search-input').val(params.term); } - if (params.by) { - $('#posted-by-user').val(params.by); + if (formData.in) { + $('#search-in').val(formData.in); + updateFormItemVisiblity(formData.in); + } + + if (formData.by) { + $('#posted-by-user').val(formData.by); } - if (params.categories) { - $('#posted-in-categories').val(params.categories); + if (formData.categories) { + $('#posted-in-categories').val(formData.categories); } - if (params.searchChildren) { + if (formData.searchChildren) { $('#search-children').prop('checked', true); } - if (params.replies) { - $('#reply-count').val(params.replies); - $('#reply-count-filter').val(params.repliesFilter); + if (formData.replies) { + $('#reply-count').val(formData.replies); + $('#reply-count-filter').val(formData.repliesFilter); } - if (params.timeRange) { - $('#post-time-range').val(params.timeRange); - $('#post-time-filter').val(params.timeFilter); + if (formData.timeRange) { + $('#post-time-range').val(formData.timeRange); + $('#post-time-filter').val(formData.timeFilter); } - if (params.sortBy) { - $('#post-sort-by').val(params.sortBy); - $('#post-sort-direction').val(params.sortDirection); + if (formData.sortBy) { + $('#post-sort-by').val(formData.sortBy); + $('#post-sort-direction').val(formData.sortDirection); } - if (params.showAs) { - var isTopic = params.showAs === 'topics'; - var isPost = params.showAs === 'posts'; + if (formData.showAs) { + var isTopic = formData.showAs === 'topics'; + var isPost = formData.showAs === 'posts'; $('#show-as-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic); $('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost); } @@ -121,10 +120,10 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco var regexStr = searchQuery.replace(/^"/, '').replace(/"$/, '').trim().split(' ').join('|'); var regex = new RegExp('(' + regexStr + ')', 'gi'); - $('.search-result-text p, .search-result-text h4').each(function() { + $('.search-result-text p, .search-result-text h4').each(function () { var result = $(this), nested = []; - result.find('*').each(function() { + result.find('*').each(function () { $(this).after('<!-- ' + nested.length + ' -->'); nested.push($('<div />').append($(this))); }); @@ -140,13 +139,13 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco } function handleSavePreferences() { - $('#save-preferences').on('click', function() { + $('#save-preferences').on('click', function () { localStorage.setItem('search-preferences', JSON.stringify(getSearchData())); app.alertSuccess('[[search:search-preferences-saved]]'); return false; }); - $('#clear-preferences').on('click', function() { + $('#clear-preferences').on('click', function () { localStorage.removeItem('search-preferences'); var query = $('#search-input').val(); $('#advanced-search')[0].reset(); @@ -156,14 +155,6 @@ define('forum/search', ['search', 'autocomplete'], function(searchModule, autoco }); } - function getSearchPreferences() { - try { - return JSON.parse(localStorage.getItem('search-preferences')); - } catch(e) { - return {}; - } - } - function enableAutoComplete() { autocomplete.user($('#posted-by-user')); } diff --git a/public/src/client/tag.js b/public/src/client/tag.js index 1758539b61..e48252ec76 100644 --- a/public/src/client/tag.js +++ b/public/src/client/tag.js @@ -2,21 +2,23 @@ /* globals define, app, ajaxify, socket */ -define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function(recent, infinitescroll) { +define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function (recent, infinitescroll) { var Tag = {}; - Tag.init = function() { + Tag.init = function () { app.enterRoom('tags'); if ($('body').height() <= $(window).height() && $('[component="category"]').children().length >= 20) { $('#load-more-btn').show(); } - $('#load-more-btn').on('click', function() { + $('#load-more-btn').on('click', function () { loadMoreTopics(); }); - infinitescroll.init(loadMoreTopics); + if (!config.usePagination) { + infinitescroll.init(loadMoreTopics); + } function loadMoreTopics(direction) { if(direction < 0 || !$('[component="category"]').length) { @@ -26,7 +28,7 @@ define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function(recent, i infinitescroll.loadMore('topics.loadMoreFromSet', { set: 'tag:' + ajaxify.data.tag + ':topics', after: $('[component="category"]').attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (data.topics && data.topics.length) { recent.onTopicsLoaded('tag', data.topics, false, done); } else { diff --git a/public/src/client/tags.js b/public/src/client/tags.js index 5ea916257e..4fbae1ecf5 100644 --- a/public/src/client/tags.js +++ b/public/src/client/tags.js @@ -2,14 +2,14 @@ /* globals define, app, utils, socket */ -define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { +define('forum/tags', ['forum/infinitescroll'], function (infinitescroll) { var Tags = {}; var timeoutId = 0; - Tags.init = function() { + Tags.init = function () { app.enterRoom('tags'); - $('#tag-search').on('input propertychange', function() { + $('#tag-search').on('input propertychange', function () { if (timeoutId) { clearTimeout(timeoutId); timeoutId = 0; @@ -19,12 +19,12 @@ define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { return resetSearch(); } - timeoutId = setTimeout(function() { - socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, results) { + timeoutId = setTimeout(function () { + socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function (err, results) { if (err) { return app.alertError(err.message); } - onTagsLoaded(results.tags, true, function() { + onTagsLoaded(results.tags, true, function () { timeoutId = 0; }); }); @@ -34,14 +34,14 @@ define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { infinitescroll.init(Tags.loadMoreTags); }; - Tags.loadMoreTags = function(direction) { + Tags.loadMoreTags = function (direction) { if(direction < 0 || !$('.tag-list').length) { return; } infinitescroll.loadMore('topics.loadMoreTags', { after: $('.tag-list').attr('data-nextstart') - }, function(data, done) { + }, function (data, done) { if (data && data.tags && data.tags.length) { onTagsLoaded(data.tags, false, done); $('.tag-list').attr('data-nextstart', data.nextStart); @@ -54,7 +54,7 @@ define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { function resetSearch() { socket.emit('topics.loadMoreTags', { after: 0 - }, function(err, data) { + }, function (err, data) { if (err) { return app.alertError(err.message); } @@ -63,8 +63,8 @@ define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { } function onTagsLoaded(tags, replace, callback) { - callback = callback || function() {}; - app.parseAndTranslate('tags', 'tags', {tags: tags}, function(html) { + callback = callback || function () {}; + app.parseAndTranslate('tags', 'tags', {tags: tags}, function (html) { $('.tag-list')[replace ? 'html' : 'append'](html); utils.makeNumbersHumanReadable(html.find('.human-readable-number')); callback(); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 9d52262676..3408f18560 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -12,11 +12,11 @@ define('forum/topic', [ 'navigator', 'sort', 'components' -], function(infinitescroll, threadTools, postTools, events, posts, navigator, sort, components) { +], function (infinitescroll, threadTools, postTools, events, posts, navigator, sort, components) { var Topic = {}, currentUrl = ''; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { if (Topic.replaceURLTimeout) { clearTimeout(Topic.replaceURLTimeout); Topic.replaceURLTimeout = 0; @@ -31,8 +31,8 @@ define('forum/topic', [ $(window).off('keydown', onKeyDown); } - if (!data.url.startsWith('topic/')) { - require(['search'], function(search) { + if (data.url && !data.url.startsWith('topic/')) { + require(['search'], function (search) { if (search.topicDOM.active) { search.topicDOM.end(); } @@ -40,7 +40,7 @@ define('forum/topic', [ } }); - Topic.init = function() { + Topic.init = function () { var tid = ajaxify.data.tid; $(window).trigger('action:topic.loading'); @@ -55,7 +55,9 @@ define('forum/topic', [ sort.handleSort('topicPostSort', 'user.setTopicSort', 'topic/' + ajaxify.data.slug); - enableInfiniteLoadingOrPagination(); + if (!config.usePagination) { + infinitescroll.init($('[component="topic"]'), posts.loadMorePosts); + } addBlockQuoteHandler(); @@ -63,7 +65,7 @@ define('forum/topic', [ handleKeys(); - navigator.init('[component="post/anchor"]', ajaxify.data.postcount, Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex); + navigator.init('[component="post"]', ajaxify.data.postcount, Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex); handleBookmark(tid); @@ -71,7 +73,7 @@ define('forum/topic', [ handleTopicSearch(); - $(window).trigger('action:topic.loaded'); + $(window).trigger('action:topic.loaded', ajaxify.data); }; function handleKeys() { @@ -96,16 +98,16 @@ define('forum/topic', [ } function handleTopicSearch() { - require(['search', 'mousetrap'], function(search, Mousetrap) { + require(['search', 'mousetrap'], function (search, mousetrap) { $('.topic-search') - .on('click', '.prev', function() { + .on('click', '.prev', function () { search.topicDOM.prev(); }) - .on('click', '.next', function() { + .on('click', '.next', function () { search.topicDOM.next(); }); - Mousetrap.bind('ctrl+f', function(e) { + mousetrap.bind('ctrl+f', function (e) { if (config.topicSearchEnabled) { // If in topic, open search window and populate, otherwise regular behaviour var match = ajaxify.currentPage.match(/^topic\/([\d]+)/), @@ -121,12 +123,15 @@ define('forum/topic', [ }); } - Topic.toTop = function() { + Topic.toTop = function () { navigator.scrollTop(0); }; - Topic.toBottom = function() { - socket.emit('topics.postcount', ajaxify.data.tid, function(err, postCount) { + Topic.toBottom = function () { + socket.emit('topics.postcount', ajaxify.data.tid, function (err, postCount) { + if (err) { + return app.alertError(err.message); + } if (config.topicPostSort !== 'oldest_to_newest') { postCount = 2; } @@ -141,23 +146,23 @@ define('forum/topic', [ if (postIndex && window.location.search.indexOf('page=') === -1) { if (components.get('post/anchor', postIndex).length) { - return navigator.scrollToPostIndex(postIndex, true); + return navigator.scrollToPostIndex(postIndex, true, 0); } - } else if (bookmark && (!config.usePagination || (config.usePagination && ajaxify.data.pagination.currentPage === 1)) && ajaxify.data.postcount > 5) { + } else if (bookmark && (!config.usePagination || (config.usePagination && ajaxify.data.pagination.currentPage === 1)) && ajaxify.data.postcount > ajaxify.data.bookmarkThreshold) { navigator.update(0); app.alert({ alert_id: 'bookmark', message: '[[topic:bookmark_instructions]]', timeout: 0, type: 'info', - clickfn : function() { + clickfn : function () { navigator.scrollToPost(parseInt(bookmark - 1, 10), true); }, - closefn : function() { + closefn : function () { localStorage.removeItem('topic:' + tid + ':bookmark'); } }); - setTimeout(function() { + setTimeout(function () { app.removeAlert('bookmark'); }, 10000); } else { @@ -182,7 +187,7 @@ define('forum/topic', [ } function addBlockQuoteHandler() { - components.get('topic').on('click', 'blockquote .toggle', function() { + components.get('topic').on('click', 'blockquote .toggle', function () { var blockQuote = $(this).parent('blockquote'); var toggle = $(this); blockQuote.toggleClass('uncollapsed'); @@ -192,34 +197,18 @@ define('forum/topic', [ } function addParentHandler() { - components.get('topic').on('click', '[component="post/parent"]', function() { + components.get('topic').on('click', '[component="post/parent"]', function (e) { var toPid = $(this).attr('data-topid'); var toPost = $('[component="post"][data-pid="' + toPid + '"]'); if (toPost.length) { - return navigator.scrollToPost(toPost.attr('data-index'), true); + e.preventDefault(); + navigator.scrollToPost(toPost.attr('data-index'), true); + return false; } - - socket.emit('posts.getPidIndex', {pid: toPid, tid: ajaxify.data.tid, topicPostSort: config.topicPostSort}, function(err, index) { - if (err) { - return app.alertError(err.message); - } - - if (utils.isNumber(index)) { - navigator.scrollToPost(index, true); - } - }); }); } - function enableInfiniteLoadingOrPagination() { - if (!config.usePagination) { - infinitescroll.init($('[component="topic"]'), posts.loadMorePosts); - } else { - navigator.disable(); - } - } - function updateTopicTitle() { var span = components.get('navbar/title').find('span'); if ($(window).scrollTop() > 50 && span.hasClass('hidden')) { @@ -232,47 +221,54 @@ define('forum/topic', [ } } - Topic.calculateIndex = function(index, elementCount) { + Topic.calculateIndex = function (index, elementCount) { if (index !== 1 && config.topicPostSort !== 'oldest_to_newest') { return elementCount - index + 2; } return index; }; - Topic.navigatorCallback = function(index, elementCount, threshold) { + Topic.navigatorCallback = function (index, elementCount, threshold) { var path = ajaxify.removeRelativePath(window.location.pathname.slice(1)); if (!path.startsWith('topic')) { - return 1; + return; } - if (!navigator.scrollActive) { - var parts = ajaxify.removeRelativePath(window.location.pathname.slice(1)).split('/'); - var topicId = parts[1], - slug = parts[2]; - var newUrl = 'topic/' + topicId + '/' + (slug ? slug : ''); - if (index > 1) { - newUrl += '/' + index; + if (navigator.scrollActive) { + return; + } + + posts.loadImages(threshold); + + var newUrl = 'topic/' + ajaxify.data.slug + (index > 1 ? ('/' + index) : ''); + + if (newUrl !== currentUrl) { + + if (Topic.replaceURLTimeout) { + clearTimeout(Topic.replaceURLTimeout); } - posts.loadImages(threshold); + Topic.replaceURLTimeout = setTimeout(function () { - if (newUrl !== currentUrl) { - if (Topic.replaceURLTimeout) { - clearTimeout(Topic.replaceURLTimeout); + if (index >= elementCount && app.user.uid) { + socket.emit('topics.markAsRead', [ajaxify.data.tid]); } - Topic.replaceURLTimeout = setTimeout(function() { - updateUserBookmark(index); - - Topic.replaceURLTimeout = 0; - if (history.replaceState) { - var search = (window.location.search ? window.location.search : ''); - history.replaceState({ - url: newUrl + search - }, null, window.location.protocol + '//' + window.location.host + RELATIVE_PATH + '/' + newUrl + search); + + updateUserBookmark(index); + + Topic.replaceURLTimeout = 0; + if (history.replaceState) { + var search = window.location.search || ''; + if (!config.usePagination) { + search = (search && !/^\?page=\d+$/.test(search) ? search : ''); } - currentUrl = newUrl; - }, 500); - } + + history.replaceState({ + url: newUrl + search + }, null, window.location.protocol + '//' + window.location.host + RELATIVE_PATH + '/' + newUrl + search); + } + currentUrl = newUrl; + }, 500); } }; @@ -280,12 +276,12 @@ define('forum/topic', [ var bookmarkKey = 'topic:' + ajaxify.data.tid + ':bookmark'; var currentBookmark = ajaxify.data.bookmark || localStorage.getItem(bookmarkKey); - if (ajaxify.data.postcount > 5 && (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10))) { + if (ajaxify.data.postcount > ajaxify.data.bookmarkThreshold && (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10))) { if (app.user.uid) { socket.emit('topics.bookmark', { 'tid': ajaxify.data.tid, 'index': index - }, function(err) { + }, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/client/topic/delete-posts.js b/public/src/client/topic/delete-posts.js index 1a7ce5221b..ec7a0ca1d5 100644 --- a/public/src/client/topic/delete-posts.js +++ b/public/src/client/topic/delete-posts.js @@ -1,68 +1,56 @@ 'use strict'; -/* globals define, app, ajaxify, socket, templates, translator */ +/* globals define, app, ajaxify, socket */ -define('forum/topic/delete-posts', ['components', 'postSelect'], function(components, postSelect) { +define('forum/topic/delete-posts', ['components', 'postSelect'], function (components, postSelect) { - var DeletePosts = {}, - modal, - deleteBtn, - purgeBtn; + var DeletePosts = {}; + var modal; + var deleteBtn; + var purgeBtn; - DeletePosts.init = function() { + DeletePosts.init = function () { $('.topic').on('click', '[component="topic/delete/posts"]', onDeletePostsClicked); + $(window).on('action:ajaxify.start', onAjaxifyStart); }; + function onAjaxifyStart() { + closeModal(); + $(window).off('action:ajaxify.start', onAjaxifyStart); + } + function onDeletePostsClicked() { - parseModal(function(html) { - modal = $(html); + app.parseAndTranslate('partials/delete_posts_modal', {}, function (html) { + modal = html; - modal.on('hidden.bs.modal', function() { - modal.remove(); - }); + $('body').append(modal); deleteBtn = modal.find('#delete_posts_confirm'); purgeBtn = modal.find('#purge_posts_confirm'); - showModal(); - modal.find('.close,#delete_posts_cancel').on('click', closeModal); - postSelect.init(function() { + postSelect.init(function () { checkButtonEnable(); showPostsSelected(); }); showPostsSelected(); - deleteBtn.on('click', function() { + deleteBtn.on('click', function () { deletePosts(deleteBtn, 'posts.deletePosts'); }); - purgeBtn.on('click', function() { + purgeBtn.on('click', function () { deletePosts(purgeBtn, 'posts.purgePosts'); }); }); } - function parseModal(callback) { - templates.parse('partials/delete_posts_modal', {}, function(html) { - translator.translate(html, callback); - }); - } - - function showModal() { - modal.modal({backdrop: false, show: true}) - .css('position', 'fixed') - .css('left', Math.max(0, (($(window).width() - modal.outerWidth()) / 2) + $(window).scrollLeft()) + 'px') - .css('top', '0px') - .css('z-index', '2000'); - } - function deletePosts(btn, command) { btn.attr('disabled', true); socket.emit(command, { tid: ajaxify.data.tid, pids: postSelect.pids - }, function(err) { + }, function (err) { btn.removeAttr('disabled'); if (err) { return app.alertError(err.message); @@ -74,7 +62,7 @@ define('forum/topic/delete-posts', ['components', 'postSelect'], function(compon function showPostsSelected() { if (postSelect.pids.length) { - modal.find('#pids').text(postSelect.pids.join(', ')); + modal.find('#pids').translateHtml('[[topic:fork_pid_count, ' + postSelect.pids.length + ']]'); } else { modal.find('#pids').translateHtml('[[topic:fork_no_pids]]'); } @@ -91,11 +79,14 @@ define('forum/topic/delete-posts', ['components', 'postSelect'], function(compon } function closeModal() { - postSelect.pids.forEach(function(pid) { - components.get('post', 'pid', pid).css('opacity', 1); + postSelect.pids.forEach(function (pid) { + components.get('post', 'pid', pid).toggleClass('bg-success', false); }); - modal.modal('hide'); + if (modal) { + modal.remove(); + modal = null; + } components.get('topic').off('click', '[data-pid]'); postSelect.enableClicksOnPosts(); diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 7f769f984b..f6a6aecc6d 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -9,14 +9,14 @@ define('forum/topic/events', [ 'forum/topic/posts', 'components', 'translator' -], function(postTools, threadTools, posts, components, translator) { +], function (postTools, threadTools, posts, components, translator) { var Events = {}; var events = { 'event:user_status_change': onUserStatusChange, 'event:voted': updatePostVotesAndUserReputation, - 'event:favourited': updateFavouriteCount, + 'event:bookmarked': updateBookmarkCount, 'event:topic_deleted': threadTools.setDeleteState, 'event:topic_restored': threadTools.setDeleteState, @@ -36,8 +36,8 @@ define('forum/topic/events', [ 'event:post_deleted': togglePostDeleteState, 'event:post_restored': togglePostDeleteState, - 'posts.favourite': togglePostFavourite, - 'posts.unfavourite': togglePostFavourite, + 'posts.bookmark': togglePostBookmark, + 'posts.unbookmark': togglePostBookmark, 'posts.upvote': togglePostVote, 'posts.downvote': togglePostVote, @@ -47,7 +47,7 @@ define('forum/topic/events', [ 'event:new_post': posts.onNewPost }; - Events.init = function() { + Events.init = function () { Events.removeListeners(); for(var eventName in events) { if (events.hasOwnProperty(eventName)) { @@ -56,7 +56,7 @@ define('forum/topic/events', [ } }; - Events.removeListeners = function() { + Events.removeListeners = function () { for(var eventName in events) { if (events.hasOwnProperty(eventName)) { socket.removeListener(eventName, events[eventName]); @@ -69,15 +69,15 @@ define('forum/topic/events', [ } function updatePostVotesAndUserReputation(data) { - var votes = components.get('post/vote-count', data.post.pid), - reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); + var votes = components.get('post/vote-count', data.post.pid); + var reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); votes.html(data.post.votes).attr('data-votes', data.post.votes); reputationElements.html(data.user.reputation).attr('data-reputation', data.user.reputation); } - function updateFavouriteCount(data) { - $('[data-pid="' + data.post.pid + '"] .favouriteCount').html(data.post.reputation).attr('data-favourites', data.post.reputation); + function updateBookmarkCount(data) { + $('[data-pid="' + data.post.pid + '"] .bookmarkCount').html(data.post.bookmarks).attr('data-bookmarks', data.post.bookmarks); } function onTopicPurged() { @@ -96,24 +96,29 @@ define('forum/topic/events', [ if (!data || !data.post) { return; } - var editedPostEl = components.get('post/content', data.post.pid), - editorEl = $('[data-pid="' + data.post.pid + '"] [component="post/editor"]'), - topicTitle = components.get('topic/title'), - breadCrumb = components.get('breadcrumb/current'); + var editedPostEl = components.get('post/content', data.post.pid); + var editorEl = $('[data-pid="' + data.post.pid + '"] [component="post/editor"]'); + var topicTitle = components.get('topic/title'); + var navbarTitle = components.get('navbar/title').find('span'); + var breadCrumb = components.get('breadcrumb/current'); if (topicTitle.length && data.topic.title && topicTitle.html() !== data.topic.title) { + ajaxify.data.title = data.topic.title; var newUrl = 'topic/' + data.topic.slug + (window.location.search ? window.location.search : ''); history.replaceState({url: newUrl}, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + newUrl); - topicTitle.fadeOut(250, function() { + topicTitle.fadeOut(250, function () { topicTitle.html(data.topic.title).fadeIn(250); }); - breadCrumb.fadeOut(250, function() { + breadCrumb.fadeOut(250, function () { breadCrumb.html(data.topic.title).fadeIn(250); }); + navbarTitle.fadeOut(250, function () { + navbarTitle.html(data.topic.title).fadeIn(250); + }); } - editedPostEl.fadeOut(250, function() { + editedPostEl.fadeOut(250, function () { editedPostEl.html(translator.unescape(data.post.content)); editedPostEl.find('img:not(.not-responsive)').addClass('img-responsive'); app.replaceSelfLinks(editedPostEl.find('a')); @@ -121,27 +126,27 @@ define('forum/topic/events', [ posts.unloadImages(editedPostEl.parent()); posts.loadImages(); editedPostEl.fadeIn(250); - $(window).trigger('action:posts.edited', data); - }); - var editData = { - editor: data.editor, - editedISO: utils.toISOString(data.post.edited) - }; - - templates.parse('partials/topic/post-editor', editData, function(html) { - translator.translate(html, function(translated) { - html = $(translated); - editorEl.replaceWith(html); - html.find('.timeago').timeago(); + var editData = { + editor: data.editor, + editedISO: utils.toISOString(data.post.edited) + }; + + templates.parse('partials/topic/post-editor', editData, function (html) { + translator.translate(html, function (translated) { + html = $(translated); + editorEl.replaceWith(html); + html.find('.timeago').timeago(); + $(window).trigger('action:posts.edited', data); + }); }); }); if (data.topic.tags && tagsUpdated(data.topic.tags)) { - templates.parse('partials/post_bar', 'tags', {tags: data.topic.tags}, function(html) { + templates.parse('partials/post_bar', 'tags', {tags: data.topic.tags}, function (html) { var tags = $('.tags'); - tags.fadeOut(250, function() { + tags.fadeOut(250, function () { tags.html(html).fadeIn(250); }); }); @@ -153,7 +158,7 @@ define('forum/topic/events', [ return true; } - for (var i=0; i<tags.length; ++i) { + for (var i = 0; i < tags.length; ++i) { if (!$('.tags .tag-item[data-tag="' + tags[i].value + '"]').length) { return true; } @@ -162,7 +167,7 @@ define('forum/topic/events', [ } function onPostPurged(pid) { - components.get('post', 'pid', pid).fadeOut(500, function() { + components.get('post', 'pid', pid).fadeOut(500, function () { $(this).remove(); ajaxify.data.postcount --; postTools.updatePostCount(ajaxify.data.postcount); @@ -183,7 +188,7 @@ define('forum/topic/events', [ var isDeleted = postEl.hasClass('deleted'); postTools.toggle(data.pid, isDeleted); - if (!app.user.isAdmin && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) { + if (!ajaxify.data.privileges.isAdminOrMod && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) { postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted); if (isDeleted) { postEl.find('[component="post/content"]').translateHtml('[[topic:post_is_deleted]]'); @@ -193,17 +198,17 @@ define('forum/topic/events', [ } } - function togglePostFavourite(data) { - var favBtn = $('[data-pid="' + data.post.pid + '"] [component="post/favourite"]'); + function togglePostBookmark(data) { + var el = $('[data-pid="' + data.post.pid + '"] [component="post/bookmark"]'); - if (!favBtn.length) { + if (!el.length) { return; } - favBtn.attr('data-favourited', data.isFavourited); + el.attr('data-bookmarked', data.isBookmarked); - favBtn.find('[component="post/favourite/on"]').toggleClass('hidden', !data.isFavourited); - favBtn.find('[component="post/favourite/off"]').toggleClass('hidden', data.isFavourited); + el.find('[component="post/bookmark/on"]').toggleClass('hidden', !data.isBookmarked); + el.find('[component="post/bookmark/off"]').toggleClass('hidden', data.isBookmarked); } function togglePostVote(data) { @@ -215,7 +220,7 @@ define('forum/topic/events', [ function onNewNotification(data) { var tid = ajaxify.data.tid; if (data && data.tid && parseInt(data.tid, 10) === parseInt(tid, 10)) { - socket.emit('topics.markTopicNotificationsRead', tid); + socket.emit('topics.markTopicNotificationsRead', [tid]); } } diff --git a/public/src/client/topic/flag.js b/public/src/client/topic/flag.js index 3101c0cd1c..72e2524ce0 100644 --- a/public/src/client/topic/flag.js +++ b/public/src/client/topic/flag.js @@ -1,28 +1,28 @@ 'use strict'; -/* globals define, app, socket, templates, translator */ +/* globals define, app, socket, templates */ -define('forum/topic/flag', [], function() { +define('forum/topic/flag', [], function () { var Flag = {}, flagModal, flagCommit; - Flag.showFlagModal = function(pid) { - parseModal(function(html) { + Flag.showFlagModal = function (pid) { + parseModal(function (html) { flagModal = $(html); - flagModal.on('hidden.bs.modal', function() { + flagModal.on('hidden.bs.modal', function () { flagModal.remove(); }); flagCommit = flagModal.find('#flag-post-commit'); - flagModal.on('click', '.flag-reason', function() { + flagModal.on('click', '.flag-reason', function () { flagPost(pid, $(this).text()); }); - flagCommit.on('click', function() { + flagCommit.on('click', function () { flagPost(pid, flagModal.find('#flag-reason-custom').val()); }); @@ -33,8 +33,10 @@ define('forum/topic/flag', [], function() { }; function parseModal(callback) { - templates.parse('partials/modals/flag_post_modal', {}, function(html) { - translator.translate(html, callback); + templates.parse('partials/modals/flag_post_modal', {}, function (html) { + require(['translator'], function (translator) { + translator.translate(html, callback); + }); }); } @@ -42,7 +44,7 @@ define('forum/topic/flag', [], function() { if (!pid || !reason) { return; } - socket.emit('posts.flag', {pid: pid, reason: reason}, function(err) { + socket.emit('posts.flag', {pid: pid, reason: reason}, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js index a55b45220c..80165b589c 100644 --- a/public/src/client/topic/fork.js +++ b/public/src/client/topic/fork.js @@ -1,33 +1,35 @@ 'use strict'; -/* globals define, app, ajaxify, socket, templates, translator */ +/* globals define, app, ajaxify, socket */ -define('forum/topic/fork', ['components', 'postSelect'], function(components, postSelect) { +define('forum/topic/fork', ['components', 'postSelect'], function (components, postSelect) { - var Fork = {}, - forkModal, - forkCommit; + var Fork = {}; + var forkModal; + var forkCommit; - Fork.init = function() { + Fork.init = function () { $('.topic').on('click', '[component="topic/fork"]', onForkThreadClicked); + $(window).on('action:ajaxify.start', onAjaxifyStart); }; - function onForkThreadClicked() { - parseModal(function(html) { - forkModal = $(html); + function onAjaxifyStart() { + closeForkModal(); + $(window).off('action:ajaxify.start', onAjaxifyStart); + } - forkModal.on('hidden.bs.modal', function() { - forkModal.remove(); - }); + function onForkThreadClicked() { + app.parseAndTranslate('partials/fork_thread_modal', {}, function (html) { + forkModal = html; forkCommit = forkModal.find('#fork_thread_commit'); - showForkModal(); + $(document.body).append(forkModal); forkModal.find('.close,#fork_thread_cancel').on('click', closeForkModal); - forkModal.find('#fork-title').on('change', checkForkButtonEnable); + forkModal.find('#fork-title').on('keyup', checkForkButtonEnable); - postSelect.init(function() { + postSelect.init(function () { checkForkButtonEnable(); showPostsSelected(); }); @@ -37,28 +39,15 @@ define('forum/topic/fork', ['components', 'postSelect'], function(components, po }); } - function parseModal(callback) { - templates.parse('partials/fork_thread_modal', {}, function(html) { - translator.translate(html, callback); - }); - } - - function showForkModal() { - forkModal.modal({backdrop: false, show: true}) - .css('position', 'fixed') - .css('left', Math.max(0, (($(window).width() - forkModal.outerWidth()) / 2) + $(window).scrollLeft()) + 'px') - .css('top', '0px') - .css('z-index', '2000'); - } - function createTopicFromPosts() { forkCommit.attr('disabled', true); socket.emit('topics.createTopicFromPosts', { title: forkModal.find('#fork-title').val(), - pids: postSelect.pids - }, function(err, newTopic) { + pids: postSelect.pids, + fromTid: ajaxify.data.tid + }, function (err, newTopic) { function fadeOutAndRemove(pid) { - components.get('post', 'pid', pid).fadeOut(500, function() { + components.get('post', 'pid', pid).fadeOut(500, function () { $(this).remove(); }); } @@ -72,12 +61,12 @@ define('forum/topic/fork', ['components', 'postSelect'], function(components, po title: '[[global:alert.success]]', message: '[[topic:fork_success]]', type: 'success', - clickfn: function() { + clickfn: function () { ajaxify.go('topic/' + newTopic.slug); } }); - postSelect.pids.forEach(function(pid) { + postSelect.pids.forEach(function (pid) { fadeOutAndRemove(pid); }); @@ -87,14 +76,14 @@ define('forum/topic/fork', ['components', 'postSelect'], function(components, po function showPostsSelected() { if (postSelect.pids.length) { - forkModal.find('#fork-pids').text(postSelect.pids.join(', ')); + forkModal.find('#fork-pids').translateHtml('[[topic:fork_pid_count, ' + postSelect.pids.length + ']]'); } else { forkModal.find('#fork-pids').translateHtml('[[topic:fork_no_pids]]'); } } function checkForkButtonEnable() { - if (forkModal.find('#fork-title').length && postSelect.pids.length) { + if (forkModal.find('#fork-title').val().length && postSelect.pids.length) { forkCommit.removeAttr('disabled'); } else { forkCommit.attr('disabled', true); @@ -102,11 +91,14 @@ define('forum/topic/fork', ['components', 'postSelect'], function(components, po } function closeForkModal() { - postSelect.pids.forEach(function(pid) { - components.get('post', 'pid', pid).css('opacity', 1); + postSelect.pids.forEach(function (pid) { + components.get('post', 'pid', pid).toggleClass('bg-success', false); }); - forkModal.modal('hide'); + if (forkModal) { + forkModal.remove(); + forkModal = null; + } components.get('topic').off('click', '[data-pid]'); postSelect.enableClicksOnPosts(); diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js index d16eb9bc41..de57f2a0d5 100644 --- a/public/src/client/topic/move.js +++ b/public/src/client/topic/move.js @@ -1,14 +1,14 @@ 'use strict'; -/* globals define, app, socket, templates, translator */ +/* globals define, app, socket, templates */ -define('forum/topic/move', function() { +define('forum/topic/move', function () { var Move = {}, modal, selectedEl; - Move.init = function(tids, currentCid, onComplete) { + Move.init = function (tids, currentCid, onComplete) { Move.tids = tids; Move.currentCid = currentCid; Move.onComplete = onComplete; @@ -22,9 +22,9 @@ define('forum/topic/move', function() { return app.alertError(err.message); } - parseModal(categories, function() { + parseModal(categories, function () { - modal.on('hidden.bs.modal', function() { + modal.on('hidden.bs.modal', function () { modal.remove(); }); @@ -34,7 +34,7 @@ define('forum/topic/move', function() { modal.find('.modal-header h3').translateText('[[topic:move_topics]]'); } - modal.on('click', '.category-list li[data-cid]', function() { + modal.on('click', '.category-list li[data-cid]', function () { selectCategory($(this)); }); @@ -45,15 +45,17 @@ define('forum/topic/move', function() { } function parseModal(categories, callback) { - templates.parse('partials/move_thread_modal', {categories: []}, function(html) { - translator.translate(html, function(html) { - modal = $(html); - categories.forEach(function(category) { - if (!category.link) { - buildRecursive(modal.find('.category-list'), category, ''); - } + templates.parse('partials/move_thread_modal', {categories: []}, function (html) { + require(['translator'], function (translator) { + translator.translate(html, function (html) { + modal = $(html); + categories.forEach(function (category) { + if (!category.link) { + buildRecursive(modal.find('.category-list'), category, ''); + } + }); + callback(); }); - callback(); }); }); } @@ -78,7 +80,7 @@ define('forum/topic/move', function() { parentEl.append('<br/>'); var indent = ' '; - category.children.forEach(function(childCategory) { + category.children.forEach(function (childCategory) { if (!childCategory.link) { buildRecursive(parentEl, childCategory, indent + level); } @@ -108,7 +110,7 @@ define('forum/topic/move', function() { tids: Move.tids, cid: selectedEl.attr('data-cid'), currentCid: Move.currentCid - }, function(err) { + }, function (err) { modal.modal('hide'); if (err) { diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index d1961a29a6..d09325ebfd 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -2,11 +2,15 @@ /* globals define, app, ajaxify, bootbox, socket, templates, utils, config */ -define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator'], function(share, navigator, components, translator) { +define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator'], function (share, navigator, components, translator) { var PostTools = {}; - PostTools.init = function(tid) { + var staleReplyAnyway = false; + + PostTools.init = function (tid) { + staleReplyAnyway = false; + renderMenu(); addPostHandlers(tid); @@ -19,7 +23,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }; function renderMenu() { - $('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function() { + $('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function () { var $this = $(this); var dropdownMenu = $this.find('.dropdown-menu'); if (dropdownMenu.html()) { @@ -29,14 +33,14 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator var pid = postEl.attr('data-pid'); var index = parseInt(postEl.attr('data-index'), 10); - socket.emit('posts.loadPostTools', {pid: pid, cid: ajaxify.data.cid}, function(err, data) { + socket.emit('posts.loadPostTools', {pid: pid, cid: ajaxify.data.cid}, function (err, data) { if (err) { - return app.alertError(err); + return app.alertError(err.message); } data.posts.display_move_tools = data.posts.display_move_tools && index !== 0; - templates.parse('partials/topic/post-menu-list', data, function(html) { - translator.translate(html, function(html) { + templates.parse('partials/topic/post-menu-list', data, function (html) { + translator.translate(html, function (html) { dropdownMenu.html(html); $(window).trigger('action:post.tools.load'); }); @@ -45,18 +49,20 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }); } - PostTools.toggle = function(pid, isDeleted) { + PostTools.toggle = function (pid, isDeleted) { var postEl = components.get('post', 'pid', pid); - postEl.find('[component="post/quote"], [component="post/favourite"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') + postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') .toggleClass('hidden', isDeleted); postEl.find('[component="post/delete"]').toggleClass('hidden', isDeleted); postEl.find('[component="post/restore"]').toggleClass('hidden', !isDeleted); postEl.find('[component="post/purge"]').toggleClass('hidden', !isDeleted); + + postEl.find('[component="post/tools"] .dropdown-menu').html(''); }; - PostTools.updatePostCount = function(postCount) { + PostTools.updatePostCount = function (postCount) { var postCountEl = components.get('topic/post-count'); postCountEl.html(postCount).attr('title', postCount); utils.makeNumbersHumanReadable(postCountEl); @@ -65,9 +71,9 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator function addVoteHandler() { components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip); - components.get('topic').on('mouseout', '[data-pid] [component="post/vote-count"]', function() { + components.get('topic').on('mouseout', '[data-pid] [component="post/vote-count"]', function () { var el = $(this).parent(); - el.on('shown.bs.tooltip', function() { + el.on('shown.bs.tooltip', function () { $('.tooltip').tooltip('destroy'); el.off('shown.bs.tooltip'); }); @@ -86,7 +92,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator $('.tooltip').tooltip('destroy'); $this.off('mouseenter', loadDataAndCreateTooltip); - socket.emit('posts.getUpvoters', [pid], function(err, data) { + socket.emit('posts.getUpvoters', [pid], function (err, data) { if (err) { return app.alertError(err.message); } @@ -109,7 +115,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } if (usernames.length + data.otherCount > 6) { usernames = usernames.join(', ').replace(/,/g, '|'); - translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function(translated) { + translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function (translated) { translated = translated.replace(/\|/g, ','); doCreateTooltip(translated); }); @@ -122,20 +128,20 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator function addPostHandlers(tid) { var postContainer = components.get('topic'); - postContainer.on('click', '[component="post/quote"]', function() { + postContainer.on('click', '[component="post/quote"]', function () { onQuoteClicked($(this), tid); }); - postContainer.on('click', '[component="post/reply"]', function() { + postContainer.on('click', '[component="post/reply"]', function () { onReplyClicked($(this), tid); }); - $('.topic').on('click', '[component="topic/reply"]', function() { + $('.topic').on('click', '[component="topic/reply"]', function () { onReplyClicked($(this), tid); }); - $('.topic').on('click', '[component="topic/reply-as-topic"]', function() { - translator.translate('[[topic:link_back, ' + ajaxify.data.titleRaw + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function(body) { + $('.topic').on('click', '[component="topic/reply-as-topic"]', function () { + translator.translate('[[topic:link_back, ' + ajaxify.data.titleRaw + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) { $(window).trigger('action:composer.topic.new', { cid: ajaxify.data.cid, body: body @@ -143,140 +149,194 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }); }); - postContainer.on('click', '[component="post/favourite"]', function() { - favouritePost($(this), getData($(this), 'data-pid')); + postContainer.on('click', '[component="post/bookmark"]', function () { + bookmarkPost($(this), getData($(this), 'data-pid')); }); - postContainer.on('click', '[component="post/upvote"]', function() { + postContainer.on('click', '[component="post/upvote"]', function () { return toggleVote($(this), '.upvoted', 'posts.upvote'); }); - postContainer.on('click', '[component="post/downvote"]', function() { + postContainer.on('click', '[component="post/downvote"]', function () { return toggleVote($(this), '.downvoted', 'posts.downvote'); }); - postContainer.on('click', '[component="post/vote-count"]', function() { + postContainer.on('click', '[component="post/vote-count"]', function () { showVotes(getData($(this), 'data-pid')); }); - postContainer.on('click', '[component="post/flag"]', function() { + postContainer.on('click', '[component="post/flag"]', function () { var pid = getData($(this), 'data-pid'); - require(['forum/topic/flag'], function(flag) { + require(['forum/topic/flag'], function (flag) { flag.showFlagModal(pid); }); }); - postContainer.on('click', '[component="post/edit"]', function() { + postContainer.on('click', '[component="post/edit"]', function () { var btn = $(this); - $(window).trigger('action:composer.post.edit', { - pid: getData(btn, 'data-pid') - }); + + var timestamp = parseInt(getData(btn, 'data-timestamp'), 10); + var postEditDuration = parseInt(ajaxify.data.postEditDuration, 10); + + if (checkDuration(postEditDuration, timestamp, 'post-edit-duration-expired')) { + $(window).trigger('action:composer.post.edit', { + pid: getData(btn, 'data-pid') + }); + } }); - postContainer.on('click', '[component="post/delete"]', function() { - togglePostDelete($(this), tid); + postContainer.on('click', '[component="post/delete"]', function () { + var btn = $(this); + var timestamp = parseInt(getData(btn, 'data-timestamp'), 10); + var postDeleteDuration = parseInt(ajaxify.data.postDeleteDuration, 10); + if (checkDuration(postDeleteDuration, timestamp, 'post-delete-duration-expired')) { + togglePostDelete($(this), tid); + } }); - postContainer.on('click', '[component="post/restore"]', function() { + function checkDuration(duration, postTimestamp, languageKey) { + if (!ajaxify.data.privileges.isAdminOrMod && duration && Date.now() - postTimestamp > duration * 1000) { + var numDays = Math.floor(duration / 86400); + var numHours = Math.floor((duration % 86400) / 3600); + var numMinutes = Math.floor(((duration % 86400) % 3600) / 60); + var numSeconds = ((duration % 86400) % 3600) % 60; + var msg = '[[error:' + languageKey + ', ' + duration + ']]'; + if (numDays) { + if (numHours) { + msg = '[[error:' + languageKey + '-days-hours, ' + numDays + ', ' + numHours + ']]'; + } else { + msg = '[[error:' + languageKey + '-days, ' + numDays + ']]'; + } + } else if (numHours) { + if (numMinutes) { + msg = '[[error:' + languageKey + '-hours-minutes, ' + numHours + ', ' + numMinutes + ']]'; + } else { + msg = '[[error:' + languageKey + '-hours, ' + numHours + ']]'; + } + } else if (numMinutes) { + if (numSeconds) { + msg = '[[error:' + languageKey + '-minutes-seconds, ' + numMinutes + ', ' + numSeconds + ']]'; + } else { + msg = '[[error:' + languageKey + '-minutes, ' + numMinutes + ']]'; + } + } + app.alertError(msg); + return false; + } + return true; + } + + postContainer.on('click', '[component="post/restore"]', function () { togglePostDelete($(this), tid); }); - postContainer.on('click', '[component="post/purge"]', function() { + postContainer.on('click', '[component="post/purge"]', function () { purgePost($(this), tid); }); - postContainer.on('click', '[component="post/move"]', function() { + postContainer.on('click', '[component="post/move"]', function () { openMovePostModal($(this)); }); - postContainer.on('click', '[component="post/chat"]', function() { + postContainer.on('click', '[component="post/chat"]', function () { openChat($(this)); }); } function onReplyClicked(button, tid) { - showStaleWarning(function(proceed) { - if (!proceed) { - var selectionText = ''; - var selection = window.getSelection ? window.getSelection() : document.selection.createRange(); - var content = button.parents('[component="post"]').find('[component="post/content"]').get(0); - - if (content && selection.containsNode(content, true)) { - var bounds = document.createRange(); - bounds.selectNodeContents(content); - var range = selection.getRangeAt(0).cloneRange(); - if (range.compareBoundaryPoints(Range.START_TO_START, bounds) < 0) { - range.setStart(bounds.startContainer, bounds.startOffset); - } - if (range.compareBoundaryPoints(Range.END_TO_END, bounds) > 0) { - range.setEnd(bounds.endContainer, bounds.endOffset); - } - bounds.detach(); - selectionText = range.toString(); - range.detach(); - } + var selectedText = getSelectedText(button); - var username = getUserName(button); - if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) { - username = ''; - } + showStaleWarning(function () { + var username = getUserName(button); + if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) { + username = ''; + } - var toPid = button.is('[component="post/reply"]') ? getData(button, 'data-pid') : null; - - if (selectionText.length) { - $(window).trigger('action:composer.addQuote', { - tid: tid, - slug: ajaxify.data.slug, - index: getData(button, 'data-index'), - pid: toPid, - topicName: ajaxify.data.titleRaw, - username: username, - text: selectionText - }); - } else { - $(window).trigger('action:composer.post.new', { - tid: tid, - pid: toPid, - topicName: ajaxify.data.titleRaw, - text: username ? username + ' ' : '' - }); - } + var toPid = button.is('[component="post/reply"]') ? getData(button, 'data-pid') : null; + + if (selectedText) { + $(window).trigger('action:composer.addQuote', { + tid: tid, + slug: ajaxify.data.slug, + index: getData(button, 'data-index'), + pid: toPid, + topicName: ajaxify.data.titleRaw, + username: username, + text: selectedText + }); + } else { + $(window).trigger('action:composer.post.new', { + tid: tid, + pid: toPid, + topicName: ajaxify.data.titleRaw, + text: username ? username + ' ' : '' + }); } }); } function onQuoteClicked(button, tid) { - showStaleWarning(function(proceed) { - if (!proceed) { - var username = getUserName(button), - pid = getData(button, 'data-pid'); - - socket.emit('posts.getRawPost', pid, function(err, post) { - if(err) { - return app.alertError(err.message); - } + var selectedText = getSelectedText(button); - $(window).trigger('action:composer.addQuote', { - tid: tid, - slug: ajaxify.data.slug, - index: getData(button, 'data-index'), - pid: pid, - username: username, - topicName: ajaxify.data.titleRaw, - text: post - }); + showStaleWarning(function () { + + function quote(text) { + $(window).trigger('action:composer.addQuote', { + tid: tid, + slug: ajaxify.data.slug, + index: getData(button, 'data-index'), + pid: pid, + username: username, + topicName: ajaxify.data.titleRaw, + text: text }); } + + var username = getUserName(button); + var pid = getData(button, 'data-pid'); + + if (selectedText) { + return quote(selectedText); + } + socket.emit('posts.getRawPost', pid, function (err, post) { + if (err) { + return app.alertError(err.message); + } + + quote(post); + }); }); } - function favouritePost(button, pid) { - var method = button.attr('data-favourited') === 'false' ? 'posts.favourite' : 'posts.unfavourite'; + function getSelectedText(button) { + var selectionText = ''; + var selection = window.getSelection ? window.getSelection() : document.selection.createRange(); + var content = button.parents('[component="post"]').find('[component="post/content"]').get(0); + + if (selection && selection.containsNode && content && selection.containsNode(content, true)) { + var bounds = document.createRange(); + bounds.selectNodeContents(content); + var range = selection.getRangeAt(0).cloneRange(); + if (range.compareBoundaryPoints(Range.START_TO_START, bounds) < 0) { + range.setStart(bounds.startContainer, bounds.startOffset); + } + if (range.compareBoundaryPoints(Range.END_TO_END, bounds) > 0) { + range.setEnd(bounds.endContainer, bounds.endOffset); + } + bounds.detach(); + selectionText = range.toString(); + range.detach(); + } + return selectionText; + } + + function bookmarkPost(button, pid) { + var method = button.attr('data-bookmarked') === 'false' ? 'posts.bookmark' : 'posts.unbookmark'; socket.emit(method, { pid: pid, room_id: app.currentRoom - }, function(err) { + }, function (err) { if (err) { app.alertError(err.message); } @@ -292,7 +352,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator socket.emit(currentState ? 'posts.unvote' : method , { pid: post.attr('data-pid'), room_id: app.currentRoom - }, function(err) { + }, function (err) { if (err) { if (err.message === 'self-vote') { showVotes(post.attr('data-pid')); @@ -306,7 +366,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function showVotes(pid) { - socket.emit('posts.getVoters', {pid: pid, cid: ajaxify.data.cid}, function(err, data) { + socket.emit('posts.getVoters', {pid: pid, cid: ajaxify.data.cid}, function (err, data) { if (err) { if (err.message === '[[error:no-privileges]]') { return; @@ -316,16 +376,19 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator return app.alertError(err.message); } - templates.parse('partials/modals/votes_modal', data, function(html) { - var dialog = bootbox.dialog({ - title: 'Voters', - message: html, - className: 'vote-modal', - show: true - }); + templates.parse('partials/modals/votes_modal', data, function (html) { + translator.translate(html, function (translated) { + var dialog = bootbox.dialog({ + title: 'Voters', + message: translated, + className: 'vote-modal', + show: true + }); + + dialog.on('click', function () { + dialog.modal('hide'); + }); - dialog.on('click', function() { - dialog.modal('hide'); }); }); }); @@ -354,9 +417,9 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function togglePostDelete(button, tid) { - var pid = getData(button, 'data-pid'), - postEl = components.get('post', 'pid', pid), - action = !postEl.hasClass('deleted') ? 'delete' : 'restore'; + var pid = getData(button, 'data-pid'); + var postEl = components.get('post', 'pid', pid); + var action = !postEl.hasClass('deleted') ? 'delete' : 'restore'; postAction(action, pid, tid); } @@ -366,8 +429,8 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function postAction(action, pid, tid) { - translator.translate('[[topic:post_' + action + '_confirm]]', function(msg) { - bootbox.confirm(msg, function(confirm) { + translator.translate('[[topic:post_' + action + '_confirm]]', function (msg) { + bootbox.confirm(msg, function (confirm) { if (!confirm) { return; } @@ -375,7 +438,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator socket.emit('posts.' + action, { pid: pid, tid: tid - }, function(err) { + }, function (err) { if (err) { app.alertError(err.message); } @@ -385,28 +448,28 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function openMovePostModal(button) { - parseMoveModal(function(html) { + parseMoveModal(function (html) { var moveModal = $(html); var moveBtn = moveModal.find('#move_post_commit'), topicId = moveModal.find('#topicId'); - moveModal.on('hidden.bs.modal', function() { + moveModal.on('hidden.bs.modal', function () { moveModal.remove(); }); showMoveModal(moveModal); - moveModal.find('.close, #move_post_cancel').on('click', function() { + moveModal.find('.close, #move_post_cancel').on('click', function () { moveModal.addClass('hide'); }); - topicId.on('keyup change', function() { + topicId.on('keyup change', function () { moveBtn.attr('disabled', !topicId.val()); }); - moveBtn.on('click', function() { - movePost(button.parents('[data-pid]'), getData(button, 'data-pid'), topicId.val(), function() { + moveBtn.on('click', function () { + movePost(button.parents('[data-pid]'), getData(button, 'data-pid'), topicId.val(), function () { moveModal.modal('hide'); topicId.val(''); }); @@ -416,7 +479,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function parseMoveModal(callback) { - templates.parse('partials/move_post_modal', {}, function(html) { + templates.parse('partials/move_post_modal', {}, function (html) { translator.translate(html, callback); }); } @@ -430,13 +493,13 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function movePost(post, pid, tid, callback) { - socket.emit('posts.movePost', {pid: pid, tid: tid}, function(err) { + socket.emit('posts.movePost', {pid: pid, tid: tid}, function (err) { if (err) { app.alertError(err.message); return callback(); } - post.fadeOut(500, function() { + post.fadeOut(500, function () { post.remove(); }); @@ -454,39 +517,40 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function showStaleWarning(callback) { - if (ajaxify.data.lastposttime < (Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays))) { - translator.translate('[[topic:stale.warning]]', function(translated) { - var warning = bootbox.dialog({ - title: '[[topic:stale.title]]', - message: translated, - buttons: { - reply: { - label: '[[topic:stale.reply_anyway]]', - className: 'btn-link', - callback: function() { - callback(false); - } - }, - create: { - label: '[[topic:stale.create]]', - className: 'btn-primary', - callback: function() { - translator.translate('[[topic:link_back, ' + ajaxify.data.title + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function(body) { - $(window).trigger('action:composer.topic.new', { - cid: ajaxify.data.cid, - body: body - }); + if (staleReplyAnyway || ajaxify.data.lastposttime >= (Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays))) { + return callback(); + } + + translator.translate('[[topic:stale.warning]]', function (translated) { + var warning = bootbox.dialog({ + title: '[[topic:stale.title]]', + message: translated, + buttons: { + reply: { + label: '[[topic:stale.reply_anyway]]', + className: 'btn-link', + callback: function () { + staleReplyAnyway = true; + callback(); + } + }, + create: { + label: '[[topic:stale.create]]', + className: 'btn-primary', + callback: function () { + translator.translate('[[topic:link_back, ' + ajaxify.data.title + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) { + $(window).trigger('action:composer.topic.new', { + cid: ajaxify.data.cid, + body: body }); - } + }); } } - }); + } + }); - warning.modal(); - }); - } else { - callback(false); - } + warning.modal(); + }); } return PostTools; diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index b79067ec3b..bc2d297112 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -1,6 +1,6 @@ 'use strict'; -/* globals config, app, ajaxify, define, socket, utils */ +/* globals config, app, ajaxify, define, utils */ define('forum/topic/posts', [ 'forum/pagination', @@ -8,13 +8,13 @@ define('forum/topic/posts', [ 'forum/topic/postTools', 'navigator', 'components' -], function(pagination, infinitescroll, postTools, navigator, components) { +], function (pagination, infinitescroll, postTools, navigator, components) { var Posts = { _imageLoaderTimeout: undefined }; - Posts.onNewPost = function(data) { + Posts.onNewPost = function (data) { if (!data || !data.posts || !data.posts.length) { return; } @@ -24,14 +24,18 @@ define('forum/topic/posts', [ } data.loggedIn = app.user.uid ? true : false; - data.posts.forEach(function(post) { + data.privileges = ajaxify.data.privileges; + data.posts.forEach(function (post) { post.selfPost = !!app.user.uid && parseInt(post.uid, 10) === parseInt(app.user.uid, 10); - post.display_moderator_tools = post.selfPost || ajaxify.data.privileges.isAdminOrMod; + post.display_edit_tools = (ajaxify.data.privileges['posts:edit'] && post.selfPost) || ajaxify.data.privileges.isAdminOrMod; + post.display_delete_tools = (ajaxify.data.privileges['posts:delete'] && post.selfPost) || ajaxify.data.privileges.isAdminOrMod; + post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools; post.display_move_tools = ajaxify.data.privileges.isAdminOrMod; - post.display_post_menu = ajaxify.data.privileges.isAdminOrMod || post.selfPost || ((app.user.uid || ajaxify.data.postSharing.length) && !post.deleted); + post.display_post_menu = ajaxify.data.privileges.isAdminOrMod || (post.selfPost && !ajaxify.data.locked) || ((app.user.uid || ajaxify.data.postSharing.length) && !post.deleted); }); updatePostCounts(data.posts); + ajaxify.data.postcount ++; postTools.updatePostCount(ajaxify.data.postcount); @@ -43,7 +47,7 @@ define('forum/topic/posts', [ }; function updatePostCounts(posts) { - for (var i=0; i<posts.length; ++i) { + for (var i = 0; i < posts.length; ++i) { var cmp = components.get('user/postcount', posts[i].uid); cmp.html(parseInt(cmp.attr('data-postcount'), 10) + 1); utils.addCommasToNumbers(cmp); @@ -53,6 +57,7 @@ define('forum/topic/posts', [ function onNewPostPagination(data) { function scrollToPost() { scrollToPostIfSelf(data.posts[0]); + Posts.loadImages(); } var posts = data.posts; @@ -60,29 +65,44 @@ define('forum/topic/posts', [ ajaxify.data.pagination.pageCount = Math.max(1, Math.ceil((posts[0].topic.postcount - 1) / config.postsPerPage)); var direction = config.topicPostSort === 'oldest_to_newest' || config.topicPostSort === 'most_votes' ? 1 : -1; - var isPostVisible = (ajaxify.data.pagination.currentPage === ajaxify.data.pagination.pageCount && direction === 1) || (ajaxify.data.pagination.currentPage === 1 && direction === -1); + var isPostVisible = (ajaxify.data.pagination.currentPage === ajaxify.data.pagination.pageCount && direction === 1) || + (ajaxify.data.pagination.currentPage === 1 && direction === -1); if (isPostVisible) { createNewPosts(data, components.get('post').not('[data-index=0]'), direction, scrollToPost); } else if (ajaxify.data.scrollToMyPost && parseInt(posts[0].uid, 10) === parseInt(app.user.uid, 10)) { - pagination.loadPage(ajaxify.data.pagination.pageCount, scrollToPost); + // https://github.com/NodeBB/NodeBB/issues/5004#issuecomment-247157441 + setTimeout(function () { + pagination.loadPage(ajaxify.data.pagination.pageCount, scrollToPost); + }, 250); + } else { + updatePagination(); } } + function updatePagination() { + $.get(config.relative_path + '/api/topic/pagination/' + ajaxify.data.tid, {page: ajaxify.data.pagination.currentPage}, function (paginationData) { + app.parseAndTranslate('partials/paginator', {pagination: paginationData}, function (html) { + $('[component="pagination"]').after(html).remove(); + }); + }); + } + function onNewPostInfiniteScroll(data) { var direction = config.topicPostSort === 'oldest_to_newest' || config.topicPostSort === 'most_votes' ? 1 : -1; - createNewPosts(data, components.get('post').not('[data-index=0]'), direction, function(html) { + createNewPosts(data, components.get('post').not('[data-index=0]'), direction, function (html) { if (html) { html.addClass('new'); } scrollToPostIfSelf(data.posts[0]); + Posts.loadImages(); }); } function scrollToPostIfSelf(post) { if (!ajaxify.data.scrollToMyPost) { - return; + return; } var isSelfPost = parseInt(post.uid, 10) === parseInt(app.user.uid, 10); if (isSelfPost) { @@ -91,7 +111,7 @@ define('forum/topic/posts', [ } function createNewPosts(data, repliesSelector, direction, callback) { - callback = callback || function() {}; + callback = callback || function () {}; if (!data || (data.posts && !data.posts.length)) { return callback(); } @@ -101,14 +121,14 @@ define('forum/topic/posts', [ if (newPosts.length === data.posts.length) { var allSamePids = true; - newPosts.each(function(index, el) { + newPosts.each(function (index, el) { if (parseInt($(el).attr('data-pid'), 10) !== parseInt(data.posts[index].pid, 10)) { allSamePids = false; } }); if (allSamePids) { - newPosts.each(function() { + newPosts.each(function () { $(this).removeClass('new'); }); data.posts.length = 0; @@ -117,7 +137,7 @@ define('forum/topic/posts', [ } if (newPosts.length && data.posts.length > 1) { - data.posts.forEach(function(post) { + data.posts.forEach(function (post) { var p = components.get('post', 'pid', post.pid); if (p.hasClass('new')) { p.remove(); @@ -125,7 +145,7 @@ define('forum/topic/posts', [ }); } - data.posts = data.posts.filter(function(post) { + data.posts = data.posts.filter(function (post) { return $('[component="post"][data-pid="' + post.pid + '"]').length === 0; }); } @@ -148,9 +168,9 @@ define('forum/topic/posts', [ $(window).trigger('action:posts.loading', {posts: data.posts, after: after, before: before}); - app.parseAndTranslate('topic', 'posts', data, function(html) { + app.parseAndTranslate('topic', 'posts', data, function (html) { - html = html.filter(function() { + html = html.filter(function () { var pid = $(this).attr('data-pid'); return pid && $('[component="post"][data-pid="' + pid + '"]').length === 0; }); @@ -180,12 +200,12 @@ define('forum/topic/posts', [ }); } - Posts.loadMorePosts = function(direction) { + Posts.loadMorePosts = function (direction) { if (!components.get('topic').length || navigator.scrollActive || Posts._infiniteScrollTimeout) { return; } - Posts._infiniteScrollTimeout = setTimeout(function() { + Posts._infiniteScrollTimeout = setTimeout(function () { delete Posts._infiniteScrollTimeout; }, 1000); var replies = components.get('post').not('[data-index=0]').not('.new'); @@ -205,23 +225,21 @@ define('forum/topic/posts', [ infinitescroll.loadMore('topics.loadMore', { tid: tid, after: after, - direction: direction + direction: direction, + topicPostSort: config.topicPostSort }, function (data, done) { indicatorEl.fadeOut(); if (data && data.posts && data.posts.length) { createNewPosts(data, replies, direction, done); } else { - if (app.user.uid) { - socket.emit('topics.markAsRead', [tid]); - } navigator.update(); done(); } }); }; - Posts.processPage = function(posts) { + Posts.processPage = function (posts) { Posts.unloadImages(posts); Posts.showBottomPostBar(); posts.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); @@ -230,28 +248,30 @@ define('forum/topic/posts', [ utils.addCommasToNumbers(posts.find('.formatted-number')); utils.makeNumbersHumanReadable(posts.find('.human-readable-number')); posts.find('.timeago').timeago(); - Posts.wrapImagesInLinks(posts); addBlockquoteEllipses(posts.find('[component="post/content"] > blockquote > blockquote')); hidePostToolsForDeletedPosts(posts); }; - Posts.unloadImages = function(posts) { + Posts.unloadImages = function (posts) { var images = posts.find('[component="post/content"] img:not(.not-responsive)'); - images.each(function() { - $(this).attr('data-src', $(this).attr('src')); - $(this).attr('data-state', 'unloaded'); - $(this).attr('src', 'about:blank'); - }); + if (config.delayImageLoading) { + images.each(function () { + $(this).attr('data-src', $(this).attr('src')); + }).attr('data-state', 'unloaded').attr('src', 'about:blank'); + } else { + images.attr('data-state', 'loaded'); + Posts.wrapImagesInLinks(posts); + } }; - Posts.loadImages = function(threshold) { + Posts.loadImages = function (threshold) { if (Posts._imageLoaderTimeout) { clearTimeout(Posts._imageLoaderTimeout); } - Posts._imageLoaderTimeout = setTimeout(function() { + Posts._imageLoaderTimeout = setTimeout(function () { /* If threshold is defined, images loaded above this threshold will modify the user's scroll position so they are not scrolled away from content @@ -262,13 +282,16 @@ define('forum/topic/posts', [ */ var images = components.get('post/content').find('img[data-state="unloaded"]'), - visible = images.filter(function() { + visible = images.filter(function () { return utils.isElementInViewport(this); }), + posts = $.unique(visible.map(function () { + return $(this).parents('[component="post"]').get(0); + })), scrollTop = $(window).scrollTop(), adjusting = false, adjustQueue = [], - adjustPosition = function() { + adjustPosition = function () { adjusting = true; oldHeight = document.body.clientHeight; @@ -286,16 +309,19 @@ define('forum/topic/posts', [ adjustQueue.pop()(); } else { adjusting = false; + + Posts.wrapImagesInLinks(posts); + posts.length = 0; } }, oldHeight, newHeight; // For each image, reset the source and adjust scrollTop when loaded visible.attr('data-state', 'loading'); - visible.each(function(index, image) { + visible.each(function (index, image) { image = $(image); - image.on('load', function() { + image.on('load', function () { if (!adjusting) { adjustPosition.call(this); } else { @@ -304,35 +330,45 @@ define('forum/topic/posts', [ }); image.attr('src', image.attr('data-src')); - if (image.parent().attr('href') === 'about:blank') { - image.parent().attr('href', image.attr('data-src')); - } image.removeAttr('data-src'); }); }, 250); }; - Posts.wrapImagesInLinks = function(posts) { - posts.find('[component="post/content"] img:not(.emoji)').each(function() { - var $this = $(this); + Posts.wrapImagesInLinks = function (posts) { + posts.find('[component="post/content"] img:not(.emoji)').each(function () { + var $this = $(this), + src = $this.attr('src'), + suffixRegex = /-resized(\.[\w]+)?$/; + + if (src === 'about:blank') { + return; + } + + if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { + src = src.replace(suffixRegex, '$1'); + } + if (!$this.parent().is('a')) { - $this.wrap('<a href="' + $this.attr('src') + '" target="_blank">'); + $this.wrap('<a href="' + src + '" target="_blank">'); } }); }; - Posts.showBottomPostBar = function() { + Posts.showBottomPostBar = function () { var mainPost = components.get('post', 'index', 0); + var placeHolder = $('.post-bar-placeholder'); var posts = $('[component="post"]'); - if (!!mainPost.length && posts.length > 1 && $('.post-bar').length < 2) { - $('.post-bar').clone().appendTo(mainPost); + if (!!mainPost.length && posts.length > 1 && $('.post-bar').length < 2 && placeHolder.length) { + $('.post-bar').clone().insertAfter(placeHolder); + placeHolder.remove(); } else if (mainPost.length && posts.length < 2) { mainPost.find('.post-bar').remove(); } }; function hidePostToolsForDeletedPosts(posts) { - posts.each(function() { + posts.each(function () { if ($(this).hasClass('deleted')) { postTools.toggle($(this).attr('data-pid'), true); } @@ -340,7 +376,7 @@ define('forum/topic/posts', [ } function addBlockquoteEllipses(blockquotes) { - blockquotes.each(function() { + blockquotes.each(function () { var $this = $(this); if ($this.find(':hidden:not(br)').length && !$this.find('.toggle').length) { $this.append('<i class="fa fa-angle-down pointer toggle"></i>'); diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 7b5b9605f7..7eef6306a3 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -8,53 +8,53 @@ define('forum/topic/threadTools', [ 'forum/topic/delete-posts', 'components', 'translator' -], function(fork, move, deletePosts, components, translator) { +], function (fork, move, deletePosts, components, translator) { var ThreadTools = {}; - ThreadTools.init = function(tid) { + ThreadTools.init = function (tid) { renderMenu(); var topicContainer = $('.topic'); - topicContainer.on('click', '[component="topic/delete"]', function() { + topicContainer.on('click', '[component="topic/delete"]', function () { topicCommand('delete', tid); return false; }); - topicContainer.on('click', '[component="topic/restore"]', function() { + topicContainer.on('click', '[component="topic/restore"]', function () { topicCommand('restore', tid); return false; }); - topicContainer.on('click', '[component="topic/purge"]', function() { + topicContainer.on('click', '[component="topic/purge"]', function () { topicCommand('purge', tid); return false; }); - topicContainer.on('click', '[component="topic/lock"]', function() { + topicContainer.on('click', '[component="topic/lock"]', function () { socket.emit('topics.lock', {tids: [tid], cid: ajaxify.data.cid}); return false; }); - topicContainer.on('click', '[component="topic/unlock"]', function() { + topicContainer.on('click', '[component="topic/unlock"]', function () { socket.emit('topics.unlock', {tids: [tid], cid: ajaxify.data.cid}); return false; }); - topicContainer.on('click', '[component="topic/pin"]', function() { + topicContainer.on('click', '[component="topic/pin"]', function () { socket.emit('topics.pin', {tids: [tid], cid: ajaxify.data.cid}); return false; }); - topicContainer.on('click', '[component="topic/unpin"]', function() { + topicContainer.on('click', '[component="topic/unpin"]', function () { socket.emit('topics.unpin', {tids: [tid], cid: ajaxify.data.cid}); return false; }); - topicContainer.on('click', '[component="topic/mark-unread"]', function() { - socket.emit('topics.markUnread', tid, function(err) { + topicContainer.on('click', '[component="topic/mark-unread"]', function () { + socket.emit('topics.markUnread', tid, function (err) { if (err) { return app.alertError(err); } @@ -63,9 +63,9 @@ define('forum/topic/threadTools', [ return false; }); - topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function() { + topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () { var btn = $(this); - socket.emit('topics.markAsUnreadForAll', [tid], function(err) { + socket.emit('topics.markAsUnreadForAll', [tid], function (err) { if (err) { return app.alertError(err.message); } @@ -75,7 +75,7 @@ define('forum/topic/threadTools', [ return false; }); - topicContainer.on('click', '[component="topic/move"]', function() { + topicContainer.on('click', '[component="topic/move"]', function () { move.init([tid], ajaxify.data.cid); return false; }); @@ -83,12 +83,18 @@ define('forum/topic/threadTools', [ deletePosts.init(); fork.init(); - components.get('topic').on('click', '[component="topic/follow"], [component="topic/unfollow"]', follow); - components.get('topic/follow').off('click').on('click', follow); - components.get('topic/unfollow').off('click').on('click', follow); + $('.topic').on('click', '[component="topic/following"]', function () { + changeWatching('follow'); + }); + $('.topic').on('click', '[component="topic/not-following"]', function () { + changeWatching('unfollow'); + }); + $('.topic').on('click', '[component="topic/ignoring"]', function () { + changeWatching('ignore'); + }); - function follow() { - socket.emit('topics.toggleFollow', tid, function(err, state) { + function changeWatching(type) { + socket.emit('topics.changeWatching', {tid: tid, type: type}, function (err) { if (err) { return app.alert({ type: 'danger', @@ -98,15 +104,24 @@ define('forum/topic/threadTools', [ timeout: 5000 }); } - - setFollowState(state); + var message = ''; + if (type === 'follow') { + message = '[[topic:following_topic.message]]'; + } else if (type === 'unfollow') { + message = '[[topic:not_following_topic.message]]'; + } else if (type === 'ignore') { + message = '[[topic:ignoring_topic.message]]'; + } + setFollowState(type); app.alert({ alert_id: 'follow_thread', - message: state ? '[[topic:following_topic.message]]' : '[[topic:not_following_topic.message]]', + message: message, type: 'success', timeout: 5000 }); + + $(window).trigger('action:topics.changeWatching', {tid: tid, type: type}); }); return false; @@ -121,13 +136,13 @@ define('forum/topic/threadTools', [ return; } - socket.emit('topics.loadTopicTools', {tid: ajaxify.data.tid, cid: ajaxify.data.cid}, function(err, data) { + socket.emit('topics.loadTopicTools', {tid: ajaxify.data.tid, cid: ajaxify.data.cid}, function (err, data) { if (err) { return app.alertError(err); } - templates.parse('partials/topic/topic-menu-list', data, function(html) { - translator.translate(html, function(html) { + templates.parse('partials/topic/topic-menu-list', data, function (html) { + translator.translate(html, function (html) { dropdownMenu.html(html); $(window).trigger('action:topic.tools.load'); }); @@ -137,13 +152,13 @@ define('forum/topic/threadTools', [ } function topicCommand(command, tid) { - translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function(msg) { - bootbox.confirm(msg, function(confirm) { + translator.translate('[[topic:thread_tools.' + command + '_confirm]]', function (msg) { + bootbox.confirm(msg, function (confirm) { if (!confirm) { return; } - socket.emit('topics.' + command, {tids: [tid], cid: ajaxify.data.cid}, function(err) { + socket.emit('topics.' + command, {tids: [tid], cid: ajaxify.data.cid}, function (err) { if (err) { app.alertError(err.message); } @@ -152,24 +167,33 @@ define('forum/topic/threadTools', [ }); } - ThreadTools.setLockedState = function(data) { + ThreadTools.setLockedState = function (data) { var threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; } - var isLocked = data.isLocked && !app.user.isAdmin; + var isLocked = data.isLocked && !ajaxify.data.privileges.isAdminOrMod; components.get('topic/lock').toggleClass('hidden', data.isLocked); components.get('topic/unlock').toggleClass('hidden', !data.isLocked); - components.get('topic/reply').toggleClass('hidden', isLocked); - components.get('topic/reply/locked').toggleClass('hidden', !isLocked); - threadEl.find('[component="post/reply"], [component="post/quote"], [component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked); + var hideReply = (data.isLocked || ajaxify.data.deleted) && !ajaxify.data.privileges.isAdminOrMod; + + components.get('topic/reply/container').toggleClass('hidden', hideReply); + components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !data.isLocked || ajaxify.data.deleted); + + threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); + threadEl.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked); + + threadEl.find('[component="post"][data-uid="' + app.user.uid + '"].deleted [component="post/tools"]').toggleClass('hidden', isLocked); + $('[component="post/header"] i.fa-lock').toggleClass('hidden', !data.isLocked); + $('[component="post/tools"] .dropdown-menu').html(''); + ajaxify.data.locked = data.isLocked; }; - ThreadTools.setDeleteState = function(data) { + ThreadTools.setDeleteState = function (data) { var threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; @@ -180,10 +204,18 @@ define('forum/topic/threadTools', [ components.get('topic/purge').toggleClass('hidden', !data.isDelete); components.get('topic/deleted/message').toggleClass('hidden', !data.isDelete); + var hideReply = data.isDelete && !ajaxify.data.privileges.isAdminOrMod; + + components.get('topic/reply/container').toggleClass('hidden', hideReply); + components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !ajaxify.data.locked || data.isDelete); + threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); + threadEl.toggleClass('deleted', data.isDelete); + ajaxify.data.deleted = data.isDelete; }; - ThreadTools.setPinnedState = function(data) { + + ThreadTools.setPinnedState = function (data) { var threadEl = components.get('topic'); if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { return; @@ -192,11 +224,21 @@ define('forum/topic/threadTools', [ components.get('topic/pin').toggleClass('hidden', data.isPinned); components.get('topic/unpin').toggleClass('hidden', !data.isPinned); $('[component="post/header"] i.fa-thumb-tack').toggleClass('hidden', !data.isPinned); + ajaxify.data.pinned = data.isPinned; }; function setFollowState(state) { - components.get('topic/follow').toggleClass('hidden', state); - components.get('topic/unfollow').toggleClass('hidden', !state); + var menu = components.get('topic/following/menu'); + menu.toggleClass('hidden', state !== 'follow'); + components.get('topic/following/check').toggleClass('fa-check', state === 'follow'); + + menu = components.get('topic/not-following/menu'); + menu.toggleClass('hidden', state !== 'unfollow'); + components.get('topic/not-following/check').toggleClass('fa-check', state === 'unfollow'); + + menu = components.get('topic/ignoring/menu'); + menu.toggleClass('hidden', state !== 'ignore' ); + components.get('topic/ignoring/check').toggleClass('fa-check', state === 'ignore'); } diff --git a/public/src/client/unread.js b/public/src/client/unread.js index 47b96e54ad..e185788981 100644 --- a/public/src/client/unread.js +++ b/public/src/client/unread.js @@ -2,30 +2,32 @@ /* globals define, app, socket */ -define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', 'components'], function(recent, topicSelect, infinitescroll, components) { +define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', 'components'], function (recent, topicSelect, infinitescroll, components) { var Unread = {}; - $(window).on('action:ajaxify.start', function(ev, data) { + $(window).on('action:ajaxify.start', function (ev, data) { if (ajaxify.currentPage !== data.url) { recent.removeListeners(); } }); - Unread.init = function() { + Unread.init = function () { app.enterRoom('unread_topics'); - $('#new-topics-alert').on('click', function() { + $('#new-topics-alert').on('click', function () { $(this).addClass('hide'); }); recent.watchForNewPosts(); - $('#markSelectedRead').on('click', function() { + $(window).trigger('action:topics.loaded', {topics: ajaxify.data.topics}); + + $('#markSelectedRead').on('click', function () { var tids = topicSelect.getSelectedTids(); if(!tids.length) { return; } - socket.emit('topics.markAsRead', tids, function(err) { + socket.emit('topics.markAsRead', tids, function (err) { if(err) { return app.alertError(err.message); } @@ -34,8 +36,8 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' }); }); - $('#markAllRead').on('click', function() { - socket.emit('topics.markAllRead', function(err) { + $('#markAllRead').on('click', function () { + socket.emit('topics.markAllRead', function (err) { if(err) { return app.alertError(err.message); } @@ -43,15 +45,16 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' app.alertSuccess('[[unread:topics_marked_as_read.success]]'); $('[component="category"]').empty(); + $('[component="pagination"]').addClass('hidden'); $('#category-no-topics').removeClass('hidden'); $('.markread').addClass('hidden'); }); }); - $('.markread').on('click', '.category', function() { + $('.markread').on('click', '.category', function () { function getCategoryTids(cid) { var tids = []; - components.get('category/topic', 'cid', cid).each(function() { + components.get('category/topic', 'cid', cid).each(function () { tids.push($(this).attr('data-tid')); }); return tids; @@ -59,7 +62,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' var cid = $(this).attr('data-cid'); var tids = getCategoryTids(cid); - socket.emit('topics.markCategoryTopicsRead', cid, function(err) { + socket.emit('topics.markCategoryTopicsRead', cid, function (err) { if(err) { return app.alertError(err.message); } @@ -74,11 +77,13 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' $('#load-more-btn').show(); } - $('#load-more-btn').on('click', function() { + $('#load-more-btn').on('click', function () { loadMoreTopics(); }); - infinitescroll.init(loadMoreTopics); + if (!config.usePagination) { + infinitescroll.init(loadMoreTopics); + } function loadMoreTopics(direction) { if(direction < 0 || !$('[component="category"]').length) { @@ -88,8 +93,9 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' var cid = params.cid; infinitescroll.loadMore('topics.loadMoreUnreadTopics', { after: $('[component="category"]').attr('data-nextstart'), - cid: cid - }, function(data, done) { + cid: cid, + filter: ajaxify.data.selectedFilter.filter + }, function (data, done) { if (data.topics && data.topics.length) { recent.onTopicsLoaded('unread', data.topics, true, done); $('[component="category"]').attr('data-nextstart', data.nextStart); @@ -113,7 +119,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' } function removeTids(tids) { - for(var i=0; i<tids.length; ++i) { + for(var i = 0; i < tids.length; ++i) { components.get('category/topic', 'tid', tids[i]).remove(); } } diff --git a/public/src/client/users.js b/public/src/client/users.js index 4091390068..bbf11b46db 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -1,16 +1,24 @@ 'use strict'; -/* globals define, socket, app, templates, bootbox, ajaxify */ +/* globals define, socket, app, templates, bootbox, utils */ -define('forum/users', ['translator'], function(translator) { +define('forum/users', ['translator'], function (translator) { var Users = {}; - var loadingMoreUsers = false; + var searchTimeoutID = 0; - Users.init = function() { + $(window).on('action:ajaxify.start', function () { + if (searchTimeoutID) { + clearTimeout(searchTimeoutID); + searchTimeoutID = 0; + } + }); + + Users.init = function () { app.enterRoom('user_list'); - $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + '"]').parent().addClass('active'); + var section = utils.params().section ? ('?section=' + utils.params().section) : ''; + $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + section + '"]').parent().addClass('active'); handleSearch(); @@ -18,104 +26,53 @@ define('forum/users', ['translator'], function(translator) { socket.removeListener('event:user_status_change', onUserStatusChange); socket.on('event:user_status_change', onUserStatusChange); - - $('#load-more-users-btn').on('click', loadMoreUsers); - - $(window).off('scroll').on('scroll', function() { - var bottom = ($(document).height() - $(window).height()) * 0.9; - - if ($(window).scrollTop() > bottom && !loadingMoreUsers) { - loadMoreUsers(); - } - }); }; - function loadMoreUsers() { - if ($('#search-user').val()) { - return; - } - - if (ajaxify.data.setName) { - startLoading(ajaxify.data.setName, $('#users-container').children('.registered-user').length); - } - } - - function startLoading(set, after) { - loadingMoreUsers = true; - - socket.emit('user.loadMore', { - set: set, - after: after - }, function(err, data) { - if (data && data.users.length) { - onUsersLoaded(data); - $('#load-more-users-btn').removeClass('disabled'); - } else { - $('#load-more-users-btn').addClass('disabled'); - } - loadingMoreUsers = false; - }); - } - - function onUsersLoaded(data) { - data.users = data.users.filter(function(user) { - return !$('.users-box[data-uid="' + user.uid + '"]').length; - }); - - templates.parse('users', 'users', data, function(html) { - translator.translate(html, function(translated) { - translated = $(translated); - $('#users-container').append(translated); - translated.find('span.timeago').timeago(); - $('#users-container .anon-user').appendTo($('#users-container')); - }); - }); - } - function handleSearch() { - var timeoutId = 0; + searchTimeoutID = 0; - $('#search-user').on('keyup', function() { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = 0; + $('#search-user').on('keyup', function () { + if (searchTimeoutID) { + clearTimeout(searchTimeoutID); + searchTimeoutID = 0; } - timeoutId = setTimeout(doSearch, 250); + searchTimeoutID = setTimeout(doSearch, 150); }); - $('.search select, .search input[type="checkbox"]').on('change', function() { + $('.search select, .search input[type="checkbox"]').on('change', function () { doSearch(); }); - - $('.users').on('click', '.pagination a', function() { - doSearch($(this).attr('data-page')); - return false; - }); } - function doSearch(page) { + function doSearch() { + $('[component="user/search/icon"]').removeClass('fa-search').addClass('fa-spinner fa-spin'); var username = $('#search-user').val(); - page = page || 1; + var activeSection = getActiveSection(); + + var query = { + section: activeSection, + page: 1 + }; if (!username) { - return loadPage(page); + return loadPage(query); } - socket.emit('user.search', { - query: username, - page: page, - searchBy: 'username', - sortBy: $('.search select').val() || getSortBy(), - onlineOnly: $('.search .online-only').is(':checked') || (getActiveSection() === 'online'), - bannedOnly: getActiveSection() === 'banned' - }, function(err, data) { - if (err) { - return app.alertError(err.message); - } + query.term = username; + query.sortBy = getSortBy(); - renderSearchResults(data); - }); + if ($('.search .online-only').is(':checked') || (activeSection === 'online')) { + query.onlineOnly = true; + } + if (activeSection === 'banned') { + query.bannedOnly = true; + } + if (activeSection === 'flagged') { + query.flaggedOnly = true; + } + + loadPage(query); } function getSortBy() { @@ -131,25 +88,27 @@ define('forum/users', ['translator'], function(translator) { return sortBy; } - function loadPage(page) { - var section = getActiveSection(); - section = section !== 'users' ? section : ''; - $.get('/api/users/' + section + '?page=' + page, function(data) { - renderSearchResults(data); + + function loadPage(query) { + var qs = decodeURIComponent($.param(query)); + $.get(config.relative_path + '/api/users?' + qs, renderSearchResults).fail(function (xhrErr) { + if (xhrErr && xhrErr.responseJSON && xhrErr.responseJSON.error) { + app.alertError(xhrErr.responseJSON.error); + } }); } function renderSearchResults(data) { - $('#load-more-users-btn').addClass('hide'); - templates.parse('partials/paginator', {pagination: data.pagination}, function(html) { + templates.parse('partials/paginator', {pagination: data.pagination}, function (html) { $('.pagination-container').replaceWith(html); }); - templates.parse('users', 'users', data, function(html) { - translator.translate(html, function(translated) { + templates.parse('users', 'users', data, function (html) { + translator.translate(html, function (translated) { translated = $(translated); $('#users-container').html(translated); translated.find('span.timeago').timeago(); + $('[component="user/search/icon"]').addClass('fa-search').removeClass('fa-spinner fa-spin'); }); }); } @@ -163,23 +122,21 @@ define('forum/users', ['translator'], function(translator) { } function updateUser(data) { - app.updateUserStatus($('#users-container [data-uid="' + data.uid +'"] [component="user/status"]'), data.status); + app.updateUserStatus($('#users-container [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); } function getActiveSection() { - var url = window.location.href, - parts = url.split('/'); - return parts[parts.length - 1]; + return utils.params().section || ''; } function handleInvite() { - $('[component="user/invite"]').on('click', function() { - bootbox.prompt('Email: ', function(email) { + $('[component="user/invite"]').on('click', function () { + bootbox.prompt('Email: ', function (email) { if (!email) { return; } - socket.emit('user.invite', email, function(err) { + socket.emit('user.invite', email, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/installer/install.js b/public/src/installer/install.js index f7224fb71a..cb645771e4 100644 --- a/public/src/installer/install.js +++ b/public/src/installer/install.js @@ -1,7 +1,7 @@ "use strict"; /*global utils*/ -$('document').ready(function() { +$('document').ready(function () { setupInputs(); $('[name="username"]').focus(); @@ -19,7 +19,7 @@ $('document').ready(function() { function setupInputs() { - $('form').on('focus', '.form-control', function() { + $('form').on('focus', '.form-control', function () { var parent = $(this).parents('.input-row'); $('.input-row.active').removeClass('active'); @@ -29,7 +29,7 @@ $('document').ready(function() { help.html(help.attr('data-help')); }); - $('form').on('blur change', '[name]', function() { + $('form').on('blur change', '[name]', function () { activate($(this).attr('name'), $(this)); }); @@ -37,14 +37,14 @@ $('document').ready(function() { } function validateAll(ev) { - $('form .admin [name]').each(function() { + $('form .admin [name]').each(function () { activate($(this).attr('name'), $(this)); }); if ($('form .admin .error').length) { ev.preventDefault(); $('html, body').animate({'scrollTop': '0px'}, 400); - + return false; } else { $('#submit .fa-spin').removeClass('hide'); @@ -69,6 +69,9 @@ $('document').ready(function() { if (!utils.isPasswordValid(field)) { parent.addClass('error'); help.html('Invalid Password.'); + } else if (field.length < $('[name="admin:password"]').attr('data-minimum-length')) { + parent.addClass('error'); + help.html('Password is too short.'); } else { parent.removeClass('error'); } @@ -113,9 +116,9 @@ $('document').ready(function() { function launchForum() { $('#launch .fa-spin').removeClass('hide'); - $.post('/launch', function() { - setInterval(function() { - $.get('/admin').done(function(data) { + $.post('/launch', function () { + setInterval(function () { + $.get('/admin').done(function (data) { window.location = 'admin'; }); }, 750); diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js index 3700b64e0d..1c9b0bd086 100644 --- a/public/src/modules/alerts.js +++ b/public/src/modules/alerts.js @@ -1,7 +1,7 @@ 'use strict'; /* globals define, templates */ -define('alerts', ['translator', 'components'], function(translator, components) { +define('alerts', ['translator', 'components'], function (translator, components) { var module = {}; module.alert = function (params) { @@ -19,8 +19,8 @@ define('alerts', ['translator', 'components'], function(translator, components) }; function createNew(params) { - templates.parse('alert', params, function(alertTpl) { - translator.translate(alertTpl, function(translatedHTML) { + templates.parse('alert', params, function (alertTpl) { + translator.translate(alertTpl, function (translatedHTML) { var alert = $('#' + params.alert_id); if (alert.length) { return updateAlert(alert, params); @@ -31,7 +31,7 @@ define('alerts', ['translator', 'components'], function(translator, components) components.get('toaster/tray').prepend(alert); if(typeof params.closefn === 'function') { - alert.find('button').on('click', function() { + alert.find('button').on('click', function () { params.closefn(); fadeOut(alert); return false; @@ -56,7 +56,7 @@ define('alerts', ['translator', 'components'], function(translator, components) }); } - module.remove = function(id) { + module.remove = function (id) { $('#alert_button_' + id).remove(); }; @@ -71,7 +71,7 @@ define('alerts', ['translator', 'components'], function(translator, components) } alert.children().fadeOut(100); - translator.translate(alert.html(), function(translatedHTML) { + translator.translate(alert.html(), function (translatedHTML) { alert.children().fadeIn(100); alert.html(translatedHTML); }); diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 60a5550b8c..0a6c664d52 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -3,25 +3,25 @@ /* globals define, socket, app */ -define('autocomplete', function() { +define('autocomplete', function () { var module = {}; module.user = function (input, onselect) { - app.loadJQueryUI(function() { + app.loadJQueryUI(function () { input.autocomplete({ delay: 200, - open: function() { + open: function () { $(this).autocomplete('widget').css('z-index', 20000); }, select: onselect, - source: function(request, response) { - socket.emit('user.search', {query: request.term}, function(err, result) { + 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) { + var names = result.users.map(function (user) { var username = $('<div/>').html(user.username).text() return user && { label: username, @@ -42,21 +42,21 @@ define('autocomplete', function() { }); }; - module.group = function(input, onselect) { - app.loadJQueryUI(function() { + module.group = function (input, onselect) { + app.loadJQueryUI(function () { input.autocomplete({ delay: 200, select: onselect, - source: function(request, response) { + source: function (request, response) { socket.emit('groups.search', { query: request.term - }, function(err, results) { + }, function (err, results) { if (err) { return app.alertError(err.message); } if (results && results.length) { - var names = results.map(function(group) { + var names = results.map(function (group) { return group && { label: group.name, value: group.name, diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index e860a0ff20..af7ef0fff3 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -1,16 +1,24 @@ "use strict"; /* globals app, define, socket, templates, utils, ajaxify */ -define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'translator'], function(components, taskbar, S, sounds, Chats, translator) { +define('chat', [ + 'components', + 'taskbar', + 'string', + 'sounds', + 'forum/chats', + 'forum/chats/messages', + 'translator' +], function (components, taskbar, S, sounds, Chats, ChatsMessages, translator) { var module = {}; var newMessage = false; - module.prepareDOM = function() { + module.prepareDOM = function () { var chatsToggleEl = components.get('chat/dropdown'); var chatsListEl = components.get('chat/list'); - chatsToggleEl.on('click', function() { + chatsToggleEl.on('click', function () { if (chatsToggleEl.parent().hasClass('open')) { return; } @@ -18,15 +26,27 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra module.loadChatsDropdown(chatsListEl); }); - $('[component="chats/mark-all-read"]').on('click', function() { - socket.emit('modules.chats.markAllRead', function(err) { + chatsListEl.on('click', '[data-roomid]', function (ev) { + if ($(ev.target).parents('.user-link').length) { + return; + } + var roomId = $(this).attr('data-roomid'); + if (!ajaxify.currentPage.match(/^chats\//)) { + app.openChat(roomId); + } else { + ajaxify.go('user/' + app.user.userslug + '/chats/' + roomId); + } + }); + + $('[component="chats/mark-all-read"]').on('click', function () { + socket.emit('modules.chats.markAllRead', function (err) { if (err) { return app.alertError(err); } }); }); - socket.on('event:chats.receive', function(data) { + socket.on('event:chats.receive', function (data) { var username = data.message.fromUser.username; var isSelf = data.self === 1; data.message.self = data.self; @@ -35,11 +55,11 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra if (module.modalExists(data.roomId)) { var modal = module.getModal(data.roomId); - Chats.appendChatMessage(modal.find('.chat-content'), data.message); + ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message); if (modal.is(':visible')) { taskbar.updateActive(modal.attr('UUID')); - Chats.scrollToBottom(modal.find('.chat-content')); + ChatsMessages.scrollToBottom(modal.find('.chat-content')); } else { module.toggleNew(modal.attr('UUID'), true, true); } @@ -55,15 +75,16 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra }); } } else { - socket.emit('modules.chats.loadRoom', {roomId: data.roomId}, function(err, roomData) { + socket.emit('modules.chats.loadRoom', {roomId: data.roomId}, function (err, roomData) { if (err) { return app.alertError(err.message); } - roomData.users = roomData.users.filter(function(user) { + roomData.users = roomData.users.filter(function (user) { return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); }); roomData.silent = true; - module.createModal(roomData, function(modal) { + roomData.uid = app.user.uid; + module.createModal(roomData, function (modal) { module.toggleNew(modal.attr('UUID'), !isSelf, true); if (!isSelf) { app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + username + ']]'); @@ -74,72 +95,40 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra } }); - socket.on('event:user_status_change', function(data) { + socket.on('event:user_status_change', function (data) { var modal = module.getModal(data.uid); app.updateUserStatus(modal.find('[component="user/status"]'), data.status); }); - socket.on('event:chats.roomRename', function(data) { + socket.on('event:chats.roomRename', function (data) { module.getModal(data.roomId).find('[component="chat/room/name"]').val($('<div/>').html(data.newName).text()); }); - Chats.onChatEdit(); + ChatsMessages.onChatMessageEdit(); }; - module.loadChatsDropdown = function(chatsListEl) { - socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, data) { + module.loadChatsDropdown = function (chatsListEl) { + socket.emit('modules.chats.getRecentChats', {uid: app.user.uid, after: 0}, function (err, data) { if (err) { return app.alertError(err.message); } - var rooms = data.rooms; - - chatsListEl.empty(); - - if (!rooms.length) { - translator.translate('[[modules:chat.no_active]]', function(str) { - $('<li />') - .addClass('no_active') - .html('<a href="#">' + str + '</a>') - .appendTo(chatsListEl); - }); - return; - } - - rooms.forEach(function(roomObj) { - function createUserImage(userObj) { - return '<a data-ajaxify="false">' + - (userObj.picture ? - '<img src="' + userObj.picture + '" title="' + userObj.username +'" />' : - '<div class="user-icon" style="background-color: ' + userObj['icon:bgColor'] + '">' + userObj['icon:text'] + '</div>') + - '<i class="fa fa-circle status ' + userObj.status + '"></i> ' + - roomObj.usernames + '</a>'; - } - - var dropdownEl = $('<li class="' + (roomObj.unread ? 'unread' : '') + '"/>') - .attr('data-roomId', roomObj.roomId) - .appendTo(chatsListEl); - - if (roomObj.lastUser) { - dropdownEl.append(createUserImage(roomObj.lastUser)); - } else { - translator.translate('[[modules:chat.no-users-in-room]]', function(str) { - dropdownEl.append(str); - }); - } + var rooms = data.rooms.filter(function (room) { + return room.teaser; + }); - dropdownEl.click(function() { - if (!ajaxify.currentPage.match(/^chats\//)) { - app.openChat(roomObj.roomId); - } else { - ajaxify.go('chats/' + roomObj.roomId); - } + templates.parse('partials/chat_dropdown', { + rooms: rooms + }, function (html) { + translator.translate(html, function (translated) { + chatsListEl.empty().html(translated); + app.createUserTooltips(chatsListEl, 'right'); }); }); }); }; - module.bringModalToTop = function(chatModal) { + module.bringModalToTop = function (chatModal) { var topZ = 0; taskbar.updateActive(chatModal.attr('UUID')); @@ -147,7 +136,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra if ($('.chat-modal').length === 1) { return; } - $('.chat-modal').each(function() { + $('.chat-modal').each(function () { var thisZ = parseInt($(this).css('zIndex'), 10); if (thisZ > topZ) { topZ = thisZ; @@ -157,156 +146,133 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra chatModal.css('zIndex', topZ + 1); }; - module.getModal = function(roomId) { + module.getModal = function (roomId) { return $('#chat-modal-' + roomId); }; - module.modalExists = function(roomId) { + module.modalExists = function (roomId) { return $('#chat-modal-' + roomId).length !== 0; }; - function checkStatus(chatModal) { - socket.emit('user.checkStatus', chatModal.attr('touid'), function(err, status) { - if (err) { - return app.alertError(err.message); - } - - app.updateUserStatus(chatModal.find('[component="user/status"]'), status); - }); - } - - module.createModal = function(data, callback) { - templates.parse('chat', data, function(chatTpl) { - translator.translate(chatTpl, function (chatTpl) { - - var chatModal = $(chatTpl), - uuid = utils.generateUUID(), - dragged = false; - - chatModal.attr('id', 'chat-modal-' + data.roomId); - chatModal.attr('roomId', data.roomId); - chatModal.attr('intervalId', 0); - chatModal.attr('UUID', uuid); - chatModal.css('position', 'fixed'); - chatModal.css('zIndex', 100); - chatModal.appendTo($('body')); - module.center(chatModal); - - app.loadJQueryUI(function() { - chatModal.find('.modal-content').resizable({ - minHeight: 250, - minWidth: 400 - }); - - chatModal.find('.modal-content').on('resize', function(event, ui) { - if (ui.originalSize.height === ui.size.height) { - return; - } + module.createModal = function (data, callback) { + app.parseAndTranslate('chat', data, function (chatModal) { + + var uuid = utils.generateUUID(); + var dragged = false; + + chatModal.attr('id', 'chat-modal-' + data.roomId); + chatModal.attr('roomId', data.roomId); + chatModal.attr('intervalId', 0); + chatModal.attr('UUID', uuid); + chatModal.css('position', 'fixed'); + chatModal.css('zIndex', 100); + chatModal.appendTo($('body')); + chatModal.find('.timeago').timeago(); + module.center(chatModal); + + app.loadJQueryUI(function () { + chatModal.find('.modal-content').resizable({ + handles: 'n, e, s, w, se', + minHeight: 250, + minWidth: 400 + }); - chatModal.find('.chat-content').css('height', module.calculateChatListHeight(chatModal)); - }); + chatModal.find('.modal-content').on('resize', function (event, ui) { + if (ui.originalSize.height === ui.size.height) { + return; + } - chatModal.draggable({ - start:function() { - module.bringModalToTop(chatModal); - }, - stop:function() { - chatModal.find('#chat-message-input').focus(); - }, - distance: 10, - handle: '.modal-header' - }); + chatModal.find('.chat-content').css('height', module.calculateChatListHeight(chatModal)); }); - chatModal.find('#chat-close-btn').on('click', function() { - module.close(chatModal); + chatModal.draggable({ + start:function () { + module.bringModalToTop(chatModal); + }, + stop:function () { + chatModal.find('#chat-message-input').focus(); + }, + distance: 10, + handle: '.modal-header' }); + }); - function gotoChats() { - var text = components.get('chat/input').val(); - $(window).one('action:ajaxify.end', function() { - components.get('chat/input').val(text); - }); + chatModal.find('#chat-close-btn').on('click', function () { + module.close(chatModal); + }); - ajaxify.go('chats/' + chatModal.attr('roomId')); - module.close(chatModal); - } + function gotoChats() { + var text = components.get('chat/input').val(); + $(window).one('action:ajaxify.end', function () { + components.get('chat/input').val(text); + }); - chatModal.find('.modal-header').on('dblclick', gotoChats); - chatModal.find('button[data-action="maximize"]').on('click', gotoChats); + ajaxify.go('user/' + app.user.userslug + '/chats/' + chatModal.attr('roomId')); + module.close(chatModal); + } - chatModal.on('click', function() { - module.bringModalToTop(chatModal); + chatModal.find('.modal-header').on('dblclick', gotoChats); + chatModal.find('button[data-action="maximize"]').on('click', gotoChats); - if (dragged) { - dragged = false; - } - }); + chatModal.on('click', function () { + module.bringModalToTop(chatModal); - chatModal.on('mousemove', function(e) { - if (e.which === 1) { - dragged = true; - } - }); + if (dragged) { + dragged = false; + } + }); - chatModal.on('mousemove keypress click', function() { - if (newMessage) { - socket.emit('modules.chats.markRead', data.roomId); - newMessage = false; - } - }); + chatModal.on('mousemove', function (e) { + if (e.which === 1) { + dragged = true; + } + }); - chatModal.find('[component="chat/messages"]') - .on('click', '[data-action="edit"]', function() { - var messageId = $(this).parents('[data-mid]').attr('data-mid'); - var inputEl = chatModal.find('[component="chat/input"]'); - Chats.prepEdit(inputEl, messageId, data.roomId); - }) - .on('click', '[data-action="delete"]', function() { - var messageId = $(this).parents('[data-mid]').attr('data-mid'); - Chats.delete(messageId, data.roomId); - }); + chatModal.on('mousemove keypress click', function () { + if (newMessage) { + socket.emit('modules.chats.markRead', data.roomId); + newMessage = false; + } + }); - chatModal.find('[component="chat/controlsToggle"]').on('click', function() { - var messagesEl = chatModal.find('[component="chat/messages"]'); + Chats.addEditDeleteHandler(chatModal.find('[component="chat/messages"]'), data.roomId); - chatModal.find('[component="chat/controls"]').toggle(); - messagesEl.css('height', module.calculateChatListHeight(chatModal)); - }); + chatModal.find('[component="chat/controlsToggle"]').on('click', function () { + var messagesEl = chatModal.find('[component="chat/messages"]'); - Chats.addSinceHandler(chatModal.attr('roomId'), chatModal.find('.chat-content'), chatModal.find('[data-since]')); - Chats.addRenameHandler(chatModal.attr('roomId'), chatModal.find('[component="chat/room/name"]')); + chatModal.find('[component="chat/controls"]').toggle(); + messagesEl.css('height', module.calculateChatListHeight(chatModal)); + }); - Chats.addSendHandlers(chatModal.attr('roomId'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); + Chats.addRenameHandler(chatModal.attr('roomId'), chatModal.find('[component="chat/room/name"]')); - Chats.createTagsInput(chatModal.find('.users-tag-input'), data); - Chats.createAutoComplete(chatModal.find('[component="chat/input"]')); + Chats.addSendHandlers(chatModal.attr('roomId'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); - Chats.loadChatSince(chatModal.attr('roomId'), chatModal.find('.chat-content'), 'recent'); + Chats.createTagsInput(chatModal.find('.users-tag-input'), data); + Chats.createAutoComplete(chatModal.find('[component="chat/input"]')); - checkStatus(chatModal); + Chats.addScrollHandler(chatModal.attr('roomId'), data.uid, chatModal.find('.chat-content')); - taskbar.push('chat', chatModal.attr('UUID'), { - title: data.users.length ? data.users[0].username : '', - roomId: data.roomId, - icon: 'fa-comment', - state: '' - }); + taskbar.push('chat', chatModal.attr('UUID'), { + title: data.users.length ? data.users[0].username : '', + roomId: data.roomId, + icon: 'fa-comment', + state: '' + }); - $(window).trigger('action:chat.loaded', chatModal); + $(window).trigger('action:chat.loaded', chatModal); - if (typeof callback === 'function') { - callback(chatModal); - } - }); + if (typeof callback === 'function') { + callback(chatModal); + } }); }; - module.focusInput = function(chatModal) { + module.focusInput = function (chatModal) { chatModal.find('#chat-message-input').focus(); }; - module.close = function(chatModal, silent) { + module.close = function (chatModal) { clearInterval(chatModal.attr('intervalId')); chatModal.attr('intervalId', 0); chatModal.remove(); @@ -318,7 +284,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra } }; - module.center = function(chatModal) { + module.center = function (chatModal) { var hideAfter = false; if (chatModal.hasClass('hide')) { chatModal.removeClass('hide'); @@ -333,12 +299,11 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra return chatModal; }; - module.load = function(uuid) { + module.load = function (uuid) { var chatModal = $('div[UUID="' + uuid + '"]'); chatModal.removeClass('hide'); - checkStatus(chatModal); taskbar.updateActive(uuid); - Chats.scrollToBottom(chatModal.find('.chat-content')); + ChatsMessages.scrollToBottom(chatModal.find('.chat-content')); module.bringModalToTop(chatModal); module.focusInput(chatModal); socket.emit('modules.chats.markRead', chatModal.attr('roomId')); @@ -349,32 +314,31 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra } }; - module.enableMobileBehaviour = function(modalEl) { + module.enableMobileBehaviour = function (modalEl) { app.toggleNavbar(false); modalEl.attr('data-mobile', '1'); var messagesEl = modalEl.find('.chat-content'); messagesEl.css('height', module.calculateChatListHeight(modalEl)); - $(window).on('resize', function() { + $(window).on('resize', function () { messagesEl.css('height', module.calculateChatListHeight(modalEl)); }); }; - module.disableMobileBehaviour = function(modalEl) { + module.disableMobileBehaviour = function () { app.toggleNavbar(true); }; - module.calculateChatListHeight = function(modalEl) { - var totalHeight = modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight(), - padding = parseInt(modalEl.find('.modal-body').css('padding-top'), 10) + parseInt(modalEl.find('.modal-body').css('padding-bottom'), 10), - contentMargin = parseInt(modalEl.find('.chat-content').css('margin-top'), 10) + parseInt(modalEl.find('.chat-content').css('margin-bottom'), 10), - sinceHeight = modalEl.find('.since-bar').outerHeight(true), - inputGroupHeight = modalEl.find('.input-group').outerHeight(); + module.calculateChatListHeight = function (modalEl) { + var totalHeight = modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight(); + var padding = parseInt(modalEl.find('.modal-body').css('padding-top'), 10) + parseInt(modalEl.find('.modal-body').css('padding-bottom'), 10); + var contentMargin = parseInt(modalEl.find('.chat-content').css('margin-top'), 10) + parseInt(modalEl.find('.chat-content').css('margin-bottom'), 10); + var inputGroupHeight = modalEl.find('.input-group').outerHeight(); return totalHeight - padding - contentMargin - inputGroupHeight; }; - module.minimize = function(uuid) { + module.minimize = function (uuid) { var chatModal = $('div[UUID="' + uuid + '"]'); chatModal.addClass('hide'); taskbar.minimize('chat', uuid); diff --git a/public/src/modules/components.js b/public/src/modules/components.js index f4de069e01..ef936d0f75 100644 --- a/public/src/modules/components.js +++ b/public/src/modules/components.js @@ -1,51 +1,61 @@ "use strict"; -define('components', function() { +define('components', function () { var components = {}; components.core = { - 'post': function(name, value) { + 'topic/teaser': function (tid) { + if (tid) { + return $('[component="category/topic"][data-tid="' + tid + '"] [component="topic/teaser"]'); + } else { + return $('[component="topic/teaser"]'); + } + }, + 'topic': function (name, value) { + return $('[component="topic"][data-' + name + '="' + value + '"]'); + }, + 'post': function (name, value) { return $('[component="post"][data-' + name + '="' + value + '"]'); }, - 'post/content': function(pid) { + 'post/content': function (pid) { return components.core.post('pid', pid).find('[component="post/content"]'); }, - 'post/header': function(pid) { + 'post/header': function (pid) { return components.core.post('pid', pid).find('[component="post/header"]'); }, - 'post/anchor': function(index) { + 'post/anchor': function (index) { return components.core.post('index', index).find('[component="post/anchor"]'); }, - 'post/vote-count': function(pid) { + 'post/vote-count': function (pid) { return components.core.post('pid', pid).find('[component="post/vote-count"]'); }, - 'post/favourite-count': function(pid) { - return components.core.post('pid', pid).find('[component="post/favourite-count"]'); + 'post/bookmark-count': function (pid) { + return components.core.post('pid', pid).find('[component="post/bookmark-count"]'); }, - 'user/postcount': function(uid) { + 'user/postcount': function (uid) { return $('[component="user/postcount"][data-uid="' + uid + '"]'); }, - 'user/reputation': function(uid) { + 'user/reputation': function (uid) { return $('[component="user/reputation"][data-uid="' + uid + '"]'); }, - 'category/topic': function(name, value) { + 'category/topic': function (name, value) { return $('[component="category/topic"][data-' + name + '="' + value + '"]'); }, - 'categories/category': function(name, value) { + 'categories/category': function (name, value) { return $('[component="categories/category"][data-' + name + '="' + value + '"]'); }, - 'chat/message': function(messageId) { + 'chat/message': function (messageId) { return $('[component="chat/message"][data-mid="' + messageId + '"]'); }, - 'chat/message/body': function(messageId) { + 'chat/message/body': function (messageId) { return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]'); } }; - components.get = function() { + components.get = function () { var args = Array.prototype.slice.call(arguments, 1); if (components.core[arguments[0]] && args.length) { @@ -56,4 +66,4 @@ define('components', function() { }; return components; -}); \ No newline at end of file +}); diff --git a/public/src/modules/coverPhoto.js b/public/src/modules/coverPhoto.js index 6a45732c9b..fa6bdc4d7d 100644 --- a/public/src/modules/coverPhoto.js +++ b/public/src/modules/coverPhoto.js @@ -3,19 +3,19 @@ define('coverPhoto', [ 'vendor/jquery/draggable-background/backgroundDraggable' -], function() { +], function () { var coverPhoto = { coverEl: null, saveFn: null }; - coverPhoto.init = function(coverEl, saveFn, uploadFn, removeFn) { + coverPhoto.init = function (coverEl, saveFn, uploadFn, removeFn) { coverPhoto.coverEl = coverEl; coverPhoto.saveFn = saveFn; coverEl.find('.upload').on('click', uploadFn); - coverEl.find('.resize').on('click', function() { + coverEl.find('.resize').on('click', function () { enableDragging(coverEl); }); coverEl.find('.remove').on('click', removeFn); @@ -28,13 +28,13 @@ define('coverPhoto', [ coverEl.addClass('initialised'); }; - coverPhoto.onDragOver = function(e) { + coverPhoto.onDragOver = function (e) { e.stopPropagation(); e.preventDefault(); e.originalEvent.dataTransfer.dropEffect = 'copy'; }; - coverPhoto.onDrop = function(e) { + coverPhoto.onDrop = function (e) { e.stopPropagation(); e.preventDefault(); @@ -42,7 +42,7 @@ define('coverPhoto', [ reader = new FileReader(); if (files.length && files[0].type.match('image.*')) { - reader.onload = function(e) { + reader.onload = function (e) { coverPhoto.coverEl.css('background-image', 'url(' + e.target.result + ')'); coverPhoto.newCover = e.target.result; }; @@ -67,10 +67,10 @@ define('coverPhoto', [ }); } - coverPhoto.save = function() { + coverPhoto.save = function () { coverPhoto.coverEl.addClass('saving'); - coverPhoto.saveFn(coverPhoto.newCover || undefined, coverPhoto.coverEl.css('background-position'), function(err) { + coverPhoto.saveFn(coverPhoto.newCover || undefined, coverPhoto.coverEl.css('background-position'), function (err) { if (!err) { coverPhoto.coverEl.toggleClass('active', 0); coverPhoto.coverEl.backgroundDraggable('disable'); diff --git a/public/src/modules/csrf.js b/public/src/modules/csrf.js deleted file mode 100644 index 10a9b002f0..0000000000 --- a/public/src/modules/csrf.js +++ /dev/null @@ -1,17 +0,0 @@ -"use strict"; -/*globals define*/ - -define('csrf', function() { - var csrf = {}, - _data = {}; - - csrf.get = function() { - return _data.token; - }; - - csrf.set = function(token) { - _data.token = token; - }; - - return csrf; -}); \ No newline at end of file diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index fda87711fb..5de0cdbb3b 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -1,4 +1,4 @@ -;(function(exports) { +;(function (exports) { "use strict"; /* globals define, utils, config */ @@ -9,7 +9,7 @@ var helpers = {}; - helpers.displayMenuItem = function(data, index) { + helpers.displayMenuItem = function (data, index) { var item = data.navigation[index]; if (!item) { return false; @@ -36,7 +36,7 @@ return true; }; - helpers.buildMetaTag = function(tag) { + helpers.buildMetaTag = function (tag) { var name = tag.name ? 'name="' + tag.name + '" ' : '', property = tag.property ? 'property="' + tag.property + '" ' : '', content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : ''; @@ -44,7 +44,7 @@ return '<meta ' + name + property + content + '/>\n\t'; }; - helpers.buildLinkTag = function(tag) { + helpers.buildLinkTag = function (tag) { var link = tag.link ? 'link="' + tag.link + '" ' : '', rel = tag.rel ? 'rel="' + tag.rel + '" ' : '', type = tag.type ? 'type="' + tag.type + '" ' : '', @@ -54,12 +54,12 @@ return '<link ' + link + rel + type + sizes + href + '/>\n\t'; }; - helpers.stringify = function(obj) { + helpers.stringify = function (obj) { // Turns the incoming object into a JSON string return JSON.stringify(obj).replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">").replace(/"/g, '"'); }; - helpers.escape = function(str) { + helpers.escape = function (str) { if (typeof utils !== 'undefined') { return utils.escapeHTML(str); } else { @@ -67,7 +67,7 @@ } }; - helpers.stripTags = function(str) { + helpers.stripTags = function (str) { if (typeof S !== 'undefined') { return S(str).stripTags().s; } else { @@ -76,7 +76,7 @@ } }; - helpers.generateCategoryBackground = function(category) { + helpers.generateCategoryBackground = function (category) { if (!category) { return ''; } @@ -100,11 +100,13 @@ return style.join('; ') + ';'; }; - helpers.generateChildrenCategories = function(category) { + helpers.generateChildrenCategories = function (category) { var html = ''; var relative_path = (typeof config !== 'undefined' ? config.relative_path : require('nconf').get('relative_path')); - - category.children.forEach(function(child) { + if (!category || !category.children) { + return html; + } + category.children.forEach(function (child) { if (!child) { return; } @@ -119,7 +121,7 @@ return html; }; - helpers.generateTopicClass = function(topic) { + helpers.generateTopicClass = function (topic) { var style = []; if (topic.locked) { @@ -141,16 +143,16 @@ return style.join(' '); }; - helpers.getBookmarkFromIndex = function(topic) { + helpers.getBookmarkFromIndex = function (topic) { return (topic.index || 0) + 1; }; - helpers.displayUserSearch = function(data, allowGuestUserSearching) { + helpers.displayUserSearch = function (data, allowGuestUserSearching) { return data.loggedIn || allowGuestUserSearching === 'true'; }; // Groups helpers - helpers.membershipBtn = function(groupObj) { + helpers.membershipBtn = function (groupObj) { if (groupObj.name === 'administrators') { return ''; } @@ -170,7 +172,7 @@ } }; - helpers.spawnPrivilegeStates = function(member, privileges) { + helpers.spawnPrivilegeStates = function (member, privileges) { var states = []; for(var priv in privileges) { if (privileges.hasOwnProperty(priv)) { @@ -180,16 +182,16 @@ }); } } - return states.map(function(priv) { + return states.map(function (priv) { return '<td class="text-center" data-privilege="' + priv.name + '"><input type="checkbox"' + (priv.state ? ' checked' : '') + (member === 'guests' && priv.name === 'groups:moderate' ? ' disabled="disabled"' : '') + ' /></td>'; }).join(''); }; - helpers.localeToHTML = function(locale) { + helpers.localeToHTML = function (locale) { return locale.replace('_', '-'); }; - helpers.renderTopicImage = function(topicObj) { + helpers.renderTopicImage = function (topicObj) { if (topicObj.thumb) { return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />'; } else { @@ -197,7 +199,7 @@ } }; - helpers.renderDigestAvatar = function(block) { + helpers.renderDigestAvatar = function (block) { if (block.teaser) { if (block.teaser.user.picture) { return '<img style="vertical-align: middle; width: 16px; height: 16px; padding-right: 1em;" src="' + block.teaser.user.picture + '" title="' + block.teaser.user.username + '" />'; @@ -213,7 +215,7 @@ } }; - helpers.userAgentIcons = function(data) { + helpers.userAgentIcons = function (data) { var icons = ''; switch(data.platform) { @@ -265,7 +267,7 @@ return icons; }; - exports.register = function() { + exports.register = function () { var templates; if (typeof module === 'object') { @@ -274,7 +276,7 @@ templates = window.templates; } - Object.keys(helpers).forEach(function(helperName) { + Object.keys(helpers).forEach(function (helperName) { templates.registerHelper(helperName, helpers[helperName]); }); }; @@ -284,8 +286,8 @@ define('helpers', exports); } -})( +}( typeof exports === 'object' ? exports : typeof define === 'function' && define.amd ? {} : helpers = {} -); +)); diff --git a/public/src/modules/iconSelect.js b/public/src/modules/iconSelect.js index 53a5780b5a..34a7d595ce 100644 --- a/public/src/modules/iconSelect.js +++ b/public/src/modules/iconSelect.js @@ -2,11 +2,11 @@ /* globals define, bootbox, templates */ -define('iconSelect', function() { +define('iconSelect', function () { var iconSelect = {}; - iconSelect.init = function(el, onModified) { - onModified = onModified || function() {}; + iconSelect.init = function (el, onModified) { + onModified = onModified || function () {}; var doubleSize = el.hasClass('fa-2x'), selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, ''); @@ -16,17 +16,21 @@ define('iconSelect', function() { $('#icons .fa-icons .fa.' + selected).addClass('selected'); } - templates.parse('partials/fontawesome', {}, function(html) { + templates.parse('partials/fontawesome', {}, function (html) { + html = $(html); + html.find('.fa-icons').prepend($('<i class="fa fa-nbb-none"></i>')); + var picker = bootbox.dialog({ onEscape: true, backdrop: true, + show: false, message: html, title: 'Select an Icon', buttons: { noIcon: { label: 'No Icon', className: 'btn-default', - callback: function() { + callback: function () { el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '')); el.val(''); el.attr('value', ''); @@ -37,13 +41,15 @@ define('iconSelect', function() { success: { label: 'Select', className: 'btn-primary', - callback: function(confirm) { + callback: function (confirm) { var iconClass = $('.bootbox .selected').attr('class'); var categoryIconClass = $('<div/>').addClass(iconClass).removeClass('fa').removeClass('selected').attr('class'); - el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass); - el.val(categoryIconClass); - el.attr('value', categoryIconClass); + if (categoryIconClass) { + el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass); + el.val(categoryIconClass); + el.attr('value', categoryIconClass); + } onModified(el); } @@ -51,33 +57,54 @@ define('iconSelect', function() { } }); - picker.on('shown.bs.modal', function() { + picker.on('show.bs.modal', function () { + var modalEl = $(this), + searchEl = modalEl.find('input'); + + if (selected) { + modalEl.find('.' + selected).addClass('selected'); + searchEl.val(selected.replace('fa-', '')); + } + }).modal('show'); + + picker.on('shown.bs.modal', function () { var modalEl = $(this), searchEl = modalEl.find('input'), icons = modalEl.find('.fa-icons i'), submitEl = modalEl.find('button.btn-primary'); + function changeSelection(newSelection) { + modalEl.find('i.selected').removeClass('selected'); + if (newSelection) { + newSelection.addClass('selected'); + } else if (searchEl.val().length === 0) { + if (selected) { + modalEl.find('.' + selected).addClass('selected'); + } + } else { + modalEl.find('i:visible').first().addClass('selected'); + } + } + // Focus on the input box - searchEl.focus(); + searchEl.selectRange(0, searchEl.val().length); - modalEl.find('.icon-container').on('click', 'i', function() { - searchEl.val($(this).attr('class').replace('fa fa-', '')); - modalEl.find('.icon-container i').removeClass('selected'); - $(this).addClass('selected'); + modalEl.find('.icon-container').on('click', 'i', function () { + searchEl.val($(this).attr('class').replace('fa fa-', '').replace('selected', '')); + changeSelection($(this)); }); - searchEl.on('keyup', function(e) { + searchEl.on('keyup', function (e) { if (e.keyCode !== 13) { // Filter icons.show(); - icons.each(function(idx, el) { + icons.each(function (idx, el) { if (!el.className.match(new RegExp('^fa fa-.*' + searchEl.val() + '.*$'))) { $(el).hide(); } }); + changeSelection(); } else { - // Pick first match - $('.icon-container i:visible').first().addClass('selected'); submitEl.click(); } }); diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index 3d65f6b8a0..9700db947c 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -4,28 +4,30 @@ /* globals define, ajaxify, utils, config */ -define('navigator', ['forum/pagination', 'components'], function(pagination, components) { +define('navigator', ['forum/pagination', 'components'], function (pagination, components) { var navigator = {}; var index = 1; var count = 0; + var navigatorUpdateTimeoutId = 0; + navigator.scrollActive = false; - navigator.init = function(selector, count, toTop, toBottom, callback, calculateIndex) { + navigator.init = function (selector, count, toTop, toBottom, callback, calculateIndex) { index = 1; navigator.selector = selector; navigator.callback = callback; - toTop = toTop || function() {}; - toBottom = toBottom || function() {}; + toTop = toTop || function () {}; + toBottom = toBottom || function () {}; - $(window).off('scroll', navigator.update).on('scroll', navigator.update); + $(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate); - $('.pagination-block .dropdown-menu').off('click').on('click', function(e) { + $('.pagination-block .dropdown-menu').off('click').on('click', function (e) { e.stopPropagation(); }); - $('.pagination-block').off('shown.bs.dropdown', '.dropdown').on('shown.bs.dropdown', '.dropdown', function() { - setTimeout(function() { + $('.pagination-block').off('shown.bs.dropdown', '.dropdown').on('shown.bs.dropdown', '.dropdown', function () { + setTimeout(function () { $('.pagination-block input').focus(); }, 100); }); @@ -35,7 +37,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com $('.pagination-block .pagetop').off('click').on('click', toTop); $('.pagination-block .pagebottom').off('click').on('click', toBottom); - $('.pagination-block input').on('keydown', function(e) { + $('.pagination-block input').on('keydown', function (e) { if (e.which === 13) { var input = $(this); if (!utils.isNumber(input.val())) { @@ -64,16 +66,16 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com return parts[1] + '/' + parts[2] + '/' + parts[3] + (index ? '/' + index : ''); } - navigator.setCount = function(value) { + navigator.setCount = function (value) { count = parseInt(value, 10); navigator.updateTextAndProgressBar(); }; - navigator.show = function() { + navigator.show = function () { toggle(true); }; - navigator.disable = function() { + navigator.disable = function () { count = 0; index = 1; navigator.selector = navigator.callback = null; @@ -91,7 +93,15 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com $('.pagination-block').toggleClass('ready', flag); } - navigator.update = function(threshold) { + navigator.delayedUpdate = function () { + if (navigatorUpdateTimeoutId) { + clearTimeout(navigatorUpdateTimeoutId); + navigatorUpdateTimeoutId = 0; + } + navigatorUpdateTimeoutId = setTimeout(navigator.update, 100); + }; + + navigator.update = function (threshold) { /* The "threshold" is defined as the distance from the top of the page to a spot where a user is expecting to begin reading. @@ -108,7 +118,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com var documentHeight = $(document).height(); var middleOfViewport = scrollTop + windowHeight / 2; var previousDistance = Number.MAX_VALUE; - els.each(function() { + els.each(function () { var distanceToMiddle = Math.abs(middleOfViewport - $(this).offset().top); if (distanceToMiddle > previousDistance) { @@ -121,8 +131,8 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com } }); - var atTop = scrollTop === 0 && parseInt(els.first().attr('data-index'), 10) === 0, - nearBottom = scrollTop + windowHeight > documentHeight - 100 && parseInt(els.last().attr('data-index'), 10) === count - 1; + var atTop = scrollTop === 0 && parseInt(els.first().attr('data-index'), 10) === 0; + var nearBottom = scrollTop + windowHeight > documentHeight - 100 && parseInt(els.last().attr('data-index'), 10) === count - 1; if (atTop) { index = 1; @@ -131,13 +141,15 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com } // If a threshold is undefined, try to determine one based on new index - if (threshold === undefined) { + if (threshold === undefined && ajaxify.data.template.topic === true) { if (atTop) { threshold = 0; } else { var anchorEl = components.get('post/anchor', index - 1); - var anchorRect = anchorEl.get(0).getBoundingClientRect(); - threshold = anchorRect.top; + if (anchorEl.length) { + var anchorRect = anchorEl.get(0).getBoundingClientRect(); + threshold = anchorRect.top; + } } } @@ -149,7 +161,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com toggle(!!count); }; - navigator.updateTextAndProgressBar = function() { + navigator.updateTextAndProgressBar = function () { index = index > count ? count : index; $('.pagination-block .pagination-text').translateHtml('[[global:pagination.out_of, ' + index + ', ' + count + ']]'); @@ -168,19 +180,19 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com }); }; - navigator.scrollTop = function(index) { - if ($('li[data-index="' + index + '"]').length) { + navigator.scrollTop = function (index) { + if ($(navigator.selector + '[data-index="' + index + '"]').length) { navigator.scrollToPost(index, true); } else { ajaxify.go(generateUrl()); } }; - navigator.scrollBottom = function(index) { + navigator.scrollBottom = function (index) { if (parseInt(index, 10) < 0) { return; } - if ($('li[data-index="' + index + '"]').length) { + if ($(navigator.selector + '[data-index="' + index + '"]').length) { navigator.scrollToPost(index, true); } else { index = parseInt(index, 10) + 1; @@ -188,7 +200,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com } }; - navigator.scrollToPost = function(postIndex, highlight, duration) { + navigator.scrollToPost = function (postIndex, highlight, duration) { if (!utils.isNumber(postIndex) || !components.get('topic').length) { return; } @@ -201,10 +213,14 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com } if (config.usePagination) { - var page = Math.max(1, Math.ceil(postIndex / config.postsPerPage)); + var index = postIndex; + if (config.topicPostSort === 'most_votes' || config.topicPostSort === 'newest_to_oldest') { + index = ajaxify.data.postcount - index; + } + var page = Math.max(1, Math.ceil(index / config.postsPerPage)); if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { - pagination.loadPage(page, function() { + pagination.loadPage(page, function () { navigator.scrollToPostIndex(postIndex, highlight, duration); }); } else { @@ -217,13 +233,11 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com } }; - navigator.scrollToPostIndex = function(postIndex, highlight, duration) { - var scrollTo = components.get('post/anchor', postIndex), - postEl = components.get('post', 'index', postIndex), - postHeight = postEl.height(), - viewportHeight = $(window).height(), - navbarHeight = components.get('navbar').height(); - + navigator.scrollToPostIndex = function (postIndex, highlight, duration) { + var scrollTo = components.get('post', 'index', postIndex); + var postHeight = scrollTo.height(); + var viewportHeight = $(window).height(); + var navbarHeight = components.get('navbar').height(); if (!scrollTo.length) { navigator.scrollActive = false; @@ -247,7 +261,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com $('html, body').animate({ scrollTop: scrollTop + 'px' - }, duration, function() { + }, duration, function () { if (done) { // Re-enable onScroll behaviour $(window).on('scroll', navigator.update); @@ -266,9 +280,9 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com function highlightPost() { if (highlight) { - scrollTo.parents('[component="post"]').addClass('highlight'); - setTimeout(function() { - scrollTo.parents('[component="post"]').removeClass('highlight'); + scrollTo.addClass('highlight'); + setTimeout(function () { + scrollTo.removeClass('highlight'); }, 10000); } } diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 35f1e677dd..a5b32c1037 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -2,19 +2,19 @@ /* globals define, socket, app, ajaxify, templates, Tinycon*/ -define('notifications', ['sounds', 'translator', 'components'], function(sound, translator, components) { +define('notifications', ['sounds', 'translator', 'components'], function (sound, translator, components) { var Notifications = {}; var unreadNotifs = {}; - Notifications.prepareDOM = function() { + Notifications.prepareDOM = function () { var notifContainer = components.get('notifications'), notifTrigger = notifContainer.children('a'), notifList = components.get('notifications/list'), notifIcon = components.get('notifications/icon'); notifTrigger - .on('click', function(e) { + .on('click', function (e) { e.preventDefault(); if (notifContainer.hasClass('open')) { return; @@ -22,20 +22,20 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, Notifications.loadNotifications(notifList); }) - .on('dblclick', function(e) { + .on('dblclick', function (e) { e.preventDefault(); if (parseInt(notifIcon.attr('data-content'), 10) > 0) { Notifications.markAllRead(); } }); - notifList.on('click', '[data-nid]', function() { + notifList.on('click', '[data-nid]', function () { var unread = $(this).hasClass('unread'); var nid = $(this).attr('data-nid'); if (!unread) { return; } - socket.emit('notifications.markRead', nid, function(err) { + socket.emit('notifications.markRead', nid, function (err) { if (err) { return app.alertError(err.message); } @@ -48,12 +48,12 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, notifContainer.on('click', '.mark-all-read', Notifications.markAllRead); - notifList.on('click', '.mark-read', function() { + notifList.on('click', '.mark-read', function () { var liEl = $(this).parent(); var unread = liEl.hasClass('unread'); var nid = liEl.attr('data-nid'); - socket.emit('notifications.mark' + (unread ? 'Read' : 'Unread'), nid, function(err) { + socket.emit('notifications.mark' + (unread ? 'Read' : 'Unread'), nid, function (err) { if (err) { return app.alertError(err.message); } @@ -72,7 +72,7 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, Notifications.updateNotifCount(count); } - socket.on('event:new_notification', function(notifData) { + socket.on('event:new_notification', function (notifData) { // If a path is defined, show notif data, otherwise show generic data var payload = { alert_id: 'new_notif', @@ -83,15 +83,12 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, if (notifData.path) { payload.message = notifData.bodyShort; payload.type = 'info'; - payload.clickfn = function() { - socket.emit('notifications.generatePath', notifData.nid, function(err, path) { - if (err) { - return app.alertError(err.message); - } - if (path) { - ajaxify.go(path); - } - }); + payload.clickfn = function () { + if (notifData.path.startsWith('http') && notifData.path.startsWith('https')) { + window.location.href = notifData.path; + } else { + window.location.href = window.location.protocol + '//' + window.location.host + config.relative_path + notifData.path; + } }; } else { payload.message = '[[notifications:you_have_unread_notifications]]'; @@ -104,43 +101,49 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, if (ajaxify.currentPage === 'notifications') { ajaxify.refresh(); } - - if (!unreadNotifs[notifData.nid]) { - incrementNotifCount(1); + socket.emit('notifications.getCount', function (err, count) { + if (err) { + return app.alertError(err.message); + } + + Notifications.updateNotifCount(count); + }); + + if (!unreadNotifs[notifData.nid]) { sound.play('notification'); - unreadNotifs[notifData.nid] = true; - } + unreadNotifs[notifData.nid] = true; + } }); - socket.on('event:notifications.updateCount', function(count) { + socket.on('event:notifications.updateCount', function (count) { Notifications.updateNotifCount(count); }); }; - Notifications.loadNotifications = function(notifList) { - socket.emit('notifications.get', null, function(err, data) { + Notifications.loadNotifications = function (notifList) { + socket.emit('notifications.get', null, function (err, data) { if (err) { return app.alertError(err.message); } - var notifs = data.unread.concat(data.read).sort(function(a, b) { + var notifs = data.unread.concat(data.read).sort(function (a, b) { return parseInt(a.datetime, 10) > parseInt(b.datetime, 10) ? -1 : 1; }); translator.toggleTimeagoShorthand(); - for(var i=0; i<notifs.length; ++i) { + for(var i = 0; i < notifs.length; ++i) { notifs[i].timeago = $.timeago(new Date(parseInt(notifs[i].datetime, 10))); } translator.toggleTimeagoShorthand(); - templates.parse('partials/notifications_list', {notifications: notifs}, function(html) { + templates.parse('partials/notifications_list', {notifications: notifs}, function (html) { notifList.translateHtml(html); }); }); }; - Notifications.updateNotifCount = function(count) { + Notifications.updateNotifCount = function (count) { var notifIcon = components.get('notifications/icon'); count = Math.max(0, count); if (count > 0) { @@ -163,8 +166,8 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, } }; - Notifications.markAllRead = function() { - socket.emit('notifications.markAllRead', function(err) { + Notifications.markAllRead = function () { + socket.emit('notifications.markAllRead', function (err) { if (err) { app.alertError(err.message); } diff --git a/public/src/modules/postSelect.js b/public/src/modules/postSelect.js index 9812bf284e..6001488708 100644 --- a/public/src/modules/postSelect.js +++ b/public/src/modules/postSelect.js @@ -2,14 +2,14 @@ /* globals define*/ -define('postSelect', ['components'], function(components) { +define('postSelect', ['components'], function (components) { var PostSelect = {}; PostSelect.pids = []; - PostSelect.init = function(onSelect) { + PostSelect.init = function (onSelect) { PostSelect.pids.length = 0; - components.get('topic').on('click', '[data-pid]', function() { + components.get('topic').on('click', '[data-pid]', function () { togglePostSelection($(this), onSelect); }); disableClicksOnPosts(); @@ -27,14 +27,14 @@ define('postSelect', ['components'], function(components) { var index = PostSelect.pids.indexOf(newPid); if(index === -1) { PostSelect.pids.push(newPid); - post.css('opacity', '0.5'); + post.toggleClass('bg-success', true); } else { PostSelect.pids.splice(index, 1); - post.css('opacity', '1.0'); + post.toggleClass('bg-success', false); } if (PostSelect.pids.length) { - PostSelect.pids.sort(function(a,b) { return a - b; }); + PostSelect.pids.sort(function (a,b) { return a - b; }); } callback(); } @@ -49,7 +49,7 @@ define('postSelect', ['components'], function(components) { components.get('post').on('click', 'button,a', disableClicks); } - PostSelect.enableClicksOnPosts = function() { + PostSelect.enableClicksOnPosts = function () { components.get('post').off('click', 'button,a', disableClicks); }; diff --git a/public/src/modules/search.js b/public/src/modules/search.js index 7b889999ea..4319de5617 100644 --- a/public/src/modules/search.js +++ b/public/src/modules/search.js @@ -1,13 +1,13 @@ "use strict"; /* globals socket, ajaxify, app, define, config */ -define('search', ['navigator', 'translator'], function(nav, translator) { +define('search', ['navigator', 'translator'], function (nav, translator) { var Search = { - current: {} - }; + current: {} + }; - Search.query = function(data, callback) { + Search.query = function (data, callback) { var term = data.term; // Detect if a tid was specified @@ -22,11 +22,11 @@ define('search', ['navigator', 'translator'], function(nav, translator) { return app.alertError('[[error:invalid-search-term]]'); } - ajaxify.go('search/' + term + '?' + createQueryString(data)); + ajaxify.go('search?' + createQueryString(data)); callback(); } else { - var cleanedTerm = term.replace(topicSearch[0], ''), - tid = topicSearch[1]; + var cleanedTerm = term.replace(topicSearch[0], ''); + var tid = topicSearch[1]; if (cleanedTerm.length > 0) { Search.queryTopic(tid, cleanedTerm, callback); @@ -38,8 +38,9 @@ define('search', ['navigator', 'translator'], function(nav, translator) { var searchIn = data['in'] || 'titlesposts'; var postedBy = data.by || ''; var query = { - 'in': searchIn - }; + term: data.term, + 'in': searchIn + }; if (postedBy && (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts')) { query.by = postedBy; @@ -73,11 +74,19 @@ define('search', ['navigator', 'translator'], function(nav, translator) { return decodeURIComponent($.param(query)); } - Search.queryTopic = function(tid, term, callback) { + Search.getSearchPreferences = function () { + try { + return JSON.parse(localStorage.getItem('search-preferences') || '{}'); + } catch(e) { + return {}; + } + }; + + Search.queryTopic = function (tid, term, callback) { socket.emit('topics.search', { tid: tid, term: term - }, function(err, pids) { + }, function (err, pids) { if (err) { return app.alertError(err.message); } @@ -85,21 +94,21 @@ define('search', ['navigator', 'translator'], function(nav, translator) { if (Array.isArray(pids)) { // Sort pids numerically & store Search.current = { - results: pids.sort(function(a, b) { - return a-b; + results: pids.sort(function (a, b) { + return a - b; }), tid: tid, term: term }; - Search.checkPagePresence(tid, function() { + Search.checkPagePresence(tid, function () { Search.topicDOM.update(0); }); } }); }; - Search.checkPagePresence = function(tid, callback) { + Search.checkPagePresence = function (tid, callback) { if (parseInt(ajaxify.data.tid, 10) !== parseInt(tid, 10)) { ajaxify.go('topic/' + tid, callback); } else { @@ -111,60 +120,64 @@ define('search', ['navigator', 'translator'], function(nav, translator) { active: false }; - Search.topicDOM.prev = function() { - Search.topicDOM.update((Search.current.index === 0) ? Search.current.results.length-1 : Search.current.index-1); + Search.topicDOM.prev = function () { + Search.topicDOM.update((Search.current.index === 0) ? Search.current.results.length - 1 : Search.current.index - 1); }; - Search.topicDOM.next = function() { - Search.topicDOM.update((Search.current.index === Search.current.results.length-1) ? 0 : Search.current.index+1); + Search.topicDOM.next = function () { + Search.topicDOM.update((Search.current.index === Search.current.results.length - 1) ? 0 : Search.current.index + 1); }; - Search.topicDOM.update = function(index) { + Search.topicDOM.update = function (index) { var topicSearchEl = $('.topic-search'); Search.current.index = index; Search.topicDOM.start(); if (Search.current.results.length > 0) { - topicSearchEl.find('.count').html((index+1) + ' / ' + Search.current.results.length); + topicSearchEl.find('.count').html((index + 1) + ' / ' + Search.current.results.length); topicSearchEl.find('.prev, .next').removeAttr('disabled'); var data = { pid: Search.current.results[index], tid: Search.current.tid, topicPostSort: config.topicPostSort }; - socket.emit('posts.getPidIndex', data, function(err, postIndex) { + socket.emit('posts.getPidIndex', data, function (err, postIndex) { + if (err) { + return app.alertError(err.message); + } + nav.scrollToPost(postIndex, true); }); } else { - translator.translate('[[search:no-matches]]', function(text) { + translator.translate('[[search:no-matches]]', function (text) { topicSearchEl.find('.count').html(text); }); topicSearchEl.removeClass('hidden').find('.prev, .next').attr('disabled', 'disabled'); } }; - Search.topicDOM.start = function() { + Search.topicDOM.start = function () { $('.topic-search').removeClass('hidden'); if (!Search.topicDOM.active) { Search.topicDOM.active = true; // Bind to esc - require(['mousetrap'], function(Mousetrap) { - Mousetrap.bind('esc', Search.topicDOM.end); + require(['mousetrap'], function (mousetrap) { + mousetrap.bind('esc', Search.topicDOM.end); }); } }; - Search.topicDOM.end = function() { + Search.topicDOM.end = function () { $('.topic-search').addClass('hidden').find('.prev, .next').attr('disabled', 'disabled'); Search.topicDOM.active = false; // Unbind esc - require(['mousetrap'], function(Mousetrap) { - Mousetrap.unbind('esc', Search.topicDOM.end); + require(['mousetrap'], function (mousetrap) { + mousetrap.unbind('esc', Search.topicDOM.end); }); }; return Search; -}); \ No newline at end of file +}); diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 652398f539..090c83e2d0 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -448,7 +448,7 @@ define('settings', function () { helper.persistSettings(hash, Settings.cfg, notify, callback); }, load: function (hash, formEl, callback) { - callback = callback || function() {}; + callback = callback || function () {}; socket.emit('admin.settings.get', { hash: hash }, function (err, values) { @@ -468,11 +468,17 @@ define('settings', function () { } $(formEl).deserialize(values); - $(formEl).find('input[type="checkbox"]').each(function() { + $(formEl).find('input[type="checkbox"]').each(function () { $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked')); }); $(window).trigger('action:admin.settingsLoaded'); + // Handle unsaved changes + $(formEl).on('change', 'input, select, textarea', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + callback(null, values); }); }, @@ -489,7 +495,7 @@ define('settings', function () { }); // Normalizing value of multiple selects - formEl.find('select[multiple]').each(function(idx, selectEl) { + formEl.find('select[multiple]').each(function (idx, selectEl) { selectEl = $(selectEl); values[selectEl.attr('name')] = JSON.stringify(selectEl.val()); }); @@ -498,14 +504,25 @@ define('settings', function () { hash: hash, values: values }, function (err) { + // Remove unsaved flag to re-enable ajaxify + app.flags._unsaved = false; + if (typeof callback === 'function') { - callback(); + callback(err); } else { - app.alert({ - title: 'Settings Saved', - type: 'success', - timeout: 2500 - }); + if (err) { + app.alert({ + title: 'Error while saving settings', + type: 'error', + timeout: 2500 + }); + } else { + app.alert({ + title: 'Settings Saved', + type: 'success', + timeout: 2500 + }); + } } }); } diff --git a/public/src/modules/settings/array.js b/public/src/modules/settings/array.js index 81071a65fa..fd94ed8226 100644 --- a/public/src/modules/settings/array.js +++ b/public/src/modules/settings/array.js @@ -107,7 +107,7 @@ define('settings/array', function () { } catch (_error) { return $(document.createTextNode(separator)); } - })(); + }()); if (typeof attributes !== 'object') { attributes = {}; } diff --git a/public/src/modules/settings/checkbox.js b/public/src/modules/settings/checkbox.js index d091d0226d..865e89c0f3 100644 --- a/public/src/modules/settings/checkbox.js +++ b/public/src/modules/settings/checkbox.js @@ -15,6 +15,7 @@ define('settings/checkbox', function () { }, set: function (element, value) { element.prop('checked', value); + element.closest('.mdl-switch').toggleClass('is-checked', element.is(':checked')); }, get: function (element, trim, empty) { var value = element.prop('checked'); diff --git a/public/src/modules/settings/object.js b/public/src/modules/settings/object.js index 8c7a53f99a..05971d6d73 100644 --- a/public/src/modules/settings/object.js +++ b/public/src/modules/settings/object.js @@ -69,7 +69,7 @@ define('settings/object', function () { } catch (_error) { return $(document.createTextNode(separator)); } - })(); + }()); element.empty(); if (typeof value !== 'object') { value = {}; diff --git a/public/src/modules/share.js b/public/src/modules/share.js index c0648009f0..9229ca549e 100644 --- a/public/src/modules/share.js +++ b/public/src/modules/share.js @@ -2,11 +2,11 @@ /* globals define */ -define('share', function() { +define('share', function () { var module = {}; - module.addShareHandlers = function(name) { + module.addShareHandlers = function (name) { var baseUrl = window.location.protocol + '//' + window.location.host; @@ -15,18 +15,18 @@ define('share', function() { return false; } - $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function() { + $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () { var postLink = $(this).find('.post-link'); postLink.val(baseUrl + getPostUrl($(this))); // without the setTimeout can't select the text in the input - setTimeout(function() { + setTimeout(function () { postLink.putCursorAtEnd().select(); }, 50); }); - addHandler('.post-link', function(e) { + addHandler('.post-link', function (e) { e.preventDefault(); return false; }); @@ -51,9 +51,8 @@ define('share', function() { } function getPostUrl(clickedElement) { - var parts = window.location.pathname.split('/'); - var postIndex = parseInt(clickedElement.parents('[data-index]').attr('data-index'), 10); - return '/' + parts[1] + '/' + parts[2] + (parts[3] ? '/' + parts[3] : '') + (postIndex ? '/' + (postIndex + 1) : ''); + var pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10); + return '/post' + (pid ? '/' + (pid) : ''); } return module; diff --git a/public/src/modules/sort.js b/public/src/modules/sort.js index 960e21facb..c9e80020af 100644 --- a/public/src/modules/sort.js +++ b/public/src/modules/sort.js @@ -1,7 +1,7 @@ 'use strict'; /* globals define, config, socket, app, ajaxify, templates */ -define('sort', ['components'], function(components) { +define('sort', ['components'], function (components) { var module = {}; module.handleSort = function (field, method, gotoOnSave) { @@ -10,9 +10,9 @@ define('sort', ['components'], function(components) { var currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]'); currentSetting.find('i').addClass('fa-check'); - $('.category, .topic').on('click', '[component="thread/sort"] a', function() { + $('.category, .topic').on('click', '[component="thread/sort"] a', function () { var newSetting = $(this).attr('data-sort'); - socket.emit(method, newSetting, function(err) { + socket.emit(method, newSetting, function (err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/modules/sounds.js b/public/src/modules/sounds.js index 4a6bb80f51..c82391159e 100644 --- a/public/src/modules/sounds.js +++ b/public/src/modules/sounds.js @@ -1,28 +1,28 @@ "use strict"; /* global app, define, socket, config */ -define('sounds', ['buzz'], function(buzz) { +define('sounds', ['buzz'], function (buzz) { var Sounds = {}; var loadedSounds = {}; var eventSoundMapping; var files; - socket.on('event:sounds.reloadMapping', loadMapping); + socket.on('event:sounds.reloadMapping', function () { + Sounds.reloadMapping(); + }); - function loadMapping(callback) { - callback = callback || function() {}; - socket.emit('modules.sounds.getMapping', function(err, mapping) { + Sounds.reloadMapping = function () { + socket.emit('modules.sounds.getMapping', function (err, mapping) { if (err) { - return app.alertError('[sounds] Could not load sound mapping!'); + return app.alertError(err.message); } eventSoundMapping = mapping; - callback(); }); } function loadData(callback) { - socket.emit('modules.sounds.getData', function(err, data) { + socket.emit('modules.sounds.getData', function (err, data) { if (err) { return app.alertError('[sounds] Could not load sound mapping!'); } @@ -54,15 +54,11 @@ define('sounds', ['buzz'], function(buzz) { createSound(); } - Sounds.play = function(name) { + Sounds.play = function (name) { function play() { Sounds.playFile(eventSoundMapping[name]); } - if (!config.notificationSounds) { - return; - } - if (!eventSoundMapping) { return loadData(play); } @@ -70,7 +66,7 @@ define('sounds', ['buzz'], function(buzz) { play(); }; - Sounds.playFile = function(fileName) { + Sounds.playFile = function (fileName) { if (!fileName) { return; } diff --git a/public/src/modules/taskbar.js b/public/src/modules/taskbar.js index 7f5978250b..9e51e34d72 100644 --- a/public/src/modules/taskbar.js +++ b/public/src/modules/taskbar.js @@ -1,23 +1,23 @@ "use strict"; /*global define, app, templates*/ -define('taskbar', function() { +define('taskbar', function () { var taskbar = {}; - taskbar.init = function() { + taskbar.init = function () { var self = this; - templates.parse('modules/taskbar', {}, function(html) { + templates.parse('modules/taskbar', {}, function (html) { self.taskbar = $(html); self.tasklist = self.taskbar.find('ul'); $(document.body).append(self.taskbar); - self.taskbar.on('click', 'li', function() { + self.taskbar.on('click', 'li', function () { var $btn = $(this), module = $btn.attr('data-module'), uuid = $btn.attr('data-uuid'); - require([module], function(module) { + require([module], function (module) { if (!$btn.hasClass('active')) { minimizeAll(); module.load(uuid); @@ -36,14 +36,14 @@ define('taskbar', function() { }); }; - taskbar.discard = function(module, uuid) { + taskbar.discard = function (module, uuid) { var btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); btnEl.remove(); update(); }; - taskbar.push = function(module, uuid, options) { + taskbar.push = function (module, uuid, options) { var element = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]'); var data = { @@ -60,20 +60,20 @@ define('taskbar', function() { } }; - taskbar.get = function(module) { - var items = $('[data-module="' + module + '"]').map(function(idx, el) { + taskbar.get = function (module) { + var items = $('[data-module="' + module + '"]').map(function (idx, el) { return $(el).data(); }); return items; }; - taskbar.minimize = function(module, uuid) { + taskbar.minimize = function (module, uuid) { var btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); btnEl.removeClass('active'); }; - taskbar.toggleNew = function(uuid, state, silent) { + taskbar.toggleNew = function (uuid, state, silent) { var btnEl = taskbar.tasklist.find('[data-uuid="' + uuid + '"]'); btnEl.toggleClass('new', state); @@ -82,13 +82,13 @@ define('taskbar', function() { } }; - taskbar.updateActive = function(uuid) { + taskbar.updateActive = function (uuid) { var tasks = taskbar.tasklist.find('li'); tasks.removeClass('active'); tasks.filter('[data-uuid="' + uuid + '"]').addClass('active'); }; - taskbar.isActive = function(uuid) { + taskbar.isActive = function (uuid) { var taskBtn = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]'); return taskBtn.hasClass('active'); }; @@ -114,7 +114,7 @@ define('taskbar', function() { .addClass(data.options.className) .html('<a href="#">' + (data.options.icon ? '<i class="fa ' + data.options.icon + '"></i> ' : '') + - (data.options.image ? '<img src="' + data.options.image + '"/> ': '') + + (data.options.image ? '<img src="' + data.options.image + '"/> ' : '') + '<span>' + title + '</span>' + '</a>') .attr({ diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index d0dd85b601..6169b1f891 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -2,19 +2,19 @@ /* globals define*/ -define('topicSelect', ['components'], function(components) { +define('topicSelect', ['components'], function (components) { var TopicSelect = {}; var lastSelected; var topicsContainer; - TopicSelect.init = function(onSelect) { + TopicSelect.init = function (onSelect) { topicsContainer = $('[component="category"]'); - topicsContainer.on('selectstart', function() { + topicsContainer.on('selectstart', function () { return false; }); - topicsContainer.on('click', '[component="topic/select"]', function(ev) { + topicsContainer.on('click', '[component="topic/select"]', function (ev) { var select = $(this); if (ev.shiftKey) { @@ -38,15 +38,15 @@ define('topicSelect', ['components'], function(components) { select.parents('[component="category/topic"]').toggleClass('selected', isSelected); } - TopicSelect.getSelectedTids = function() { + TopicSelect.getSelectedTids = function () { var tids = []; - topicsContainer.find('[component="category/topic"].selected').each(function() { + topicsContainer.find('[component="category/topic"].selected').each(function () { tids.push($(this).attr('data-tid')); }); return tids; }; - TopicSelect.unselectAll = function() { + TopicSelect.unselectAll = function () { topicsContainer.find('[component="category/topic"].selected').removeClass('selected'); topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true); }; @@ -71,7 +71,7 @@ define('topicSelect', ['components'], function(components) { end = tmp; } - for(var i=start; i<=end; ++i) { + for(var i = start; i <= end; ++i) { var topic = $('[component="category/topic"]').eq(i); toggleSelect(topic.find('[component="topic/select"]'), isSelected); } diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index b208a0af48..d462ea5401 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -1,366 +1,459 @@ -;(function(translator) { - "use strict"; - /* globals RELATIVE_PATH, config, define */ +/* global define, jQuery, config, RELATIVE_PATH, utils, window, Promise */ - var S = null; - var stringDefer = null; +(function (factory) { + 'use strict'; + function loadClient(language, filename) { + return Promise.resolve(jQuery.getJSON(config.relative_path + '/language/' + language + '/' + (filename + '.json?v=' + config['cache-buster']))); + } + if (typeof define === 'function' && define.amd) { + // AMD. Register as a named module + define('translator', ['string'], function (string) { + return factory(string, loadClient); + }); + } else if (typeof module === 'object' && module.exports) { + // Node + (function () { + require('promise-polyfill'); + var languages = require('../../../src/languages'); + + function loadServer(language, filename) { + return new Promise(function (resolve, reject) { + languages.get(language, filename + '.json', function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } - // export the class if we are in a Node-like system. - if (typeof module === 'object' && module.exports === translator) { - exports = module.exports = translator; - S = require('string'); + module.exports = factory(require('string'), loadServer); + }()); } else { - stringDefer = $.Deferred(); - require(['string'], function(stringLib) { - S = stringLib; - stringDefer.resolve(S); - }); + window.translator = factory(window.string, loadClient); } +}(function (string, load) { + 'use strict'; + var assign = Object.assign || jQuery.extend; + function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var Translator = (function () { + /** + * Construct a new Translator object + * @param {string} language - Language code for this translator instance + */ + function Translator(language) { + classCallCheck(this, Translator); + + if (!language) { + throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : '')); + } - var languages = {}, - regexes = { - match: /\[\[\w+:[\w\.]+((?!\[\[).)*?\]\]/g, // see tests/translator.js for an explanation re: this monster - split: /[,][\s]*/, - replace: /\]+$/ - }; + this.lang = language; + this.translations = {}; + } - translator.addTranslation = function(language, filename, translations) { - languages[language] = languages[language] || {}; - languages[language].loaded = languages[language].loaded || {}; - languages[language].loading = languages[language].loading || {}; + Translator.prototype.load = load; + + /** + * Parse the translation instructions into the language of the Translator instance + * @param {string} str - Source string + * @returns {Promise<string>} + */ + Translator.prototype.translate = function translate(str) { + // regex for valid text in namespace / key + var validText = 'a-zA-Z0-9\\-_.\\/'; + var validTextRegex = new RegExp('[' + validText + ']'); + var invalidTextRegex = new RegExp('[^' + validText + '\\]]'); + + // current cursor position + var cursor = 0; + // last break of the input string + var lastBreak = 0; + // length of the input string + var len = str.length; + // array to hold the promises for the translations + // and the strings of untranslated text in between + var toTranslate = []; + + // split a translator string into an array of tokens + // but don't split by commas inside other translator strings + function split(text) { + var len = text.length; + var arr = []; + var i = 0; + var brk = 0; + var level = 0; + + while (i + 2 <= len) { + if (text.slice(i, i + 2) === '[[') { + level += 1; + i += 1; + } else if (text.slice(i, i + 2) === ']]') { + level -= 1; + i += 1; + } else if (level === 0 && text[i] === ',') { + arr.push(text.slice(brk, i).trim()); + i += 1; + brk = i; + } + i += 1; + } + arr.push(text.slice(brk, i + 1).trim()); + return arr; + } - if (languages[language].loaded[filename]) { - var existing = languages[language].loaded[filename]; - for (var t in translations) { - if (translations.hasOwnProperty(t)) { - languages[language].loaded[filename][t] = translations[t]; + // the loooop, we'll go to where the cursor + // is equal to the length of the string since + // slice doesn't include the ending index + while (cursor + 2 <= len) { + // if the current position in the string looks + // like the beginning of a translation string + if (str.slice(cursor, cursor + 2) === '[[') { + // split the string from the last break + // to the character before the cursor + // add that to the result array + toTranslate.push(str.slice(lastBreak, cursor)); + // set the cursor position past the beginning + // brackets of the translation string + cursor += 2; + // set the last break to our current + // spot since we just broke the string + lastBreak = cursor; + + // the current level of nesting of the translation strings + var level = 0; + var sliced; + // validating the current string is actually a translation + var textBeforeColonFound = false; + var colonFound = false; + var textAfterColonFound = false; + var commaAfterNameFound = false; + + while (cursor + 2 <= len) { + sliced = str.slice(cursor, cursor + 2); + // found some text after the double bracket, + // so this is probably a translation string + if (!textBeforeColonFound && validTextRegex.test(sliced[0])) { + textBeforeColonFound = true; + cursor += 1; + // found a colon, so this is probably a translation string + } else if (textBeforeColonFound && !colonFound && sliced[0] === ':') { + colonFound = true; + cursor += 1; + // found some text after the colon, + // so this is probably a translation string + } else if (colonFound && !textAfterColonFound && validTextRegex.test(sliced[0])) { + textAfterColonFound = true; + cursor += 1; + } else if (textAfterColonFound && !commaAfterNameFound && sliced[0] === ',') { + commaAfterNameFound = true; + cursor += 1; + // a space or comma was found before the name + // this isn't a translation string, so back out + } else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) && + invalidTextRegex.test(sliced[0])) { + cursor += 1; + lastBreak -= 2; + if (level > 0) { + level -= 1; + } else { + break; + } + // if we're at the beginning of another translation string, + // we're nested, so add to our level + } else if (sliced === '[[') { + level += 1; + cursor += 2; + // if we're at the end of a translation string + } else if (sliced === ']]') { + // if we're at the base level, then this is the end + if (level === 0) { + // so grab the name and args + var result = split(str.slice(lastBreak, cursor)); + var name = result[0]; + var args = result.slice(1); + + // add the translation promise to the array + toTranslate.push(this.translateKey(name, args)); + // skip past the ending brackets + cursor += 2; + // set this as our last break + lastBreak = cursor; + // and we're no longer in a translation string, + // so continue with the main loop + break; + } + // otherwise we lower the level + level -= 1; + // and skip past the ending brackets + cursor += 2; + } else { + // otherwise just move to the next character + cursor += 1; + } + } } + // move to the next character + cursor += 1; } - } else { - languages[language].loaded[filename] = translations; - } - }; - translator.getTranslations = function(language, filename, callback) { - if (languages[language] && languages[language].loaded[filename]) { - callback(languages[language].loaded[filename]); - } else { - translator.load(language, filename, function() { - callback(languages[language].loaded[filename]); - }); - } - }; + // add the remaining text after the last translation string + toTranslate.push(str.slice(lastBreak, cursor + 2)); - translator.escape = function(text) { - return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; - }; + // and return a promise for the concatenated translated string + return Promise.all(toTranslate).then(function (translated) { + return translated.join(''); + }); + }; - translator.unescape = function(text) { - return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; - }; + /** + * Translates a specific key and array of arguments + * @param {string} name - Translation key (ex. 'global:home') + * @param {string[]} args - Arguments for `%1`, `%2`, etc + * @returns {Promise<string>} + */ + Translator.prototype.translateKey = function translateKey(name, args) { + var self = this; + + var result = name.split(':', 2); + var namespace = result[0]; + var key = result[1]; + + if (namespace && !key) { + return Promise.resolve('[[' + namespace + ']]'); + } - translator.getLanguage = function() { - return config.defaultLang; - }; + var translation = this.getTranslation(namespace, key); + var argsToTranslate = args.map(function (arg) { + return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s; + }).map(function (arg) { + return self.translate(arg); + }); - translator.prepareDOM = function() { - // Load the appropriate timeago locale file, and correct NodeBB language codes to timeago codes, if necessary - var languageCode; - switch(config.userLang) { - case 'en_GB': - case 'en_US': - languageCode = 'en'; - break; - - case 'cs': - languageCode = 'cz'; - break; - - case 'fa_IR': - languageCode = 'fa'; - break; - - case 'pt_BR': - languageCode = 'pt-br'; - break; - - case 'nb': - languageCode = 'no'; - break; - - case 'zh_TW': - languageCode = 'zh-TW'; - break; - - case 'zh_CN': - languageCode = 'zh-CN'; - break; - - default: - languageCode = config.userLang; - break; - } + // so we can await all promises at once + argsToTranslate.unshift(translation); - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').success(function() { - $('.timeago').timeago(); - translator.timeagoShort = $.extend({}, jQuery.timeago.settings.strings); + return Promise.all(argsToTranslate).then(function (result) { + var translated = result[0]; + var translatedArgs = result.slice(1); - // Retrieve the shorthand timeago values as well - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').success(function() { - // Switch back to long-form - translator.toggleTimeagoShorthand(); - }).fail(function() { - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.en-short.js').success(function() { - // Switch back to long-form - translator.toggleTimeagoShorthand(); + if (!translated) { + return key; + } + var out = translated; + translatedArgs.forEach(function (arg, i) { + out = out.replace(new RegExp('%' + (i + 1), 'g'), arg); }); + return out; }); - }).fail(function() { - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.en-short.js').success(function() { - // Switch back to long-form - translator.toggleTimeagoShorthand(); - $.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.en.js'); - }); - }); - - // Add directional code if necessary - translator.translate('[[language:dir]]', function(value) { - if (value) { - $('html').css('direction', value).attr('data-dir', value); - } - }); - }; - - translator.toggleTimeagoShorthand = function() { - var tmp = $.extend({}, jQuery.timeago.settings.strings); - jQuery.timeago.settings.strings = $.extend({}, translator.timeagoShort); - translator.timeagoShort = $.extend({}, tmp); - }; + }; - translator.translate = function (text, language, callback) { - if (typeof language === 'function') { - callback = language; - if ('undefined' !== typeof window && config) { - language = utils.params().lang || config.userLang || 'en_GB'; + /** + * Load translation file (or use a cached version), and optionally return the translation of a certain key + * @param {string} namespace - The file name of the translation namespace + * @param {string} [key] - The key of the specific translation to getJSON + * @returns {Promise<Object|string>} + */ + Translator.prototype.getTranslation = function getTranslation(namespace, key) { + var translation; + if (!namespace) { + console.warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : '')); + translation = Promise.resolve({}); } else { - var meta = require('../../../src/meta'); - language = meta.config.defaultLang || 'en_GB'; + translation = this.translations[namespace] = this.translations[namespace] || this.load(this.lang, namespace); } - } - - if (!text) { - return callback(text); - } - var keys = text.match(regexes.match); + if (key) { + return translation.then(function (x) { + return x[key]; + }); + } + return translation; + }; - if (!keys) { - return callback(text); - } + /** + * Get the language of the current environment, falling back to defaults + * @returns {string} + */ + Translator.getLanguage = function getLanguage() { + var lang; - translateKeys(keys, text, language, function(translated) { - keys = translated.match(regexes.match); - if (!keys) { - callback(translated); + if (typeof window === 'object' && window.config && window.utils) { + lang = utils.params().lang || config.userLang || config.defaultLang || 'en_GB'; } else { - translateKeys(keys, translated, language, callback); + var meta = require('../../../src/meta'); + lang = meta.config.defaultLang || 'en_GB'; } - }); - }; - function translateKeys(keys, text, language, callback) { - - var count = keys.length; - if (!count) { - return callback(text); - } - - if (S === null) { // browser environment and S not yet initialized - // we need to wait for async require call - stringDefer.then(function () { translateKeys(keys, text, language, callback); }); - return; - } + return lang; + }; - var data = {text: text}; - keys.forEach(function(key) { - translateKey(key, data, language, function(translated) { - --count; - if (count <= 0) { - callback(translated.text); - } - }); - }); - } + /** + * Create and cache a new Translator instance, or return a cached one + * @param {string} [language] - ('en_GB') Language string + * @returns {Translator} + */ + Translator.create = function create(language) { + if (!language) { + language = Translator.getLanguage(); + } - function translateKey(key, data, language, callback) { - key = '' + key; - var variables = key.split(regexes.split); + Translator.cache[language] = Translator.cache[language] || new Translator(language); - var parsedKey = key.replace('[[', '').replace(']]', '').split(':'); - parsedKey = [parsedKey[0]].concat(parsedKey.slice(1).join(':')); - if (!(parsedKey[0] && parsedKey[1])) { - return callback(data); - } + return Translator.cache[language]; + }; - var languageFile = parsedKey[0]; - parsedKey = ('' + parsedKey[1]).split(',')[0]; + Translator.cache = {}; + + return Translator; + }()); + + var adaptor = { + /** + * The Translator class + */ + Translator: Translator, + + /** + * Legacy translator function for backwards compatibility + */ + translate: function translate(text, language, callback) { + // console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' + + // 'Use the `translator.Translator` class instead.'); + + var cb = callback; + var lang = language; + if (typeof language === 'function') { + cb = language; + lang = null; + } - translator.load(language, languageFile, function(languageData) { - data.text = insertLanguage(data.text, key, languageData[parsedKey], variables); - callback(data); - }); - } + if (!(typeof text === 'string' || text instanceof String) || text === '') { + return cb(''); + } - function insertLanguage(text, key, value, variables) { - if (value) { - variables.forEach(function(variable, index) { - if (index > 0) { - variable = S(variable).chompRight(']]').collapseWhitespace().decodeHTMLEntities().escapeHTML().s; - value = value.replace('%' + index, function() { return variable; }); - } + Translator.create(lang).translate(text).then(function (output) { + return cb(output); + }).catch(function (err) { + console.error('Translation failed: ' + err.stack); }); - - text = text.replace(key, function() { return value; }); - } else { - var string = key.split(':'); - text = text.replace(key, string[string.length-1].replace(regexes.replace, '')); - } - - return text; - } - - translator.compile = function() { - var args = Array.prototype.slice.call(arguments, 0); - - return '[[' + args.join(', ') + ']]'; - }; - - translator.load = function (language, filename, callback) { - if (isLanguageFileLoaded(language, filename)) { - if (callback) { - callback(languages[language].loaded[filename]); - } - } else if (isLanguageFileLoading(language, filename)) { - if (callback) { - addLanguageFileCallback(language, filename, callback); + }, + + /** + * Construct a translator pattern + * @param {string} name - Translation name + * @param {string[]} args - Optional arguments for the pattern + */ + compile: function compile() { + var args = Array.prototype.slice.call(arguments, 0); + + return '[[' + args.join(', ') + ']]'; + }, + + /** + * Escape translation patterns from text + */ + escape: function escape(text) { + return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text; + }, + + /** + * Unescape translation patterns from text + */ + unescape: function unescape(text) { + return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text; + }, + + /** + * Add translations to the cache + */ + addTranslation: function addTranslation(language, filename, translation) { + Translator.create(language).getTranslation(filename).then(function (translations) { + assign(translations, translation); + }); + }, + + /** + * Get the translations object + */ + getTranslations: function getTranslations(language, filename, callback) { + callback = callback || function () {}; + Translator.create(language).getTranslation(filename).then(callback); + }, + + /** + * Alias of getTranslations + */ + load: function load(language, filename, callback) { + adaptor.getTranslations(language, filename, callback); + }, + + /** + * Get the language of the current environment, falling back to defaults + */ + getLanguage: Translator.getLanguage, + + toggleTimeagoShorthand: function toggleTimeagoShorthand() { + var tmp = assign({}, jQuery.timeago.settings.strings); + jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort); + adaptor.timeagoShort = assign({}, tmp); + }, + prepareDOM: function prepareDOM() { + // Load the appropriate timeago locale file, + // and correct NodeBB language codes to timeago codes, if necessary + var languageCode = void 0; + switch (config.userLang) { + case 'en_GB': + case 'en_US': + languageCode = 'en'; + break; + + case 'fa_IR': + languageCode = 'fa'; + break; + + case 'pt_BR': + languageCode = 'pt-br'; + break; + + case 'nb': + languageCode = 'no'; + break; + + case 'zh_TW': + languageCode = 'zh-TW'; + break; + + case 'zh_CN': + languageCode = 'zh-CN'; + break; + + default: + languageCode = config.userLang; + break; } - } else { - - languages[language] = languages[language] || {loading: {}, loaded: {}, callbacks: []}; - - languages[language].loading[filename] = true; - - load(language, filename, function(translations) { - languages[language].loaded[filename] = translations; + jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(function () { + jQuery('.timeago').timeago(); + adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings); - if (callback) { - callback(translations); - } + // Retrieve the shorthand timeago values as well + jQuery.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () { + // Switch back to long-form + adaptor.toggleTimeagoShorthand(); + }); + }); - while (languages[language].callbacks && languages[language].callbacks[filename] && languages[language].callbacks[filename].length) { - languages[language].callbacks[filename].pop()(translations); + // Add directional code if necessary + adaptor.translate('[[language:dir]]', function (value) { + if (value) { + jQuery('html').css('direction', value).attr('data-dir', value); } - - languages[language].loading[filename] = false; }); } }; - function isLanguageFileLoaded(language, filename) { - var languageObj = languages[language]; - return languageObj && languageObj.loaded && languageObj.loaded[filename] && !languageObj.loading[filename]; - } - - function isLanguageFileLoading(language, filename) { - return languages[language] && languages[language].loading && languages[language].loading[filename]; - } - - function addLanguageFileCallback(language, filename, callback) { - languages[language].callbacks = languages[language].callbacks || {}; - - languages[language].callbacks[filename] = languages[language].callbacks[filename] || []; - languages[language].callbacks[filename].push(callback); - } - - function load(language, filename, callback) { - if ('undefined' !== typeof window) { - loadClient(language, filename, callback); - } else { - loadServer(language, filename, callback); - } - } - - function loadClient(language, filename, callback) { - $.getJSON(config.relative_path + '/language/' + language + '/' + filename + '.json?v=' + config['cache-buster'], callback); - } - - function loadServer(language, filename, callback) { - var fs = require('fs'), - path = require('path'), - winston = require('winston'), - file = require('../../../src/file'), - plugins = require('../../../src/plugins'), - meta = require('../../../src/meta'); - - language = language || meta.config.defaultLang || 'en_GB'; - - if (!file.existsSync(path.join(__dirname, '../../language', language))) { - winston.warn('[translator] Language \'' + meta.config.defaultLang + '\' not found. Defaulting to \'en_GB\''); - language = 'en_GB'; - } - - fs.readFile(path.join(__dirname, '../../language', language, filename + '.json'), function(err, data) { - var onData = function(data) { - try { - data = JSON.parse(data.toString()); - } catch (e) { - winston.error('Could not parse `' + filename + '.json`, syntax error? Skipping...'); - data = {}; - } - callback(data); - } - - if (err) { - if (err.code === 'ENOENT' && plugins.customLanguageFallbacks.hasOwnProperty(filename)) { - // Resource non-existant but fallback exists - return fs.readFile(plugins.customLanguageFallbacks[filename], { - encoding: 'utf-8' - }, function(err, data) { - if (err) { - return winston.error('[translator] Could not load fallback language file for resource ' + filename); - } - - onData(data); - }) - } else { - winston.error('[translator] Could not load `' + filename + '`: ' + err.message + '. Skipping...'); - return callback({}); - } - } - - onData(data); - }); - } - - // Use the define() function if we're in AMD land - if (typeof define === 'function' && define.amd) { - define('translator', translator); - - var _translator = translator; - - // Expose a global `translator` object for backwards compatibility - window.translator = { - translate: function() { - if (typeof console !== 'undefined' && console.warn) { - console.warn('[translator] Global invocation of the translator is now deprecated, please `require` the module instead.'); - } - _translator.translate.apply(_translator, arguments); - } - } - } -})( - typeof exports === 'object' ? exports : - typeof define === 'function' && define.amd ? {} : - translator = {} -); + return adaptor; +})); diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index 6a0d7f161a..106b82d67d 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -2,11 +2,11 @@ /* globals define, templates */ -define('uploader', ['csrf', 'translator'], function(csrf, translator) { +define('uploader', ['translator'], function (translator) { var module = {}; - module.open = function(route, params, fileSize, callback) { + module.open = function (route, params, fileSize, callback) { console.warn('[uploader] uploader.open() is deprecated, please use uploader.show() instead, and pass parameters as a singe option with callback, e.g. uploader.show({}, callback);'); module.show({ route: route, @@ -15,7 +15,7 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { }, callback); }; - module.show = function(data, callback) { + module.show = function (data, callback) { var fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false; parseModal({ showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true, @@ -23,12 +23,12 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { title: data.title || '[[global:upload_file]]', description: data.description || '', button: data.button || '[[global:upload]]', - accept: data.accept ? data.accept.replace(/,/g, ',') : '' - }, function(uploadModal) { + accept: data.accept ? data.accept.replace(/,/g, ', ') : '' + }, function (uploadModal) { uploadModal = $(uploadModal); uploadModal.modal('show'); - uploadModal.on('hidden.bs.modal', function() { + uploadModal.on('hidden.bs.modal', function () { uploadModal.remove(); }); @@ -36,24 +36,28 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { uploadForm.attr('action', data.route); uploadForm.find('#params').val(JSON.stringify(data.params)); - uploadModal.find('#fileUploadSubmitBtn').on('click', function() { + uploadModal.find('#fileUploadSubmitBtn').on('click', function () { + $(this).addClass('disabled'); uploadForm.submit(); }); - uploadForm.submit(function() { + uploadForm.submit(function () { onSubmit(uploadModal, fileSize, callback); return false; }); }); }; - module.hideAlerts = function(modal) { + module.hideAlerts = function (modal) { $(modal).find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide'); }; function onSubmit(uploadModal, fileSize, callback) { function showAlert(type, message) { module.hideAlerts(uploadModal); + if (type === 'error') { + uploadModal.find('#fileUploadSubmitBtn').removeClass('disabled'); + } uploadModal.find('#alert-' + type).translateText(message).removeClass('hide'); } @@ -72,16 +76,16 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { uploadModal.find('#uploadForm').ajaxSubmit({ headers: { - 'x-csrf-token': csrf.get() + 'x-csrf-token': config.csrf_token }, - error: function(xhr) { + error: function (xhr) { xhr = maybeParse(xhr); showAlert('error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status); }, - uploadProgress: function(event, position, total, percent) { + uploadProgress: function (event, position, total, percent) { uploadModal.find('#upload-progress-bar').css('width', percent + '%'); }, - success: function(response) { + success: function (response) { response = maybeParse(response); if (response.error) { @@ -91,7 +95,7 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { callback(response[0].url); showAlert('success', '[[uploads:upload-success]]'); - setTimeout(function() { + setTimeout(function () { module.hideAlerts(uploadModal); uploadModal.modal('hide'); }, 750); @@ -100,7 +104,7 @@ define('uploader', ['csrf', 'translator'], function(csrf, translator) { } function parseModal(tplVals, callback) { - templates.parse('partials/modals/upload_file_modal', tplVals, function(html) { + templates.parse('partials/modals/upload_file_modal', tplVals, function (html) { translator.translate(html, callback); }); } diff --git a/public/src/overrides.js b/public/src/overrides.js index 62eca1d568..c644bb6281 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -7,8 +7,8 @@ var overrides = overrides || {}; if ('undefined' !== typeof window) { (function ($, undefined) { - require(['translator'], function(translator) { - $.fn.getCursorPosition = function() { + require(['translator'], function (translator) { + $.fn.getCursorPosition = function () { var el = $(this).get(0); var pos = 0; if('selectionStart' in el) { @@ -23,11 +23,11 @@ if ('undefined' !== typeof window) { return pos; }; - $.fn.selectRange = function(start, end) { + $.fn.selectRange = function (start, end) { if(!end) { end = start; } - return this.each(function() { + return this.each(function () { if (this.setSelectionRange) { this.focus(); this.setSelectionRange(start, end); @@ -42,8 +42,8 @@ if ('undefined' !== typeof window) { }; //http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element - $.fn.putCursorAtEnd = function() { - return this.each(function() { + $.fn.putCursorAtEnd = function () { + return this.each(function () { $(this).focus(); if (this.setSelectionRange) { @@ -56,30 +56,39 @@ if ('undefined' !== typeof window) { }); }; - $.fn.translateHtml = function(str) { + $.fn.translateHtml = function (str) { return translate(this, 'html', str); }; - $.fn.translateText = function(str) { + $.fn.translateText = function (str) { return translate(this, 'text', str); }; - $.fn.translateVal = function(str) { + $.fn.translateVal = function (str) { return translate(this, 'val', str); }; + $.fn.translateAttr = function (attr, str) { + return this.each(function () { + var el = $(this); + translator.translate(str, function (translated) { + el.attr(attr, translated); + }); + }); + }; + function translate(elements, type, str) { - return elements.each(function() { + return elements.each(function () { var el = $(this); - translator.translate(str, function(translated) { + translator.translate(str, function (translated) { el[type](translated); }); }); } }); - })(jQuery || {fn:{}}); + }(jQuery || {fn:{}})); - (function(){ + (function (){ // FIX FOR #1245 - https://github.com/NodeBB/NodeBB/issues/1245 // from http://stackoverflow.com/questions/15931962/bootstrap-dropdown-disappear-with-right-click-on-firefox // obtain a reference to the original handler @@ -100,38 +109,38 @@ if ('undefined' !== typeof window) { _clearMenus(); } }); - })(); + }()); overrides.overrideBootbox = function () { - require(['translator'], function(translator) { + require(['translator'], function (translator) { var dialog = bootbox.dialog, prompt = bootbox.prompt, confirm = bootbox.confirm; - + function translate(modal) { var header = modal.find('.modal-header'), footer = modal.find('.modal-footer'); - translator.translate(header.html(), function(html) { + translator.translate(header.html(), function (html) { header.html(html); }); - translator.translate(footer.html(), function(html) { + translator.translate(footer.html(), function (html) { footer.html(html); }); } - bootbox.dialog = function() { + bootbox.dialog = function () { var modal = $(dialog.apply(this, arguments)[0]); translate(modal); return modal; }; - bootbox.prompt = function() { + bootbox.prompt = function () { var modal = $(prompt.apply(this, arguments)[0]); translate(modal); return modal; }; - bootbox.confirm = function() { + bootbox.confirm = function () { var modal = $(confirm.apply(this, arguments)[0]); translate(modal); return modal; @@ -139,13 +148,13 @@ if ('undefined' !== typeof window) { }); }; - overrides.overrideTimeago = function() { + overrides.overrideTimeago = function () { var timeagoFn = $.fn.timeago; - $.fn.timeago = function() { + $.fn.timeago = function () { var els = timeagoFn.apply(this, arguments); if (els) { - els.each(function() { + els.each(function () { $(this).attr('title', (new Date($(this).attr('title'))).toString()); }); } diff --git a/public/src/require-config.js b/public/src/require-config.js new file mode 100644 index 0000000000..0ad2f8a58c --- /dev/null +++ b/public/src/require-config.js @@ -0,0 +1,11 @@ +require.config({ + baseUrl: config.relative_path + "/src/modules", + waitSeconds: 7, + urlArgs: "v=" + config['cache-buster'], + paths: { + 'forum': '../client', + 'admin': '../admin', + 'vendor': '../../vendor', + 'plugins': '../../plugins' + } +}); diff --git a/public/src/sockets.js b/public/src/sockets.js index 461c148be7..a8c33be1e8 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -23,11 +23,17 @@ app.isConnected = false; socket.on('disconnect', onDisconnect); - socket.on('reconnect_failed', function() { + socket.on('reconnect_failed', function () { // Wait ten times the reconnection delay and then start over setTimeout(socket.connect.bind(socket), parseInt(config.reconnectionDelay, 10) * 10); }); + socket.on('checkSession', function (uid) { + if (parseInt(uid, 10) !== parseInt(app.user.uid, 10)) { + app.handleInvalidSession(); + } + }); + socket.on('event:banned', onEventBanned); socket.on('event:alert', app.alert); @@ -42,9 +48,11 @@ app.isConnected = false; if (reconnecting) { var reconnectEl = $('#reconnect'); + var reconnectAlert = $('#reconnect-alert'); reconnectEl.tooltip('destroy'); reconnectEl.html('<i class="fa fa-check"></i>'); + reconnectAlert.fadeOut(500); reconnecting = false; reJoinCurrentRoom(); @@ -53,7 +61,7 @@ app.isConnected = false; $(window).trigger('action:reconnected'); - setTimeout(function() { + setTimeout(function () { reconnectEl.removeClass('active').addClass('hide'); }, 3000); } @@ -96,12 +104,14 @@ app.isConnected = false; function onReconnecting() { reconnecting = true; var reconnectEl = $('#reconnect'); + var reconnectAlert = $('#reconnect-alert'); if (!reconnectEl.hasClass('active')) { reconnectEl.html('<i class="fa fa-spinner fa-spin"></i>'); + reconnectAlert.fadeIn(500).removeClass('hide'); } - reconnectEl.addClass('active').removeClass("hide").tooltip({ + reconnectEl.addClass('active').removeClass('hide').tooltip({ placement: 'bottom' }); } @@ -119,7 +129,7 @@ app.isConnected = false; timeout: 1000 }); - setTimeout(function() { + setTimeout(function () { window.location.href = config.relative_path + '/'; }, 1000); } diff --git a/public/src/utils.js b/public/src/utils.js index 78e0013c25..b2d86fedb1 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -1,4 +1,4 @@ -(function(module) { +(function (module) { 'use strict'; var utils, fs, XRegExp; @@ -7,11 +7,11 @@ fs = require('fs'); XRegExp = require('xregexp'); - process.profile = function(operation, start) { + process.profile = function (operation, start) { console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start)); }; - process.elapsedTimeSince = function(start) { + process.elapsedTimeSince = function (start) { var diff = process.hrtime(start); return diff[0] * 1e3 + diff[1] / 1e6; }; @@ -22,8 +22,8 @@ module.exports = utils = { - generateUUID: function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + generateUUID: function () { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); @@ -31,10 +31,10 @@ }, //Adapted from http://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search - walk: function(dir, done) { + walk: function (dir, done) { var results = []; - fs.readdir(dir, function(err, list) { + fs.readdir(dir, function (err, list) { if (err) { return done(err); } @@ -42,11 +42,19 @@ if (!pending) { return done(null, results); } - list.forEach(function(file) { + list.forEach(function (file) { file = dir + '/' + file; - fs.stat(file, function(err, stat) { + fs.stat(file, function (err, stat) { + if (err) { + return done(err); + } + if (stat && stat.isDirectory()) { - utils.walk(file, function(err, res) { + utils.walk(file, function (err, res) { + if (err) { + return done(err); + } + results = results.concat(res); if (!--pending) { done(null, results); @@ -74,7 +82,7 @@ languageKeyRegex: /\[\[[\w]+:.+\]\]/, //http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/ - slugify: function(str, preserveCase) { + slugify: function (str, preserveCase) { if (!str) { return ''; } @@ -92,7 +100,7 @@ return str; }, - cleanUpTag: function(tag, maxLength) { + cleanUpTag: function (tag, maxLength) { if (typeof tag !== 'string' || !tag.length ) { return ''; } @@ -109,32 +117,32 @@ return tag; }, - removePunctuation: function(str) { + removePunctuation: function (str) { return str.replace(/[\.,-\/#!$%\^&\*;:{}=\-_`<>'"~()?]/g, ''); }, - isEmailValid: function(email) { + isEmailValid: function (email) { return typeof email === 'string' && email.length && email.indexOf('@') !== -1; }, - isUserNameValid: function(name) { + isUserNameValid: function (name) { return (name && name !== '' && (/^['"\s\-.*0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name))); }, - isPasswordValid: function(password) { + isPasswordValid: function (password) { return typeof password === 'string' && password.length; }, - isNumber: function(n) { + isNumber: function (n) { return !isNaN(parseFloat(n)) && isFinite(n); }, - hasLanguageKey: function(input) { + hasLanguageKey: function (input) { return utils.languageKeyRegex.test(input); }, // shallow objects merge - merge: function() { + merge: function () { var result = {}, obj, keys; for (var i = 0; i < arguments.length; i++) { obj = arguments[i] || {}; @@ -177,25 +185,25 @@ }, fileMimeType: function (path) { - utils.extensionToMimeType(utils.fileExtension(path)); + return utils.extensionToMimeType(utils.fileExtension(path)); }, - extensionToMimeType: function(extension) { + extensionToMimeType: function (extension) { return utils.extensionMimeTypeMap[extension] || '*'; }, - isRelativeUrl: function(url) { + isRelativeUrl: function (url) { var firstChar = url.slice(0, 1); return (firstChar === '.' || firstChar === '/'); }, - makeNumbersHumanReadable: function(elements) { - elements.each(function() { + makeNumbersHumanReadable: function (elements) { + elements.each(function () { $(this).html(utils.makeNumberHumanReadable($(this).attr('title'))); }); }, - makeNumberHumanReadable: function(num) { + makeNumberHumanReadable: function (num) { var n = parseInt(num, 10); if(!n) { return num; @@ -220,7 +228,7 @@ return text.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,"); }, - toISOString: function(timestamp) { + toISOString: function (timestamp) { if (!timestamp || !Date.prototype.toISOString) { return ''; } @@ -240,25 +248,25 @@ 'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'], - escapeRegexChars: function(text) { + escapeRegexChars: function (text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, - escapeHTML: function(raw) { + escapeHTML: function (raw) { return raw.replace(/&/gm,"&").replace(/</gm,"<").replace(/>/gm,">"); }, - isAndroidBrowser: function() { + isAndroidBrowser: function () { // http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser var nua = navigator.userAgent; return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1)); }, - isTouchDevice: function() { + isTouchDevice: function () { return 'ontouchstart' in document.documentElement; }, - findBootstrapEnvironment: function() { + findBootstrapEnvironment: function () { //http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api var envs = ['xs', 'sm', 'md', 'lg'], $el = $('<div>'); @@ -268,7 +276,7 @@ for (var i = envs.length - 1; i >= 0; i--) { var env = envs[i]; - $el.addClass('hidden-'+env); + $el.addClass('hidden-' + env); if ($el.is(':hidden')) { $el.remove(); return env; @@ -276,14 +284,14 @@ } }, - isMobile: function() { + isMobile: function () { var env = utils.findBootstrapEnvironment(); - return ['xs', 'sm'].some(function(targetEnv) { + return ['xs', 'sm'].some(function (targetEnv) { return targetEnv === env; }); }, - getHoursArray: function() { + getHoursArray: function () { var currentHour = new Date().getHours(), labels = []; @@ -295,14 +303,14 @@ return labels.reverse(); }, - getDaysArray: function(from) { + getDaysArray: function (from) { var currentDay = new Date(from || Date.now()).getTime(), months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], labels = [], tmpDate; - for(var x=29;x>=0;x--) { - tmpDate = new Date(currentDay - (1000*60*60*24*x)); + for(var x = 29;x >= 0;x--) { + tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x)); labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate()); } @@ -310,7 +318,7 @@ }, /* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */ - isElementInViewport: function(el) { + isElementInViewport: function (el) { //special bonus for those using jQuery if (typeof jQuery === "function" && el instanceof jQuery) { el = el[0]; @@ -327,7 +335,7 @@ }, // get all the url params in a single key/value hash - params: function(options) { + params: function (options) { var a, hash = {}, params; options = options || {}; @@ -338,7 +346,7 @@ } params = (a ? a.search : window.location.search).substring(1).split("&"); - params.forEach(function(param) { + params.forEach(function (param) { var val = param.split('='), key = decodeURI(val[0]), value = options.skipToType[key] ? decodeURI(val[1]) : utils.toType(decodeURI(val[1])); @@ -360,11 +368,11 @@ return hash; }, - param: function(key) { + param: function (key) { return this.params()[key]; }, - urlToLocation: function(url) { + urlToLocation: function (url) { var a = document.createElement('a'); a.href = url; return a; @@ -372,7 +380,7 @@ // return boolean if string 'true' or string 'false', or if a parsable string which is a number // also supports JSON object and/or arrays parsing - toType: function(str) { + toType: function (str) { var type = typeof str; if (type !== 'string') { return str; @@ -401,7 +409,7 @@ // get example: utils.props(A, 'a.b.c') // returns {d: 10} // get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError // credits to github.com/gkindel - props: function(obj, props, value) { + props: function (obj, props, value) { if(obj === undefined) { obj = window; } @@ -423,6 +431,14 @@ } return utils.props(obj[prop], newProps, value); + }, + + isInternalURI: function (targetLocation, referenceLocation, relative_path) { + return targetLocation.host === '' || // Relative paths are always internal links + ( + targetLocation.host === referenceLocation.host && targetLocation.protocol === referenceLocation.protocol && // Otherwise need to check if protocol and host match + (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true) // Subfolder installs need this additional check + ); } }; @@ -439,13 +455,13 @@ } if (typeof String.prototype.endsWith != 'function') { - String.prototype.endsWith = function(suffix) { + String.prototype.endsWith = function (suffix) { if (this.length < suffix.length) { return false; } var len = this.length; var suffixLen = suffix.length; - for (var i=1; (i <= suffixLen && this[len - i] === suffix[suffixLen - i]); ++i) { + for (var i = 1; (i <= suffixLen && this[len - i] === suffix[suffixLen - i]); ++i) { continue; } return i > suffixLen; @@ -453,7 +469,7 @@ } if (typeof String.prototype.rtrim != 'function') { - String.prototype.rtrim = function() { + String.prototype.rtrim = function () { return this.replace(/\s+$/g, ''); }; } @@ -462,8 +478,8 @@ window.utils = module.exports; } -})('undefined' === typeof module ? { +}('undefined' === typeof module ? { module: { exports: {} } -} : module); +} : module)); diff --git a/public/src/variables.js b/public/src/variables.js deleted file mode 100644 index 487099ac3c..0000000000 --- a/public/src/variables.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict"; -/*global ajaxify*/ - -(function(ajaxify) { - - ajaxify.variables = {}; - - ajaxify.variables.parse = function() { - var dataEl = $('#ajaxify-data'); - if (dataEl.length) { - ajaxify.data = JSON.parse(dataEl.text()); - dataEl.remove(); - } - }; -}(ajaxify || {})); diff --git a/public/src/widgets.js b/public/src/widgets.js index 2a8b052bba..8d4eb10e1f 100644 --- a/public/src/widgets.js +++ b/public/src/widgets.js @@ -1,101 +1,89 @@ "use strict"; -/*global ajaxify, templates, config, RELATIVE_PATH*/ +/*global ajaxify, templates, config, utils*/ -(function(ajaxify) { +(function (ajaxify) { ajaxify.widgets = {}; - ajaxify.widgets.reposition = function(location) { - $('body [no-widget-class]').each(function() { + ajaxify.widgets.reposition = function (location) { + $('body [has-widget-class]').each(function () { var $this = $(this); - if ($this.attr('no-widget-target') === location) { + if ($this.attr('has-widget-target') === location) { $this.removeClass(); - $this.addClass($this.attr('no-widget-class')); + $this.addClass($this.attr('has-widget-class')); } }); }; - ajaxify.widgets.render = function(template, url, callback) { + ajaxify.widgets.render = function (template, url, callback) { + callback = callback || function () {}; if (template.match(/^admin/)) { return callback(); } - var widgetLocations = ['sidebar', 'footer', 'header'], numLocations; + var widgetLocations = ['sidebar', 'footer', 'header']; - $('#content [widget-area]').each(function() { + $('#content [widget-area]').each(function () { var location = $(this).attr('widget-area'); if ($.inArray(location, widgetLocations) === -1) { widgetLocations.push(location); } }); - numLocations = widgetLocations.length; - - if (!numLocations) { - ajaxify.widgets.reposition(); - } - - function renderWidgets(locations) { - var areaDatas = []; - - $.get(RELATIVE_PATH + '/api/widgets/render' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), { - locations: locations, - template: template + '.tpl', - url: url - }, function(renderedAreas) { - for (var x=0; x<renderedAreas.length; ++x) { - var renderedWidgets = renderedAreas[x].widgets, - location = renderedAreas[x].location, - html = ''; - - for (var i=0; i<renderedWidgets.length; ++i) { - html += templates.parse(renderedWidgets[i].html, {}); - } - - var area = $('#content [widget-area="' + location + '"]'); + $.get(config.relative_path + '/api/widgets/render' + (config['cache-buster'] ? '?v=' + config['cache-buster'] : ''), { + locations: widgetLocations, + template: template + '.tpl', + url: url, + isMobile: utils.isMobile() + }, function (renderedAreas) { + for (var x = 0; x < renderedAreas.length; ++x) { + var renderedWidgets = renderedAreas[x].widgets; + var location = renderedAreas[x].location; + var html = ''; + + for (var i = 0; i < renderedWidgets.length; ++i) { + html += templates.parse(renderedWidgets[i].html, {}); + } - if (!area.length && window.location.pathname.indexOf('/admin') === -1 && renderedWidgets.length) { - if (location === 'footer' && !$('#content [widget-area="footer"]').length) { - $('#content').append($('<div class="row"><div widget-area="footer" class="col-xs-12"></div></div>')); - } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) { - if ($('[component="account/cover"]').length) { - $('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); - } else if ($('[component="groups/cover"]').length) { - $('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); - } else { - $('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); - } - } else if (location === 'header' && !$('#content [widget-area="header"]').length) { - $('#content').prepend($('<div class="row"><div widget-area="header" class="col-xs-12"></div></div>')); + var area = $('#content [widget-area="' + location + '"]'); + + if (!area.length && window.location.pathname.indexOf('/admin') === -1 && renderedWidgets.length) { + if (location === 'footer' && !$('#content [widget-area="footer"]').length) { + $('#content').append($('<div class="row"><div widget-area="footer" class="col-xs-12"></div></div>')); + } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) { + if ($('[component="account/cover"]').length) { + $('[component="account/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); + } else if ($('[component="groups/cover"]').length) { + $('[component="groups/cover"]').nextAll().wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); + } else { + $('#content > *').wrapAll($('<div class="row"><div class="col-lg-9 col-xs-12"></div><div widget-area="sidebar" class="col-lg-3 col-xs-12"></div></div></div>')); } - - area = $('#content [widget-area="' + location + '"]'); + } else if (location === 'header' && !$('#content [widget-area="header"]').length) { + $('#content').prepend($('<div class="row"><div widget-area="header" class="col-xs-12"></div></div>')); } - area.html(html); - - if (!renderedWidgets.length) { - area.addClass('hidden'); - ajaxify.widgets.reposition(location); - } + area = $('#content [widget-area="' + location + '"]'); } - var widgetAreas = $('#content [widget-area]'); - widgetAreas.find('img:not(.not-responsive)').addClass('img-responsive'); - widgetAreas.find('.timeago').timeago(); - widgetAreas.find('img[title].teaser-pic,img[title].user-img').each(function() { - $(this).tooltip({ - placement: 'top', - title: $(this).attr('title') - }); - }); - $(window).trigger('action:widgets.loaded', {}); + area.html(html); - if (typeof callback === 'function') { - callback(); + if (renderedWidgets.length) { + area.removeClass('hidden'); + ajaxify.widgets.reposition(location); } + } + + var widgetAreas = $('#content [widget-area]'); + widgetAreas.find('img:not(.not-responsive)').addClass('img-responsive'); + widgetAreas.find('.timeago').timeago(); + widgetAreas.find('img[title].teaser-pic,img[title].user-img').each(function () { + $(this).tooltip({ + placement: 'top', + title: $(this).attr('title') + }); }); - } + $(window).trigger('action:widgets.loaded', {}); - renderWidgets(widgetLocations); + callback(renderedAreas); + }); }; }(ajaxify || {})); diff --git a/public/vendor/autosize.js b/public/vendor/autosize.js deleted file mode 100644 index aa51c70572..0000000000 --- a/public/vendor/autosize.js +++ /dev/null @@ -1,254 +0,0 @@ -/*! - Autosize 3.0.15 - license: MIT - http://www.jacklmoore.com/autosize -*/ -(function (global, factory) { - if (typeof define === 'function' && define.amd) { - define('autosize', ['exports', 'module'], factory); - } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { - factory(exports, module); - } else { - var mod = { - exports: {} - }; - factory(mod.exports, mod); - global.autosize = mod.exports; - } -})(this, function (exports, module) { - 'use strict'; - - var set = typeof Set === 'function' ? new Set() : (function () { - var list = []; - - return { - has: function has(key) { - return Boolean(list.indexOf(key) > -1); - }, - add: function add(key) { - list.push(key); - }, - 'delete': function _delete(key) { - list.splice(list.indexOf(key), 1); - } }; - })(); - - var createEvent = function createEvent(name) { - return new Event(name); - }; - try { - new Event('test'); - } catch (e) { - // IE does not support `new Event()` - createEvent = function (name) { - var evt = document.createEvent('Event'); - evt.initEvent(name, true, false); - return evt; - }; - } - - function assign(ta) { - var _ref = arguments[1] === undefined ? {} : arguments[1]; - - var _ref$setOverflowX = _ref.setOverflowX; - var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX; - var _ref$setOverflowY = _ref.setOverflowY; - var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY; - - if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return; - - var heightOffset = null; - var overflowY = null; - var clientWidth = ta.clientWidth; - - function init() { - var style = window.getComputedStyle(ta, null); - - overflowY = style.overflowY; - - if (style.resize === 'vertical') { - ta.style.resize = 'none'; - } else if (style.resize === 'both') { - ta.style.resize = 'horizontal'; - } - - if (style.boxSizing === 'content-box') { - heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)); - } else { - heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); - } - // Fix when a textarea is not on document body and heightOffset is Not a Number - if (isNaN(heightOffset)) { - heightOffset = 0; - } - - update(); - } - - function changeOverflow(value) { - { - // Chrome/Safari-specific fix: - // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space - // made available by removing the scrollbar. The following forces the necessary text reflow. - var width = ta.style.width; - ta.style.width = '0px'; - // Force reflow: - /* jshint ignore:start */ - ta.offsetWidth; - /* jshint ignore:end */ - ta.style.width = width; - } - - overflowY = value; - - if (setOverflowY) { - ta.style.overflowY = value; - } - - resize(); - } - - function resize() { - var htmlTop = window.pageYOffset; - var bodyTop = document.body.scrollTop; - var originalHeight = ta.style.height; - - ta.style.height = 'auto'; - - var endHeight = ta.scrollHeight + heightOffset; - - if (ta.scrollHeight === 0) { - // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM. - ta.style.height = originalHeight; - return; - } - - ta.style.height = endHeight + 'px'; - - // used to check if an update is actually necessary on window.resize - clientWidth = ta.clientWidth; - - // prevents scroll-position jumping - document.documentElement.scrollTop = htmlTop; - document.body.scrollTop = bodyTop; - } - - function update() { - var startHeight = ta.style.height; - - resize(); - - var style = window.getComputedStyle(ta, null); - - if (style.height !== ta.style.height) { - if (overflowY !== 'visible') { - changeOverflow('visible'); - } - } else { - if (overflowY !== 'hidden') { - changeOverflow('hidden'); - } - } - - if (startHeight !== ta.style.height) { - var evt = createEvent('autosize:resized'); - ta.dispatchEvent(evt); - } - } - - var pageResize = function pageResize() { - if (ta.clientWidth !== clientWidth) { - update(); - } - }; - - var destroy = (function (style) { - window.removeEventListener('resize', pageResize, false); - ta.removeEventListener('input', update, false); - ta.removeEventListener('keyup', update, false); - ta.removeEventListener('autosize:destroy', destroy, false); - ta.removeEventListener('autosize:update', update, false); - set['delete'](ta); - - Object.keys(style).forEach(function (key) { - ta.style[key] = style[key]; - }); - }).bind(ta, { - height: ta.style.height, - resize: ta.style.resize, - overflowY: ta.style.overflowY, - overflowX: ta.style.overflowX, - wordWrap: ta.style.wordWrap }); - - ta.addEventListener('autosize:destroy', destroy, false); - - // IE9 does not fire onpropertychange or oninput for deletions, - // so binding to onkeyup to catch most of those events. - // There is no way that I know of to detect something like 'cut' in IE9. - if ('onpropertychange' in ta && 'oninput' in ta) { - ta.addEventListener('keyup', update, false); - } - - window.addEventListener('resize', pageResize, false); - ta.addEventListener('input', update, false); - ta.addEventListener('autosize:update', update, false); - set.add(ta); - - if (setOverflowX) { - ta.style.overflowX = 'hidden'; - ta.style.wordWrap = 'break-word'; - } - - init(); - } - - function destroy(ta) { - if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return; - var evt = createEvent('autosize:destroy'); - ta.dispatchEvent(evt); - } - - function update(ta) { - if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return; - var evt = createEvent('autosize:update'); - ta.dispatchEvent(evt); - } - - var autosize = null; - - // Do nothing in Node.js environment and IE8 (or lower) - if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') { - autosize = function (el) { - return el; - }; - autosize.destroy = function (el) { - return el; - }; - autosize.update = function (el) { - return el; - }; - } else { - autosize = function (el, options) { - if (el) { - Array.prototype.forEach.call(el.length ? el : [el], function (x) { - return assign(x, options); - }); - } - return el; - }; - autosize.destroy = function (el) { - if (el) { - Array.prototype.forEach.call(el.length ? el : [el], destroy); - } - return el; - }; - autosize.update = function (el) { - if (el) { - Array.prototype.forEach.call(el.length ? el : [el], update); - } - return el; - }; - } - - module.exports = autosize; -}); \ No newline at end of file diff --git a/public/vendor/bootstrap/js/bootstrap.js b/public/vendor/bootstrap/js/bootstrap.js new file mode 100644 index 0000000000..8a2e99a535 --- /dev/null +++ b/public/vendor/bootstrap/js/bootstrap.js @@ -0,0 +1,2377 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under the MIT license + */ + +if (typeof jQuery === 'undefined') { + throw new Error('Bootstrap\'s JavaScript requires jQuery') +} + ++function ($) { + 'use strict'; + var version = $.fn.jquery.split(' ')[0].split('.') + if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { + throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') + } +}(jQuery); + +/* ======================================================================== + * Bootstrap: transition.js v3.3.7 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.3.7 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.VERSION = '3.3.7' + + Alert.TRANSITION_DURATION = 150 + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector === '#' ? [] : selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.closest('.alert') + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + // detach from parent, fire event then clean up data + $parent.detach().trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one('bsTransitionEnd', removeElement) + .emulateTransitionEnd(Alert.TRANSITION_DURATION) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.alert + + $.fn.alert = Plugin + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.3.7 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.3.7' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state += 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + $el[val](data[state] == null ? this.options[state] : data[state]) + + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d).prop(d, true) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d).prop(d, false) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked')) changed = false + $parent.find('.active').removeClass('active') + this.$element.addClass('active') + } else if ($input.prop('type') == 'checkbox') { + if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false + this.$element.toggleClass('active') + } + $input.prop('checked', this.$element.hasClass('active')) + if (changed) $input.trigger('change') + } else { + this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + this.$element.toggleClass('active') + } + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document) + .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target).closest('.btn') + Plugin.call($btn, 'toggle') + if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { + // Prevent double click on radios, and the double selections (so cancellation) on checkboxes + e.preventDefault() + // The target component still receive the focus + if ($btn.is('input,button')) $btn.trigger('focus') + else $btn.find('input:visible,button:visible').first().trigger('focus') + } + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { + $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.3.7 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = null + this.sliding = null + this.interval = null + this.$active = null + this.$items = null + + this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) + + this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.3.7' + + Carousel.TRANSITION_DURATION = 600 + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true, + keyboard: true + } + + Carousel.prototype.keydown = function (e) { + if (/input|textarea/i.test(e.target.tagName)) return + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.getItemForDirection = function (direction, active) { + var activeIndex = this.getItemIndex(active) + var willWrap = (direction == 'prev' && activeIndex === 0) + || (direction == 'next' && activeIndex == (this.$items.length - 1)) + if (willWrap && !this.options.wrap) return active + var delta = direction == 'prev' ? -1 : 1 + var itemIndex = (activeIndex + delta) % this.$items.length + return this.$items.eq(itemIndex) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || this.getItemForDirection(type, $active) + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var that = this + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd(Carousel.TRANSITION_DURATION) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + var clickHandler = function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + } + + $(document) + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.3.7 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + +/* jshint latedef: false */ + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + + '[data-toggle="collapse"][data-target="#' + element.id + '"]') + this.transitioning = null + + if (this.options.parent) { + this.$parent = this.getParent() + } else { + this.addAriaAndCollapsedClass(this.$element, this.$trigger) + } + + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.3.7' + + Collapse.TRANSITION_DURATION = 350 + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var activesData + var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') + + if (actives && actives.length) { + activesData = actives.data('bs.collapse') + if (activesData && activesData.transitioning) return + } + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + if (actives && actives.length) { + Plugin.call(actives, 'hide') + activesData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + .attr('aria-expanded', true) + + this.$trigger + .removeClass('collapsed') + .attr('aria-expanded', true) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse in') + .attr('aria-expanded', false) + + this.$trigger + .addClass('collapsed') + .attr('aria-expanded', false) + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .removeClass('collapsing') + .addClass('collapse') + .trigger('hidden.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + Collapse.prototype.getParent = function () { + return $(this.options.parent) + .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') + .each($.proxy(function (i, element) { + var $element = $(element) + this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) + }, this)) + .end() + } + + Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { + var isOpen = $element.hasClass('in') + + $element.attr('aria-expanded', isOpen) + $trigger + .toggleClass('collapsed', !isOpen) + .attr('aria-expanded', isOpen) + } + + function getTargetFromTrigger($trigger) { + var href + var target = $trigger.attr('data-target') + || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + + return $(target) + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var $this = $(this) + + if (!$this.attr('data-target')) e.preventDefault() + + var $target = getTargetFromTrigger($this) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + + Plugin.call($target, option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.7 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.7' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.3.7 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.VERSION = '3.3.7' + + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (document !== e.target && + this.$element[0] !== e.target && + !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() + + } else if (callback) { + callback() + } + } + + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() + } + + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight + + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' + }) + } + + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) + } + + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) + }) + Plugin.call($target, option, this) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.3.7 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = null + this.options = null + this.enabled = null + this.timeout = null + this.hoverState = null + this.$element = null + this.inState = null + + this.init('tooltip', element, options) + } + + Tooltip.VERSION = '3.3.7' + + Tooltip.TRANSITION_DURATION = 150 + + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false, + viewport: { + selector: 'body', + padding: 0 + } + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) + this.inState = { click: false, hover: false, focus: false } + + if (this.$element[0] instanceof document.constructor && !this.options.selector) { + throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') + } + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true + } + + if (self.tip().hasClass('in') || self.hoverState == 'in') { + self.hoverState = 'in' + return + } + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.isInStateTrue = function () { + for (var key in this.inState) { + if (this.inState[key]) return true + } + + return false + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false + } + + if (self.isInStateTrue()) return + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) + if (e.isDefaultPrevented() || !inDom) return + var that = this + + var $tip = this.tip() + + var tipId = this.getUID(this.type) + + this.setContent() + $tip.attr('id', tipId) + this.$element.attr('aria-describedby', tipId) + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + .data('bs.' + this.type, this) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + this.$element.trigger('inserted.bs.' + this.type) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var orgPlacement = placement + var viewportDim = this.getPosition(this.$viewport) + + placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + + var complete = function () { + var prevHoverState = that.hoverState + that.$element.trigger('shown.bs.' + that.type) + that.hoverState = null + + if (prevHoverState == 'out') that.leave(that) + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + } + } + + Tooltip.prototype.applyPlacement = function (offset, placement) { + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top += marginTop + offset.left += marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) + } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + } + + var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) offset.left += delta.left + else offset.top += delta.top + + var isVertical = /top|bottom/.test(placement) + var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' + + $tip.offset(offset) + this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { + this.arrow() + .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') + .css(isVertical ? 'top' : 'left', '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function (callback) { + var that = this + var $tip = $(this.$tip) + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary. + that.$element + .removeAttr('aria-describedby') + .trigger('hidden.bs.' + that.type) + } + callback && callback() + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && $tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function ($element) { + $element = $element || this.$element + + var el = $element[0] + var isBody = el.tagName == 'BODY' + + var elRect = el.getBoundingClientRect() + if (elRect.width == null) { + // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 + elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) + } + var isSvg = window.SVGElement && el instanceof window.SVGElement + // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3. + // See https://github.com/twbs/bootstrap/issues/20280 + var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset()) + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } + var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null + + return $.extend({}, elRect, scroll, outerDims, elOffset) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + + } + + Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + if (!this.$viewport) return delta + + var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 + var viewportDimensions = this.getPosition(this.$viewport) + + if (/right|left/.test(placement)) { + var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + } else { + var leftEdgeOffset = pos.left - viewportPadding + var rightEdgeOffset = pos.left + viewportPadding + actualWidth + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } + } + + return delta + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.getUID = function (prefix) { + do prefix += ~~(Math.random() * 1000000) + while (document.getElementById(prefix)) + return prefix + } + + Tooltip.prototype.tip = function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + if (this.$tip.length != 1) { + throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') + } + } + return this.$tip + } + + Tooltip.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = this + if (e) { + self = $(e.currentTarget).data('bs.' + this.type) + if (!self) { + self = new this.constructor(e.currentTarget, this.getDelegateOptions()) + $(e.currentTarget).data('bs.' + this.type, self) + } + } + + if (e) { + self.inState.click = !self.inState.click + if (self.isInStateTrue()) self.enter(self) + else self.leave(self) + } else { + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + } + + Tooltip.prototype.destroy = function () { + var that = this + clearTimeout(this.timeout) + this.hide(function () { + that.$element.off('.' + that.type).removeData('bs.' + that.type) + if (that.$tip) { + that.$tip.detach() + } + that.$tip = null + that.$arrow = null + that.$viewport = null + that.$element = null + }) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tooltip + + $.fn.tooltip = Plugin + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.3.7 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.VERSION = '3.3.7' + + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.arrow')) + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.popover + + $.fn.popover = Plugin + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.3.7 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + this.$body = $(document.body) + this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target || '') + ' .nav li > a' + this.offsets = [] + this.targets = [] + this.activeTarget = null + this.scrollHeight = 0 + + this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) + this.refresh() + this.process() + } + + ScrollSpy.VERSION = '3.3.7' + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.getScrollHeight = function () { + return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) + } + + ScrollSpy.prototype.refresh = function () { + var that = this + var offsetMethod = 'offset' + var offsetBase = 0 + + this.offsets = [] + this.targets = [] + this.scrollHeight = this.getScrollHeight() + + if (!$.isWindow(this.$scrollElement[0])) { + offsetMethod = 'position' + offsetBase = this.$scrollElement.scrollTop() + } + + this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) + + return ($href + && $href.length + && $href.is(':visible') + && [[$href[offsetMethod]().top + offsetBase, href]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + that.offsets.push(this[0]) + that.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.getScrollHeight() + var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (this.scrollHeight != scrollHeight) { + this.refresh() + } + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) + } + + if (activeTarget && scrollTop < offsets[0]) { + this.activeTarget = null + return this.clear() + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) + && this.activate(targets[i]) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + this.clear() + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate.bs.scrollspy') + } + + ScrollSpy.prototype.clear = function () { + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.scrollspy + + $.fn.scrollspy = Plugin + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load.bs.scrollspy.data-api', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + Plugin.call($spy, $spy.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.3.7 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + // jscs:disable requireDollarBeforejQueryAssignment + this.element = $(element) + // jscs:enable requireDollarBeforejQueryAssignment + } + + Tab.VERSION = '3.3.7' + + Tab.TRANSITION_DURATION = 150 + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var $previous = $ul.find('.active:last a') + var hideEvent = $.Event('hide.bs.tab', { + relatedTarget: $this[0] + }) + var showEvent = $.Event('show.bs.tab', { + relatedTarget: $previous[0] + }) + + $previous.trigger(hideEvent) + $this.trigger(showEvent) + + if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.closest('li'), $ul) + this.activate($target, $target.parent(), function () { + $previous.trigger({ + type: 'hidden.bs.tab', + relatedTarget: $this[0] + }) + $this.trigger({ + type: 'shown.bs.tab', + relatedTarget: $previous[0] + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', false) + + element + .addClass('active') + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu').length) { + element + .closest('li.dropdown') + .addClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + } + + callback && callback() + } + + $active.length && transition ? + $active + .one('bsTransitionEnd', next) + .emulateTransitionEnd(Tab.TRANSITION_DURATION) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tab + + $.fn.tab = Plugin + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + var clickHandler = function (e) { + e.preventDefault() + Plugin.call($(this), 'show') + } + + $(document) + .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) + .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: affix.js v3.3.7 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + + this.$target = $(this.options.target) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = null + this.unpin = null + this.pinnedOffset = null + + this.checkPosition() + } + + Affix.VERSION = '3.3.7' + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0, + target: window + } + + Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + var targetHeight = this.$target.height() + + if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false + + if (this.affixed == 'bottom') { + if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' + return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' + } + + var initializing = this.affixed == null + var colliderTop = initializing ? scrollTop : position.top + var colliderHeight = initializing ? targetHeight : height + + if (offsetTop != null && scrollTop <= offsetTop) return 'top' + if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' + + return false + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var height = this.$element.height() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + var scrollHeight = Math.max($(document).height(), $(document.body).height()) + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) + + if (this.affixed != affix) { + if (this.unpin != null) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null + + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') + } + + if (affix == 'bottom') { + this.$element.offset({ + top: scrollHeight - height - offsetBottom + }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.affix + + $.fn.affix = Plugin + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom + if (data.offsetTop != null) data.offset.top = data.offsetTop + + Plugin.call($spy, data) + }) + }) + +}(jQuery); diff --git a/public/vendor/bootstrap/js/bootstrap.min.js b/public/vendor/bootstrap/js/bootstrap.min.js deleted file mode 100644 index c8f82e592a..0000000000 --- a/public/vendor/bootstrap/js/bootstrap.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v3.3.4 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27|32)/.test(b.which)&&!/input|textarea/i.test(b.target.tagName)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g&&27!=b.which||g&&27==b.which)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(b.target);38==b.which&&j>0&&j--,40==b.which&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f,g.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="menu"]',g.prototype.keydown).on("keydown.bs.dropdown.data-api",'[role="listbox"]',g.prototype.keydown)}(jQuery),+function(a){"use strict";function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=300,c.BACKDROP_TRANSITION_DURATION=150,c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var d=this,e=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){d.$element.one("mouseup.dismiss.bs.modal",function(b){a(b.target).is(d.$element)&&(d.ignoreBackdropClick=!0)})}),this.backdrop(function(){var e=a.support.transition&&d.$element.hasClass("fade");d.$element.parent().length||d.$element.appendTo(d.$body),d.$element.show().scrollTop(0),d.adjustDialog(),e&&d.$element[0].offsetWidth,d.$element.addClass("in").attr("aria-hidden",!1),d.enforceFocus();var f=a.Event("shown.bs.modal",{relatedTarget:b});e?d.$dialog.one("bsTransitionEnd",function(){d.$element.trigger("focus").trigger(f)}).emulateTransitionEnd(c.TRANSITION_DURATION):d.$element.trigger("focus").trigger(f)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(c.TRANSITION_DURATION):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},c.prototype.resize=function(){this.isShown?a(window).on("resize.bs.modal",a.proxy(this.handleUpdate,this)):a(window).off("resize.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$body.removeClass("modal-open"),a.resetAdjustments(),a.resetScrollbar(),a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var d=this,e=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var f=a.support.transition&&e;if(this.$backdrop=a('<div class="modal-backdrop '+e+'" />').appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth<a,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.init("tooltip",a,b)};c.VERSION="3.3.4",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-m<p.top?"bottom":"right"==h&&k.right+l>p.width?"left":"left"==h&&k.left-l<p.left?"right":h,f.removeClass(n).addClass(h)}var q=this.getCalculatedOffset(h,k,l,m);this.applyPlacement(q,h);var r=function(){var a=e.hoverState;e.$element.trigger("shown.bs."+e.type),e.hoverState=null,"out"==a&&e.leave(e)};a.support.transition&&this.$tip.hasClass("fade")?f.one("bsTransitionEnd",r).emulateTransitionEnd(c.TRANSITION_DURATION):r()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top=b.top+g,b.left=b.left+h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=/top|bottom/.test(c),m=l?2*k.left-e+i:2*k.top-f+j,n=l?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(m,d[0][n],l)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c?"left":"top",50*(1-a/b)+"%").css(c?"top":"left","")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},c.prototype.hide=function(b){function d(){"in"!=e.hoverState&&f.detach(),e.$element.removeAttr("aria-describedby").trigger("hidden.bs."+e.type),b&&b()}var e=this,f=a(this.$tip),g=a.Event("hide.bs."+this.type);return this.$element.trigger(g),g.isDefaultPrevented()?void 0:(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",d).emulateTransitionEnd(c.TRANSITION_DURATION):d(),this.hoverState=null,this)},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName,e=c.getBoundingClientRect();null==e.width&&(e=a.extend({},e,{width:e.right-e.left,height:e.bottom-e.top}));var f=d?{top:0,left:0}:b.offset(),g={scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop()},h=d?{width:a(window).width(),height:a(window).height()}:null;return a.extend({},e,g,h,f)},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<e[0])return this.activeTarget=null,this.clear();for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(void 0===e[a+1]||b<e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.4",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){ -var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file diff --git a/public/vendor/buzz/buzz.js b/public/vendor/buzz/buzz.js new file mode 100644 index 0000000000..eae98a9dfe --- /dev/null +++ b/public/vendor/buzz/buzz.js @@ -0,0 +1,11 @@ + // ---------------------------------------------------------------------------- + // Buzz, a Javascript HTML5 Audio library + // v1.1.10 - Built 2015-04-20 13:05 + // Licensed under the MIT license. + // http://buzz.jaysalvat.com/ + // ---------------------------------------------------------------------------- + // Copyright (C) 2010-2015 Jay Salvat + // http://jaysalvat.com/ + // ---------------------------------------------------------------------------- + +(function(t,e){"use strict";"undefined"!=typeof module&&module.exports?module.exports=e():"function"==typeof define&&define.amd?define([],e):t.buzz=e()})(this,function(){"use strict";var t=window.AudioContext||window.webkitAudioContext,e={defaults:{autoplay:!1,duration:5e3,formats:[],loop:!1,placeholder:"--",preload:"metadata",volume:80,webAudioApi:!1,document:window.document},types:{mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",aac:"audio/aac",m4a:"audio/x-m4a"},sounds:[],el:document.createElement("audio"),getAudioContext:function(){if(void 0===this.audioCtx)try{this.audioCtx=t?new t:null}catch(e){this.audioCtx=null}return this.audioCtx},sound:function(t,n){function i(t){for(var e=[],n=t.length-1,i=0;n>=i;i++)e.push({start:t.start(i),end:t.end(i)});return e}function u(t){return t.split(".").pop()}n=n||{};var s=n.document||e.defaults.document,r=0,o=[],a={},h=e.isSupported();if(this.load=function(){return h?(this.sound.load(),this):this},this.play=function(){return h?(this.sound.play(),this):this},this.togglePlay=function(){return h?(this.sound.paused?this.sound.play():this.sound.pause(),this):this},this.pause=function(){return h?(this.sound.pause(),this):this},this.isPaused=function(){return h?this.sound.paused:null},this.stop=function(){return h?(this.setTime(0),this.sound.pause(),this):this},this.isEnded=function(){return h?this.sound.ended:null},this.loop=function(){return h?(this.sound.loop="loop",this.bind("ended.buzzloop",function(){this.currentTime=0,this.play()}),this):this},this.unloop=function(){return h?(this.sound.removeAttribute("loop"),this.unbind("ended.buzzloop"),this):this},this.mute=function(){return h?(this.sound.muted=!0,this):this},this.unmute=function(){return h?(this.sound.muted=!1,this):this},this.toggleMute=function(){return h?(this.sound.muted=!this.sound.muted,this):this},this.isMuted=function(){return h?this.sound.muted:null},this.setVolume=function(t){return h?(0>t&&(t=0),t>100&&(t=100),this.volume=t,this.sound.volume=t/100,this):this},this.getVolume=function(){return h?this.volume:this},this.increaseVolume=function(t){return this.setVolume(this.volume+(t||1))},this.decreaseVolume=function(t){return this.setVolume(this.volume-(t||1))},this.setTime=function(t){if(!h)return this;var e=!0;return this.whenReady(function(){e===!0&&(e=!1,this.sound.currentTime=t)}),this},this.getTime=function(){if(!h)return null;var t=Math.round(100*this.sound.currentTime)/100;return isNaN(t)?e.defaults.placeholder:t},this.setPercent=function(t){return h?this.setTime(e.fromPercent(t,this.sound.duration)):this},this.getPercent=function(){if(!h)return null;var t=Math.round(e.toPercent(this.sound.currentTime,this.sound.duration));return isNaN(t)?e.defaults.placeholder:t},this.setSpeed=function(t){return h?(this.sound.playbackRate=t,this):this},this.getSpeed=function(){return h?this.sound.playbackRate:null},this.getDuration=function(){if(!h)return null;var t=Math.round(100*this.sound.duration)/100;return isNaN(t)?e.defaults.placeholder:t},this.getPlayed=function(){return h?i(this.sound.played):null},this.getBuffered=function(){return h?i(this.sound.buffered):null},this.getSeekable=function(){return h?i(this.sound.seekable):null},this.getErrorCode=function(){return h&&this.sound.error?this.sound.error.code:0},this.getErrorMessage=function(){if(!h)return null;switch(this.getErrorCode()){case 1:return"MEDIA_ERR_ABORTED";case 2:return"MEDIA_ERR_NETWORK";case 3:return"MEDIA_ERR_DECODE";case 4:return"MEDIA_ERR_SRC_NOT_SUPPORTED";default:return null}},this.getStateCode=function(){return h?this.sound.readyState:null},this.getStateMessage=function(){if(!h)return null;switch(this.getStateCode()){case 0:return"HAVE_NOTHING";case 1:return"HAVE_METADATA";case 2:return"HAVE_CURRENT_DATA";case 3:return"HAVE_FUTURE_DATA";case 4:return"HAVE_ENOUGH_DATA";default:return null}},this.getNetworkStateCode=function(){return h?this.sound.networkState:null},this.getNetworkStateMessage=function(){if(!h)return null;switch(this.getNetworkStateCode()){case 0:return"NETWORK_EMPTY";case 1:return"NETWORK_IDLE";case 2:return"NETWORK_LOADING";case 3:return"NETWORK_NO_SOURCE";default:return null}},this.set=function(t,e){return h?(this.sound[t]=e,this):this},this.get=function(t){return h?t?this.sound[t]:this.sound:null},this.bind=function(t,e){if(!h)return this;t=t.split(" ");for(var n=this,i=function(t){e.call(n,t)},u=0;t.length>u;u++){var s=t[u],r=s;s=r.split(".")[0],o.push({idx:r,func:i}),this.sound.addEventListener(s,i,!0)}return this},this.unbind=function(t){if(!h)return this;t=t.split(" ");for(var e=0;t.length>e;e++)for(var n=t[e],i=n.split(".")[0],u=0;o.length>u;u++){var s=o[u].idx.split(".");(o[u].idx===n||s[1]&&s[1]===n.replace(".",""))&&(this.sound.removeEventListener(i,o[u].func,!0),o.splice(u,1))}return this},this.bindOnce=function(t,e){if(!h)return this;var n=this;return a[r++]=!1,this.bind(t+"."+r,function(){a[r]||(a[r]=!0,e.call(n)),n.unbind(t+"."+r)}),this},this.trigger=function(t,e){if(!h)return this;t=t.split(" ");for(var n=0;t.length>n;n++)for(var i=t[n],u=0;o.length>u;u++){var r=o[u].idx.split(".");if(o[u].idx===i||r[0]&&r[0]===i.replace(".","")){var a=s.createEvent("HTMLEvents");a.initEvent(r[0],!1,!0),a.originalEvent=e,this.sound.dispatchEvent(a)}}return this},this.fadeTo=function(t,n,i){function u(){setTimeout(function(){t>s&&t>o.volume?(o.setVolume(o.volume+=1),u()):s>t&&o.volume>t?(o.setVolume(o.volume-=1),u()):i instanceof Function&&i.apply(o)},r)}if(!h)return this;n instanceof Function?(i=n,n=e.defaults.duration):n=n||e.defaults.duration;var s=this.volume,r=n/Math.abs(s-t),o=this;return this.play(),this.whenReady(function(){u()}),this},this.fadeIn=function(t,e){return h?this.setVolume(0).fadeTo(100,t,e):this},this.fadeOut=function(t,e){return h?this.fadeTo(0,t,e):this},this.fadeWith=function(t,e){return h?(this.fadeOut(e,function(){this.stop()}),t.play().fadeIn(e),this):this},this.whenReady=function(t){if(!h)return null;var e=this;0===this.sound.readyState?this.bind("canplay.buzzwhenready",function(){t.call(e)}):t.call(e)},this.addSource=function(t){var n=this,i=s.createElement("source");return i.src=t,e.types[u(t)]&&(i.type=e.types[u(t)]),this.sound.appendChild(i),i.addEventListener("error",function(t){n.trigger("sourceerror",t)}),i},h&&t){for(var d in e.defaults)e.defaults.hasOwnProperty(d)&&void 0===n[d]&&(n[d]=e.defaults[d]);if(this.sound=s.createElement("audio"),n.webAudioApi){var l=e.getAudioContext();l&&(this.source=l.createMediaElementSource(this.sound),this.source.connect(l.destination))}if(t instanceof Array)for(var c in t)t.hasOwnProperty(c)&&this.addSource(t[c]);else if(n.formats.length)for(var f in n.formats)n.formats.hasOwnProperty(f)&&this.addSource(t+"."+n.formats[f]);else this.addSource(t);n.loop&&this.loop(),n.autoplay&&(this.sound.autoplay="autoplay"),this.sound.preload=n.preload===!0?"auto":n.preload===!1?"none":n.preload,this.setVolume(n.volume),e.sounds.push(this)}},group:function(t){function e(){for(var e=n(null,arguments),i=e.shift(),u=0;t.length>u;u++)t[u][i].apply(t[u],e)}function n(t,e){return t instanceof Array?t:Array.prototype.slice.call(e)}t=n(t,arguments),this.getSounds=function(){return t},this.add=function(e){e=n(e,arguments);for(var i=0;e.length>i;i++)t.push(e[i])},this.remove=function(e){e=n(e,arguments);for(var i=0;e.length>i;i++)for(var u=0;t.length>u;u++)if(t[u]===e[i]){t.splice(u,1);break}},this.load=function(){return e("load"),this},this.play=function(){return e("play"),this},this.togglePlay=function(){return e("togglePlay"),this},this.pause=function(t){return e("pause",t),this},this.stop=function(){return e("stop"),this},this.mute=function(){return e("mute"),this},this.unmute=function(){return e("unmute"),this},this.toggleMute=function(){return e("toggleMute"),this},this.setVolume=function(t){return e("setVolume",t),this},this.increaseVolume=function(t){return e("increaseVolume",t),this},this.decreaseVolume=function(t){return e("decreaseVolume",t),this},this.loop=function(){return e("loop"),this},this.unloop=function(){return e("unloop"),this},this.setSpeed=function(t){return e("setSpeed",t),this},this.setTime=function(t){return e("setTime",t),this},this.set=function(t,n){return e("set",t,n),this},this.bind=function(t,n){return e("bind",t,n),this},this.unbind=function(t){return e("unbind",t),this},this.bindOnce=function(t,n){return e("bindOnce",t,n),this},this.trigger=function(t){return e("trigger",t),this},this.fade=function(t,n,i,u){return e("fade",t,n,i,u),this},this.fadeIn=function(t,n){return e("fadeIn",t,n),this},this.fadeOut=function(t,n){return e("fadeOut",t,n),this}},all:function(){return new e.group(e.sounds)},isSupported:function(){return!!e.el.canPlayType},isOGGSupported:function(){return!!e.el.canPlayType&&e.el.canPlayType('audio/ogg; codecs="vorbis"')},isWAVSupported:function(){return!!e.el.canPlayType&&e.el.canPlayType('audio/wav; codecs="1"')},isMP3Supported:function(){return!!e.el.canPlayType&&e.el.canPlayType("audio/mpeg;")},isAACSupported:function(){return!!e.el.canPlayType&&(e.el.canPlayType("audio/x-m4a;")||e.el.canPlayType("audio/aac;"))},toTimer:function(t,e){var n,i,u;return n=Math.floor(t/3600),n=isNaN(n)?"--":n>=10?n:"0"+n,i=e?Math.floor(t/60%60):Math.floor(t/60),i=isNaN(i)?"--":i>=10?i:"0"+i,u=Math.floor(t%60),u=isNaN(u)?"--":u>=10?u:"0"+u,e?n+":"+i+":"+u:i+":"+u},fromTimer:function(t){var e=(""+t).split(":");return e&&3===e.length&&(t=3600*parseInt(e[0],10)+60*parseInt(e[1],10)+parseInt(e[2],10)),e&&2===e.length&&(t=60*parseInt(e[0],10)+parseInt(e[1],10)),t},toPercent:function(t,e,n){var i=Math.pow(10,n||0);return Math.round(100*t/e*i)/i},fromPercent:function(t,e,n){var i=Math.pow(10,n||0);return Math.round(e/100*t*i)/i}};return e}); \ No newline at end of file diff --git a/public/vendor/buzz/buzz.min.js b/public/vendor/buzz/buzz.min.js deleted file mode 100644 index 556ce7663b..0000000000 --- a/public/vendor/buzz/buzz.min.js +++ /dev/null @@ -1,11 +0,0 @@ - // ---------------------------------------------------------------------------- - // Buzz, a Javascript HTML5 Audio library - // v1.1.0 - released 2013-08-15 13:18 - // Licensed under the MIT license. - // http://buzz.jaysalvat.com/ - // ---------------------------------------------------------------------------- - // Copyright (C) 2010-2013 Jay Salvat - // http://jaysalvat.com/ - // ---------------------------------------------------------------------------- - -(function(t,n,e){"undefined"!=typeof module&&module.exports?module.exports=e():"function"==typeof n.define&&n.define.amd?define(t,[],e):n[t]=e()})("buzz",this,function(){var t={defaults:{autoplay:!1,duration:5e3,formats:[],loop:!1,placeholder:"--",preload:"metadata",volume:80,document:document},types:{mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",aac:"audio/aac",m4a:"audio/x-m4a"},sounds:[],el:document.createElement("audio"),sound:function(n,e){function i(t){for(var n=[],e=t.length-1,i=0;e>=i;i++)n.push({start:t.start(i),end:t.end(i)});return n}function u(t){return t.split(".").pop()}function s(n,e){var i=r.createElement("source");i.src=e,t.types[u(e)]&&(i.type=t.types[u(e)]),n.appendChild(i)}e=e||{};var r=e.document||t.defaults.document,o=0,a=[],h={},l=t.isSupported();if(this.load=function(){return l?(this.sound.load(),this):this},this.play=function(){return l?(this.sound.play(),this):this},this.togglePlay=function(){return l?(this.sound.paused?this.sound.play():this.sound.pause(),this):this},this.pause=function(){return l?(this.sound.pause(),this):this},this.isPaused=function(){return l?this.sound.paused:null},this.stop=function(){return l?(this.setTime(0),this.sound.pause(),this):this},this.isEnded=function(){return l?this.sound.ended:null},this.loop=function(){return l?(this.sound.loop="loop",this.bind("ended.buzzloop",function(){this.currentTime=0,this.play()}),this):this},this.unloop=function(){return l?(this.sound.removeAttribute("loop"),this.unbind("ended.buzzloop"),this):this},this.mute=function(){return l?(this.sound.muted=!0,this):this},this.unmute=function(){return l?(this.sound.muted=!1,this):this},this.toggleMute=function(){return l?(this.sound.muted=!this.sound.muted,this):this},this.isMuted=function(){return l?this.sound.muted:null},this.setVolume=function(t){return l?(0>t&&(t=0),t>100&&(t=100),this.volume=t,this.sound.volume=t/100,this):this},this.getVolume=function(){return l?this.volume:this},this.increaseVolume=function(t){return this.setVolume(this.volume+(t||1))},this.decreaseVolume=function(t){return this.setVolume(this.volume-(t||1))},this.setTime=function(t){if(!l)return this;var n=!0;return this.whenReady(function(){n===!0&&(n=!1,this.sound.currentTime=t)}),this},this.getTime=function(){if(!l)return null;var n=Math.round(100*this.sound.currentTime)/100;return isNaN(n)?t.defaults.placeholder:n},this.setPercent=function(n){return l?this.setTime(t.fromPercent(n,this.sound.duration)):this},this.getPercent=function(){if(!l)return null;var n=Math.round(t.toPercent(this.sound.currentTime,this.sound.duration));return isNaN(n)?t.defaults.placeholder:n},this.setSpeed=function(t){return l?(this.sound.playbackRate=t,this):this},this.getSpeed=function(){return l?this.sound.playbackRate:null},this.getDuration=function(){if(!l)return null;var n=Math.round(100*this.sound.duration)/100;return isNaN(n)?t.defaults.placeholder:n},this.getPlayed=function(){return l?i(this.sound.played):null},this.getBuffered=function(){return l?i(this.sound.buffered):null},this.getSeekable=function(){return l?i(this.sound.seekable):null},this.getErrorCode=function(){return l&&this.sound.error?this.sound.error.code:0},this.getErrorMessage=function(){if(!l)return null;switch(this.getErrorCode()){case 1:return"MEDIA_ERR_ABORTED";case 2:return"MEDIA_ERR_NETWORK";case 3:return"MEDIA_ERR_DECODE";case 4:return"MEDIA_ERR_SRC_NOT_SUPPORTED";default:return null}},this.getStateCode=function(){return l?this.sound.readyState:null},this.getStateMessage=function(){if(!l)return null;switch(this.getStateCode()){case 0:return"HAVE_NOTHING";case 1:return"HAVE_METADATA";case 2:return"HAVE_CURRENT_DATA";case 3:return"HAVE_FUTURE_DATA";case 4:return"HAVE_ENOUGH_DATA";default:return null}},this.getNetworkStateCode=function(){return l?this.sound.networkState:null},this.getNetworkStateMessage=function(){if(!l)return null;switch(this.getNetworkStateCode()){case 0:return"NETWORK_EMPTY";case 1:return"NETWORK_IDLE";case 2:return"NETWORK_LOADING";case 3:return"NETWORK_NO_SOURCE";default:return null}},this.set=function(t,n){return l?(this.sound[t]=n,this):this},this.get=function(t){return l?t?this.sound[t]:this.sound:null},this.bind=function(t,n){if(!l)return this;t=t.split(" ");for(var e=this,i=function(t){n.call(e,t)},u=0;t.length>u;u++){var s=t[u],r=s;s=r.split(".")[0],a.push({idx:r,func:i}),this.sound.addEventListener(s,i,!0)}return this},this.unbind=function(t){if(!l)return this;t=t.split(" ");for(var n=0;t.length>n;n++)for(var e=t[n],i=e.split(".")[0],u=0;a.length>u;u++){var s=a[u].idx.split(".");(a[u].idx==e||s[1]&&s[1]==e.replace(".",""))&&(this.sound.removeEventListener(i,a[u].func,!0),a.splice(u,1))}return this},this.bindOnce=function(t,n){if(!l)return this;var e=this;return h[o++]=!1,this.bind(t+"."+o,function(){h[o]||(h[o]=!0,n.call(e)),e.unbind(t+"."+o)}),this},this.trigger=function(t){if(!l)return this;t=t.split(" ");for(var n=0;t.length>n;n++)for(var e=t[n],i=0;a.length>i;i++){var u=a[i].idx.split(".");if(a[i].idx==e||u[0]&&u[0]==e.replace(".","")){var s=r.createEvent("HTMLEvents");s.initEvent(u[0],!1,!0),this.sound.dispatchEvent(s)}}return this},this.fadeTo=function(n,e,i){function u(){setTimeout(function(){n>s&&n>o.volume?(o.setVolume(o.volume+=1),u()):s>n&&o.volume>n?(o.setVolume(o.volume-=1),u()):i instanceof Function&&i.apply(o)},r)}if(!l)return this;e instanceof Function?(i=e,e=t.defaults.duration):e=e||t.defaults.duration;var s=this.volume,r=e/Math.abs(s-n),o=this;return this.play(),this.whenReady(function(){u()}),this},this.fadeIn=function(t,n){return l?this.setVolume(0).fadeTo(100,t,n):this},this.fadeOut=function(t,n){return l?this.fadeTo(0,t,n):this},this.fadeWith=function(t,n){return l?(this.fadeOut(n,function(){this.stop()}),t.play().fadeIn(n),this):this},this.whenReady=function(t){if(!l)return null;var n=this;0===this.sound.readyState?this.bind("canplay.buzzwhenready",function(){t.call(n)}):t.call(n)},l&&n){for(var d in t.defaults)t.defaults.hasOwnProperty(d)&&(e[d]=e[d]||t.defaults[d]);if(this.sound=r.createElement("audio"),n instanceof Array)for(var c in n)n.hasOwnProperty(c)&&s(this.sound,n[c]);else if(e.formats.length)for(var f in e.formats)e.formats.hasOwnProperty(f)&&s(this.sound,n+"."+e.formats[f]);else s(this.sound,n);e.loop&&this.loop(),e.autoplay&&(this.sound.autoplay="autoplay"),this.sound.preload=e.preload===!0?"auto":e.preload===!1?"none":e.preload,this.setVolume(e.volume),t.sounds.push(this)}},group:function(t){function n(){for(var n=e(null,arguments),i=n.shift(),u=0;t.length>u;u++)t[u][i].apply(t[u],n)}function e(t,n){return t instanceof Array?t:Array.prototype.slice.call(n)}t=e(t,arguments),this.getSounds=function(){return t},this.add=function(n){n=e(n,arguments);for(var i=0;n.length>i;i++)t.push(n[i])},this.remove=function(n){n=e(n,arguments);for(var i=0;n.length>i;i++)for(var u=0;t.length>u;u++)if(t[u]==n[i]){t.splice(u,1);break}},this.load=function(){return n("load"),this},this.play=function(){return n("play"),this},this.togglePlay=function(){return n("togglePlay"),this},this.pause=function(t){return n("pause",t),this},this.stop=function(){return n("stop"),this},this.mute=function(){return n("mute"),this},this.unmute=function(){return n("unmute"),this},this.toggleMute=function(){return n("toggleMute"),this},this.setVolume=function(t){return n("setVolume",t),this},this.increaseVolume=function(t){return n("increaseVolume",t),this},this.decreaseVolume=function(t){return n("decreaseVolume",t),this},this.loop=function(){return n("loop"),this},this.unloop=function(){return n("unloop"),this},this.setTime=function(t){return n("setTime",t),this},this.set=function(t,e){return n("set",t,e),this},this.bind=function(t,e){return n("bind",t,e),this},this.unbind=function(t){return n("unbind",t),this},this.bindOnce=function(t,e){return n("bindOnce",t,e),this},this.trigger=function(t){return n("trigger",t),this},this.fade=function(t,e,i,u){return n("fade",t,e,i,u),this},this.fadeIn=function(t,e){return n("fadeIn",t,e),this},this.fadeOut=function(t,e){return n("fadeOut",t,e),this}},all:function(){return new t.group(t.sounds)},isSupported:function(){return!!t.el.canPlayType},isOGGSupported:function(){return!!t.el.canPlayType&&t.el.canPlayType('audio/ogg; codecs="vorbis"')},isWAVSupported:function(){return!!t.el.canPlayType&&t.el.canPlayType('audio/wav; codecs="1"')},isMP3Supported:function(){return!!t.el.canPlayType&&t.el.canPlayType("audio/mpeg;")},isAACSupported:function(){return!!t.el.canPlayType&&(t.el.canPlayType("audio/x-m4a;")||t.el.canPlayType("audio/aac;"))},toTimer:function(t,n){var e,i,u;return e=Math.floor(t/3600),e=isNaN(e)?"--":e>=10?e:"0"+e,i=n?Math.floor(t/60%60):Math.floor(t/60),i=isNaN(i)?"--":i>=10?i:"0"+i,u=Math.floor(t%60),u=isNaN(u)?"--":u>=10?u:"0"+u,n?e+":"+i+":"+u:i+":"+u},fromTimer:function(t){var n=(""+t).split(":");return n&&3==n.length&&(t=3600*parseInt(n[0],10)+60*parseInt(n[1],10)+parseInt(n[2],10)),n&&2==n.length&&(t=60*parseInt(n[0],10)+parseInt(n[1],10)),t},toPercent:function(t,n,e){var i=Math.pow(10,e||0);return Math.round(100*t/n*i)/i},fromPercent:function(t,n,e){var i=Math.pow(10,e||0);return Math.round(n/100*t*i)/i}};return t}); \ No newline at end of file diff --git a/public/vendor/chart.js/chart.min.js b/public/vendor/chart.js/chart.min.js deleted file mode 100644 index 626e6c3cdb..0000000000 --- a/public/vendor/chart.js/chart.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.1-beta.4 - * - * Copyright 2014 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;this.width=t.canvas.width,this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof t.define&&t.define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),x=s.radians=function(t){return t*(Math.PI/180)},S=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),x=Math.round(f/v);(x>a||a>2*x)&&!h;)if(x>a)v*=2,x=Math.round(f/v),x%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,x=Math.round(f/v)}else v/=2,x=Math.round(f/v);return h&&(x=o,v=f/x),{steps:x,stepValue:v,min:p,max:p+x*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),b=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-b.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=(s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=w(l):n.apply(o)};w(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},R=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),T=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},M=s.fontString=function(t,i,e){return i+" "+t+"px "+e},W=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},z=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return T(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:R(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),k(this,this.events),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;switch(t.fillStyle=this.fillColor,this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}z(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=M(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=W(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){z(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?W(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=W(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(x(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(x(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/(this.valuesCount-(this.offsetGridLines?0:1)),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+S(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+S(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*x(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'}; -i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/public/vendor/fontawesome/fonts/FontAwesome.otf b/public/vendor/fontawesome/fonts/FontAwesome.otf index 3ed7f8b48a..d4de13e832 100644 Binary files a/public/vendor/fontawesome/fonts/FontAwesome.otf and b/public/vendor/fontawesome/fonts/FontAwesome.otf differ diff --git a/public/vendor/fontawesome/fonts/fontawesome-webfont.eot b/public/vendor/fontawesome/fonts/fontawesome-webfont.eot index 9b6afaedc0..c7b00d2ba8 100644 Binary files a/public/vendor/fontawesome/fonts/fontawesome-webfont.eot and b/public/vendor/fontawesome/fonts/fontawesome-webfont.eot differ diff --git a/public/vendor/fontawesome/fonts/fontawesome-webfont.svg b/public/vendor/fontawesome/fonts/fontawesome-webfont.svg index d05688e9e2..8b66187fe0 100644 --- a/public/vendor/fontawesome/fonts/fontawesome-webfont.svg +++ b/public/vendor/fontawesome/fonts/fontawesome-webfont.svg @@ -169,7 +169,7 @@ <glyph unicode="" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" /> <glyph unicode="" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" /> <glyph unicode="" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" /> -<glyph unicode="" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" d="M519 336q4 6 -3 13q-9 7 -14 2q-4 -6 3 -13q9 -7 14 -2zM491 377q-5 7 -12 4q-6 -4 0 -12q7 -8 12 -5q6 4 0 13zM450 417q2 4 -5 8q-7 2 -8 -2q-3 -5 4 -8q8 -2 9 2zM471 394q2 1 1.5 4.5t-3.5 5.5q-6 7 -10 3t1 -11q6 -6 11 -2zM557 319q2 7 -9 11q-9 3 -13 -4 q-2 -7 9 -11q9 -3 13 4zM599 316q0 8 -12 8q-10 0 -10 -8t11 -8t11 8zM638 323q-2 7 -13 5t-9 -9q2 -8 12 -6t10 10zM1280 640q0 212 -150 362t-362 150t-362 -150t-150 -362q0 -167 98 -300.5t252 -185.5q18 -3 26.5 5t8.5 20q0 52 -1 95q-6 -1 -15.5 -2.5t-35.5 -2t-48 4 t-43.5 20t-29.5 41.5q-23 59 -57 74q-2 1 -4.5 3.5l-8 8t-7 9.5t4 7.5t19.5 3.5q6 0 15 -2t30 -15.5t33 -35.5q16 -28 37.5 -42t43.5 -14t38 3.5t30 9.5q7 47 33 69q-49 6 -86 18.5t-73 39t-55.5 76t-19.5 119.5q0 79 53 137q-24 62 5 136q19 6 54.5 -7.5t60.5 -29.5l26 -16 q58 17 128 17t128 -17q11 7 28.5 18t55.5 26t57 9q29 -74 5 -136q53 -58 53 -137q0 -57 -14 -100.5t-35.5 -70t-53.5 -44.5t-62.5 -26t-68.5 -12q35 -31 35 -95q0 -40 -0.5 -89t-0.5 -51q0 -12 8.5 -20t26.5 -5q154 52 252 185.5t98 300.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> <glyph unicode="" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" /> <glyph unicode="" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" /> <glyph unicode="" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" /> @@ -178,7 +178,7 @@ <glyph unicode="" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> <glyph unicode="" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" /> <glyph unicode="" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" /> -<glyph unicode="" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -40 7t-13 30q0 3 0.5 76.5t0.5 134.5q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 119 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24 q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-85 13.5q-45 -113 -8 -204q-79 -87 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-39 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5 t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -88.5t0.5 -54.5q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103zM291 305q3 7 -7 12 q-10 3 -13 -2q-3 -7 7 -12q9 -6 13 2zM322 271q7 5 -2 16q-10 9 -16 3q-7 -5 2 -16q10 -10 16 -3zM352 226q9 7 0 19q-8 13 -17 6q-9 -5 0 -18t17 -7zM394 184q8 8 -4 19q-12 12 -20 3q-9 -8 4 -19q12 -12 20 -3zM451 159q3 11 -13 16q-15 4 -19 -7t13 -15q15 -6 19 6z M514 154q0 13 -17 11q-16 0 -16 -11q0 -13 17 -11q16 0 16 11zM572 164q-2 11 -18 9q-16 -3 -14 -15t18 -8t14 14z" /> <glyph unicode="" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" /> <glyph unicode="" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" /> <glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" /> @@ -363,7 +363,7 @@ <glyph unicode="" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" /> <glyph unicode="" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" /> <glyph unicode="" d="M1289 -96h-1118v480h-160v-640h1438v640h-160v-480zM347 428l33 157l783 -165l-33 -156zM450 802l67 146l725 -339l-67 -145zM651 1158l102 123l614 -513l-102 -123zM1048 1536l477 -641l-128 -96l-477 641zM330 65v159h800v-159h-800z" /> -<glyph unicode="" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" /> +<glyph unicode="" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1162 640q0 -164 -115 -279t-279 -115t-279 115t-115 279t115 279t279 115t279 -115t115 -279zM1270 1050q0 -38 -27 -65t-65 -27t-65 27t-27 65t27 65t65 27t65 -27t27 -65zM768 1270 q-7 0 -76.5 0.5t-105.5 0t-96.5 -3t-103 -10t-71.5 -18.5q-50 -20 -88 -58t-58 -88q-11 -29 -18.5 -71.5t-10 -103t-3 -96.5t0 -105.5t0.5 -76.5t-0.5 -76.5t0 -105.5t3 -96.5t10 -103t18.5 -71.5q20 -50 58 -88t88 -58q29 -11 71.5 -18.5t103 -10t96.5 -3t105.5 0t76.5 0.5 t76.5 -0.5t105.5 0t96.5 3t103 10t71.5 18.5q50 20 88 58t58 88q11 29 18.5 71.5t10 103t3 96.5t0 105.5t-0.5 76.5t0.5 76.5t0 105.5t-3 96.5t-10 103t-18.5 71.5q-20 50 -58 88t-88 58q-29 11 -71.5 18.5t-103 10t-96.5 3t-105.5 0t-76.5 -0.5zM1536 640q0 -229 -5 -317 q-10 -208 -124 -322t-322 -124q-88 -5 -317 -5t-317 5q-208 10 -322 124t-124 322q-5 88 -5 317t5 317q10 208 124 322t322 124q88 5 317 5t317 -5q208 -10 322 -124t124 -322q5 -88 5 -317z" /> <glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" /> <glyph unicode="" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> <glyph unicode="" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" /> @@ -484,7 +484,7 @@ <glyph unicode="" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" /> <glyph unicode="" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" /> <glyph unicode="" d="M1519 890q18 -84 -4 -204q-87 -444 -565 -444h-44q-25 0 -44 -16.5t-24 -42.5l-4 -19l-55 -346l-2 -15q-5 -26 -24.5 -42.5t-44.5 -16.5h-251q-21 0 -33 15t-9 36q9 56 26.5 168t26.5 168t27 167.5t27 167.5q5 37 43 37h131q133 -2 236 21q175 39 287 144q102 95 155 246 q24 70 35 133q1 6 2.5 7.5t3.5 1t6 -3.5q79 -59 98 -162zM1347 1172q0 -107 -46 -236q-80 -233 -302 -315q-113 -40 -252 -42q0 -1 -90 -1l-90 1q-100 0 -118 -96q-2 -8 -85 -530q-1 -10 -12 -10h-295q-22 0 -36.5 16.5t-11.5 38.5l232 1471q5 29 27.5 48t51.5 19h598 q34 0 97.5 -13t111.5 -32q107 -41 163.5 -123t56.5 -196z" /> -<glyph unicode="" horiz-adv-x="1792" d="M602 949q19 -61 31 -123.5t17 -141.5t-14 -159t-62 -145q-21 81 -67 157t-95.5 127t-99 90.5t-78.5 57.5t-33 19q-62 34 -81.5 100t14.5 128t101 81.5t129 -14.5q138 -83 238 -177zM927 1236q11 -25 20.5 -46t36.5 -100.5t42.5 -150.5t25.5 -179.5t0 -205.5t-47.5 -209.5 t-105.5 -208.5q-51 -72 -138 -72q-54 0 -98 31q-57 40 -69 109t28 127q60 85 81 195t13 199.5t-32 180.5t-39 128t-22 52q-31 63 -8.5 129.5t85.5 97.5q34 17 75 17q47 0 88.5 -25t63.5 -69zM1248 567q-17 -160 -72 -311q-17 131 -63 246q25 174 -5 361q-27 178 -94 342 q114 -90 212 -211q9 -37 15 -80q26 -179 7 -347zM1520 1440q9 -17 23.5 -49.5t43.5 -117.5t50.5 -178t34 -227.5t5 -269t-47 -300t-112.5 -323.5q-22 -48 -66 -75.5t-95 -27.5q-39 0 -74 16q-67 31 -92.5 100t4.5 136q58 126 90 257.5t37.5 239.5t-3.5 213.5t-26.5 180.5 t-38.5 138.5t-32.5 90t-15.5 32.5q-34 65 -11.5 135.5t87.5 104.5q37 20 81 20q49 0 91.5 -25.5t66.5 -70.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M441 864q32 0 52 -26q266 -364 362 -774h-446q-127 441 -367 749q-12 16 -3 33.5t29 17.5h373zM1000 507q-49 -199 -125 -393q-79 310 -256 594q40 221 44 449q211 -340 337 -650zM1099 1216q235 -324 384.5 -698.5t184.5 -773.5h-451q-41 665 -553 1472h435zM1792 640 q0 -424 -101 -812q-67 560 -359 1083q-25 301 -106 584q-4 16 5.5 28.5t25.5 12.5h359q21 0 38.5 -13t22.5 -33q115 -409 115 -850z" /> <glyph unicode="" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> <glyph unicode="" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> <glyph unicode="" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" /> @@ -626,7 +626,7 @@ <glyph unicode="" horiz-adv-x="2304" d="M0 32v608h2304v-608q0 -66 -47 -113t-113 -47h-1984q-66 0 -113 47t-47 113zM640 256v-128h384v128h-384zM256 256v-128h256v128h-256zM2144 1408q66 0 113 -47t47 -113v-224h-2304v224q0 66 47 113t113 47h1984z" /> <glyph unicode="" horiz-adv-x="1792" d="M1549 857q55 0 85.5 -28.5t30.5 -83.5t-34 -82t-91 -27h-136v-177h-25v398h170zM1710 267l-4 -11l-5 -10q-113 -230 -330.5 -366t-474.5 -136q-182 0 -348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71q244 0 454.5 -124t329.5 -338l2 -4l8 -16 q-30 -15 -136.5 -68.5t-163.5 -84.5q-6 -3 -479 -268q384 -183 799 -366zM896 -234q250 0 462.5 132.5t322.5 357.5l-287 129q-72 -140 -206 -222t-292 -82q-151 0 -280 75t-204 204t-75 280t75 280t204 204t280 75t280 -73.5t204 -204.5l280 143q-116 208 -321 329 t-443 121q-119 0 -232.5 -31.5t-209 -87.5t-176.5 -137t-137 -176.5t-87.5 -209t-31.5 -232.5t31.5 -232.5t87.5 -209t137 -176.5t176.5 -137t209 -87.5t232.5 -31.5z" /> <glyph unicode="" horiz-adv-x="1792" d="M1427 827l-614 386l92 151h855zM405 562l-184 116v858l1183 -743zM1424 697l147 -95v-858l-532 335zM1387 718l-500 -802h-855l356 571z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1152 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1664 496v-752h-640v320q0 80 -56 136t-136 56t-136 -56t-56 -136v-320h-640v752q0 16 16 16h96 q16 0 16 -16v-112h128v624q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h16v393q-32 19 -32 55q0 26 19 45t45 19t45 -19t19 -45q0 -36 -32 -55v-9h272q16 0 16 -16v-224q0 -16 -16 -16h-272v-128h16q16 0 16 -16v-112h128 v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-624h128v112q0 16 16 16h96q16 0 16 -16z" /> +<glyph unicode="" horiz-adv-x="1792" d="M640 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1152 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1664 496v-752h-640v320q0 80 -56 136t-136 56t-136 -56t-56 -136v-320h-640v752q0 16 16 16h96 q16 0 16 -16v-112h128v624q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 6 2.5 9.5t8.5 5t9.5 2t11.5 0t9 -0.5v391q-32 15 -32 50q0 23 16.5 39t38.5 16t38.5 -16t16.5 -39q0 -35 -32 -50v-17q45 10 83 10q21 0 59.5 -7.5t54.5 -7.5 q17 0 47 7.5t37 7.5q16 0 16 -16v-210q0 -15 -35 -21.5t-62 -6.5q-18 0 -54.5 7.5t-55.5 7.5q-40 0 -90 -12v-133q1 0 9 0.5t11.5 0t9.5 -2t8.5 -5t2.5 -9.5v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-624h128v112q0 16 16 16h96 q16 0 16 -16z" /> <glyph unicode="" horiz-adv-x="2304" d="M2288 731q16 -8 16 -27t-16 -27l-320 -192q-8 -5 -16 -5q-9 0 -16 4q-16 10 -16 28v128h-858q37 -58 83 -165q16 -37 24.5 -55t24 -49t27 -47t27 -34t31.5 -26t33 -8h96v96q0 14 9 23t23 9h320q14 0 23 -9t9 -23v-320q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v96h-96 q-32 0 -61 10t-51 23.5t-45 40.5t-37 46t-33.5 57t-28.5 57.5t-28 60.5q-23 53 -37 81.5t-36 65t-44.5 53.5t-46.5 17h-360q-22 -84 -91 -138t-157 -54q-106 0 -181 75t-75 181t75 181t181 75q88 0 157 -54t91 -138h104q24 0 46.5 17t44.5 53.5t36 65t37 81.5q19 41 28 60.5 t28.5 57.5t33.5 57t37 46t45 40.5t51 23.5t61 10h107q21 57 70 92.5t111 35.5q80 0 136 -56t56 -136t-56 -136t-136 -56q-62 0 -111 35.5t-70 92.5h-107q-17 0 -33 -8t-31.5 -26t-27 -34t-27 -47t-24 -49t-24.5 -55q-46 -107 -83 -165h1114v128q0 18 16 28t32 -1z" /> <glyph unicode="" horiz-adv-x="1792" d="M1150 774q0 -56 -39.5 -95t-95.5 -39h-253v269h253q56 0 95.5 -39.5t39.5 -95.5zM1329 774q0 130 -91.5 222t-222.5 92h-433v-896h180v269h253q130 0 222 91.5t92 221.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348 t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> <glyph unicode="" horiz-adv-x="2304" d="M1645 438q0 59 -34 106.5t-87 68.5q-7 -45 -23 -92q-7 -24 -27.5 -38t-44.5 -14q-12 0 -24 3q-31 10 -45 38.5t-4 58.5q23 71 23 143q0 123 -61 227.5t-166 165.5t-228 61q-134 0 -247 -73t-167 -194q108 -28 188 -106q22 -23 22 -55t-22 -54t-54 -22t-55 22 q-75 75 -180 75q-106 0 -181 -74.5t-75 -180.5t75 -180.5t181 -74.5h1046q79 0 134.5 55.5t55.5 133.5zM1798 438q0 -142 -100.5 -242t-242.5 -100h-1046q-169 0 -289 119.5t-120 288.5q0 153 100 267t249 136q62 184 221 298t354 114q235 0 408.5 -158.5t196.5 -389.5 q116 -25 192.5 -118.5t76.5 -214.5zM2048 438q0 -175 -97 -319q-23 -33 -64 -33q-24 0 -43 13q-26 17 -32 48.5t12 57.5q71 104 71 233t-71 233q-18 26 -12 57t32 49t57.5 11.5t49.5 -32.5q97 -142 97 -318zM2304 438q0 -244 -134 -443q-23 -34 -64 -34q-23 0 -42 13 q-26 18 -32.5 49t11.5 57q108 164 108 358q0 195 -108 357q-18 26 -11.5 57.5t32.5 48.5q26 18 57 12t49 -33q134 -198 134 -442z" /> @@ -641,15 +641,45 @@ <glyph unicode="" d="M841 483l148 -148l-149 -149zM840 1094l149 -149l-148 -148zM710 -130l464 464l-306 306l306 306l-464 464v-611l-255 255l-93 -93l320 -321l-320 -321l93 -93l255 255v-611zM1429 640q0 -209 -32 -365.5t-87.5 -257t-140.5 -162.5t-181.5 -86.5t-219.5 -24.5 t-219.5 24.5t-181.5 86.5t-140.5 162.5t-87.5 257t-32 365.5t32 365.5t87.5 257t140.5 162.5t181.5 86.5t219.5 24.5t219.5 -24.5t181.5 -86.5t140.5 -162.5t87.5 -257t32 -365.5z" /> <glyph unicode="" horiz-adv-x="1024" d="M596 113l173 172l-173 172v-344zM596 823l173 172l-173 172v-344zM628 640l356 -356l-539 -540v711l-297 -296l-108 108l372 373l-372 373l108 108l297 -296v711l539 -540z" /> <glyph unicode="" d="M1280 256q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM512 1024q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5 t112.5 -271.5zM1440 1344q0 -20 -13 -38l-1056 -1408q-19 -26 -51 -26h-160q-26 0 -45 19t-19 45q0 20 13 38l1056 1408q19 26 51 26h160q26 0 45 -19t19 -45zM768 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" d="M104 830l792 -1015l-868 630q-18 13 -25 34.5t0 42.5l101 308v0zM566 830h660l-330 -1015v0zM368 1442l198 -612h-462l198 612q8 23 33 23t33 -23zM1688 830l101 -308q7 -21 0 -42.5t-25 -34.5l-868 -630l792 1015v0zM1688 830h-462l198 612q8 23 33 23t33 -23z" /> +<glyph unicode="" horiz-adv-x="1792" d="M384 704h160v224h-160v-224zM1221 372v92q-104 -36 -243 -38q-135 -1 -259.5 46.5t-220.5 122.5l1 -96q88 -80 212 -128.5t272 -47.5q129 0 238 49zM640 704h640v224h-640v-224zM1792 736q0 -187 -99 -352q89 -102 89 -229q0 -157 -129.5 -268t-313.5 -111 q-122 0 -225 52.5t-161 140.5q-19 -1 -57 -1t-57 1q-58 -88 -161 -140.5t-225 -52.5q-184 0 -313.5 111t-129.5 268q0 127 89 229q-99 165 -99 352q0 209 120 385.5t326.5 279.5t449.5 103t449.5 -103t326.5 -279.5t120 -385.5z" /> +<glyph unicode="" d="M515 625v-128h-252v128h252zM515 880v-127h-252v127h252zM1273 369v-128h-341v128h341zM1273 625v-128h-672v128h672zM1273 880v-127h-672v127h672zM1408 20v1240q0 8 -6 14t-14 6h-32l-378 -256l-210 171l-210 -171l-378 256h-32q-8 0 -14 -6t-6 -14v-1240q0 -8 6 -14 t14 -6h1240q8 0 14 6t6 14zM553 1130l185 150h-406zM983 1130l221 150h-406zM1536 1260v-1240q0 -62 -43 -105t-105 -43h-1240q-62 0 -105 43t-43 105v1240q0 62 43 105t105 43h1240q62 0 105 -43t43 -105z" /> +<glyph unicode="" horiz-adv-x="1792" d="M896 720q-104 196 -160 278q-139 202 -347 318q-34 19 -70 36q-89 40 -94 32t34 -38l39 -31q62 -43 112.5 -93.5t94.5 -116.5t70.5 -113t70.5 -131q9 -17 13 -25q44 -84 84 -153t98 -154t115.5 -150t131 -123.5t148.5 -90.5q153 -66 154 -60q1 3 -49 37q-53 36 -81 57 q-77 58 -179 211t-185 310zM549 177q-76 60 -132.5 125t-98 143.5t-71 154.5t-58.5 186t-52 209t-60.5 252t-76.5 289q273 0 497.5 -36t379 -92t271 -144.5t185.5 -172.5t110 -198.5t56 -199.5t12.5 -198.5t-9.5 -173t-20 -143.5t-13 -107l323 -327h-104l-281 285 q-22 -2 -91.5 -14t-121.5 -19t-138 -6t-160.5 17t-167.5 59t-179 111z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1374 879q-6 26 -28.5 39.5t-48.5 7.5q-261 -62 -401 -62t-401 62q-26 6 -48.5 -7.5t-28.5 -39.5t7.5 -48.5t39.5 -28.5q194 -46 303 -58q-2 -158 -15.5 -269t-26.5 -155.5t-41 -115.5l-9 -21q-10 -25 1 -49t36 -34q9 -4 23 -4q44 0 60 41l8 20q54 139 71 259h42 q17 -120 71 -259l8 -20q16 -41 60 -41q14 0 23 4q25 10 36 34t1 49l-9 21q-28 71 -41 115.5t-26.5 155.5t-15.5 269q109 12 303 58q26 6 39.5 28.5t7.5 48.5zM1024 1024q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1600 640q0 -143 -55.5 -273.5t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5zM896 1408q-156 0 -298 -61t-245 -164t-164 -245t-61 -298t61 -298 t164 -245t245 -164t298 -61t298 61t245 164t164 245t61 298t-61 298t-164 245t-245 164t-298 61zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> +<glyph unicode="" d="M1438 723q34 -35 29 -82l-44 -551q-4 -42 -34.5 -70t-71.5 -28q-6 0 -9 1q-44 3 -72.5 36.5t-25.5 77.5l35 429l-143 -8q55 -113 55 -240q0 -216 -148 -372l-137 137q91 101 91 235q0 145 -102.5 248t-247.5 103q-134 0 -236 -92l-137 138q120 114 284 141l264 300 l-149 87l-181 -161q-33 -30 -77 -27.5t-73 35.5t-26.5 77t34.5 73l239 213q26 23 60 26.5t64 -14.5l488 -283q36 -21 48 -68q17 -67 -26 -117l-205 -232l371 20q49 3 83 -32zM1240 1180q-74 0 -126 52t-52 126t52 126t126 52t126.5 -52t52.5 -126t-52.5 -126t-126.5 -52z M613 -62q106 0 196 61l139 -139q-146 -116 -335 -116q-148 0 -273.5 73t-198.5 198t-73 273q0 188 116 336l139 -139q-60 -88 -60 -197q0 -145 102.5 -247.5t247.5 -102.5z" /> +<glyph unicode="" d="M880 336v-160q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v160q0 14 9 23t23 9h160q14 0 23 -9t9 -23zM1136 832q0 -50 -15 -90t-45.5 -69t-52 -44t-59.5 -36q-32 -18 -46.5 -28t-26 -24t-11.5 -29v-32q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v68q0 35 10.5 64.5 t24 47.5t39 35.5t41 25.5t44.5 21q53 25 75 43t22 49q0 42 -43.5 71.5t-95.5 29.5q-56 0 -95 -27q-29 -20 -80 -83q-9 -12 -25 -12q-11 0 -19 6l-108 82q-10 7 -12 20t5 23q122 192 349 192q129 0 238.5 -89.5t109.5 -214.5zM768 1280q-130 0 -248.5 -51t-204 -136.5 t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5 t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M366 1225q-64 0 -110 45.5t-46 110.5q0 64 46 109.5t110 45.5t109.5 -45.5t45.5 -109.5q0 -65 -45.5 -110.5t-109.5 -45.5zM917 583q0 -50 -30 -67.5t-63.5 -6.5t-47.5 34l-367 438q-7 12 -14 15.5t-11 1.5l-3 -3q-7 -8 4 -21l122 -139l1 -354l-161 -457 q-67 -192 -92 -234q-16 -26 -28 -32q-50 -26 -103 -1q-29 13 -41.5 43t-9.5 57q2 17 197 618l5 416l-85 -164l35 -222q4 -24 -1 -42t-14 -27.5t-19 -16t-17 -7.5l-7 -2q-19 -3 -34.5 3t-24 16t-14 22t-7.5 19.5t-2 9.5l-46 299l211 381q23 34 113 34q75 0 107 -40l424 -521 q7 -5 14 -17l3 -3l-1 -1q7 -13 7 -29zM514 433q43 -113 88.5 -225t69.5 -168l24 -55q36 -93 42 -125q11 -70 -36 -97q-35 -22 -66 -16t-51 22t-29 35h-1q-6 16 -8 25l-124 351zM1338 -159q31 -49 31 -57q0 -5 -3 -7q-9 -5 -14.5 0.5t-15.5 26t-16 30.5q-114 172 -423 661 q3 -1 7 1t7 4l3 2q11 9 11 17z" /> +<glyph unicode="" horiz-adv-x="2304" d="M504 542h171l-1 265zM1530 641q0 87 -50.5 140t-146.5 53h-54v-388h52q91 0 145 57t54 138zM956 1018l1 -756q0 -14 -9.5 -24t-23.5 -10h-216q-14 0 -23.5 10t-9.5 24v62h-291l-55 -81q-10 -15 -28 -15h-267q-21 0 -30.5 18t3.5 35l556 757q9 14 27 14h332q14 0 24 -10 t10 -24zM1783 641q0 -193 -125.5 -303t-324.5 -110h-270q-14 0 -24 10t-10 24v756q0 14 10 24t24 10h268q200 0 326 -109t126 -302zM1939 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5 t-7.5 60t-20 91.5t-41 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2123 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-45 -108t-74 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5 h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2304 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66 104.5t41 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96 t9.5 -70.5z" /> +<glyph unicode="" horiz-adv-x="1408" d="M617 -153q0 11 -13 58t-31 107t-20 69q-1 4 -5 26.5t-8.5 36t-13.5 21.5q-15 14 -51 14q-23 0 -70 -5.5t-71 -5.5q-34 0 -47 11q-6 5 -11 15.5t-7.5 20t-6.5 24t-5 18.5q-37 128 -37 255t37 255q1 4 5 18.5t6.5 24t7.5 20t11 15.5q13 11 47 11q24 0 71 -5.5t70 -5.5 q36 0 51 14q9 8 13.5 21.5t8.5 36t5 26.5q2 9 20 69t31 107t13 58q0 22 -43.5 52.5t-75.5 42.5q-20 8 -45 8q-34 0 -98 -18q-57 -17 -96.5 -40.5t-71 -66t-46 -70t-45.5 -94.5q-6 -12 -9 -19q-49 -107 -68 -216t-19 -244t19 -244t68 -216q56 -122 83 -161q63 -91 179 -127 l6 -2q64 -18 98 -18q25 0 45 8q32 12 75.5 42.5t43.5 52.5zM776 760q-26 0 -45 19t-19 45.5t19 45.5q37 37 37 90q0 52 -37 91q-19 19 -19 45t19 45t45 19t45 -19q75 -75 75 -181t-75 -181q-21 -19 -45 -19zM957 579q-27 0 -45 19q-19 19 -19 45t19 45q112 114 112 272 t-112 272q-19 19 -19 45t19 45t45 19t45 -19q150 -150 150 -362t-150 -362q-18 -19 -45 -19zM1138 398q-27 0 -45 19q-19 19 -19 45t19 45q90 91 138.5 208t48.5 245t-48.5 245t-138.5 208q-19 19 -19 45t19 45t45 19t45 -19q109 -109 167 -249t58 -294t-58 -294t-167 -249 q-18 -19 -45 -19z" /> +<glyph unicode="" horiz-adv-x="2176" d="M192 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 352 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 864 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 1376q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 192q0 -80 -56 -136 t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 1216q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 192q0 -80 -56 -136t-136 -56t-136 56 t-56 136t56 136t136 56t136 -56t56 -136zM1664 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136 t56 136t136 56t136 -56t56 -136zM2176 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136z" /> +<glyph unicode="" horiz-adv-x="1792" d="M128 -192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM320 0q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM365 365l256 -256l-90 -90l-256 256zM704 384q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45z M1411 704q0 -59 -11.5 -108.5t-37.5 -93.5t-44 -67.5t-53 -64.5q-31 -35 -45.5 -54t-33.5 -50t-26.5 -64t-7.5 -74q0 -159 -112.5 -271.5t-271.5 -112.5q-26 0 -45 19t-19 45t19 45t45 19q106 0 181 75t75 181q0 57 11.5 105.5t37 91t43.5 66.5t52 63q40 46 59.5 72 t37.5 74.5t18 103.5q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM896 576q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45 t45 19t45 -19t19 -45zM1184 704q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 93 -65.5 158.5t-158.5 65.5q-92 0 -158 -65.5t-66 -158.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 146 103 249t249 103t249 -103t103 -249zM1578 993q10 -25 -1 -49t-36 -34q-9 -4 -23 -4 q-19 0 -35.5 11t-23.5 30q-68 178 -224 295q-21 16 -25 42t12 47q17 21 43 25t47 -12q183 -137 266 -351zM1788 1074q9 -25 -1.5 -49t-35.5 -34q-11 -4 -23 -4q-44 0 -60 41q-92 238 -297 393q-22 16 -25.5 42t12.5 47q16 22 42 25.5t47 -12.5q235 -175 341 -449z" /> +<glyph unicode="" horiz-adv-x="2304" d="M1032 576q-59 2 -84 55q-17 34 -48 53.5t-68 19.5q-53 0 -90.5 -37.5t-37.5 -90.5q0 -56 36 -89l10 -8q34 -31 82 -31q37 0 68 19.5t48 53.5q25 53 84 55zM1600 704q0 56 -36 89l-10 8q-34 31 -82 31q-37 0 -68 -19.5t-48 -53.5q-25 -53 -84 -55q59 -2 84 -55 q17 -34 48 -53.5t68 -19.5q53 0 90.5 37.5t37.5 90.5zM1174 925q-17 -35 -55 -48t-73 4q-62 31 -134 31q-51 0 -99 -17q3 0 9.5 0.5t9.5 0.5q92 0 170.5 -50t118.5 -133q17 -36 3.5 -73.5t-49.5 -54.5q-18 -9 -39 -9q21 0 39 -9q36 -17 49.5 -54.5t-3.5 -73.5 q-40 -83 -118.5 -133t-170.5 -50h-6q-16 2 -44 4l-290 27l-239 -120q-14 -7 -29 -7q-40 0 -57 35l-160 320q-11 23 -4 47.5t29 37.5l209 119l148 267q17 155 91.5 291.5t195.5 236.5q31 25 70.5 21.5t64.5 -34.5t21.5 -70t-34.5 -65q-70 -59 -117 -128q123 84 267 101 q40 5 71.5 -19t35.5 -64q5 -40 -19 -71.5t-64 -35.5q-84 -10 -159 -55q46 10 99 10q115 0 218 -50q36 -18 49 -55.5t-5 -73.5zM2137 1085l160 -320q11 -23 4 -47.5t-29 -37.5l-209 -119l-148 -267q-17 -155 -91.5 -291.5t-195.5 -236.5q-26 -22 -61 -22q-45 0 -74 35 q-25 31 -21.5 70t34.5 65q70 59 117 128q-123 -84 -267 -101q-4 -1 -12 -1q-36 0 -63.5 24t-31.5 60q-5 40 19 71.5t64 35.5q84 10 159 55q-46 -10 -99 -10q-115 0 -218 50q-36 18 -49 55.5t5 73.5q17 35 55 48t73 -4q62 -31 134 -31q51 0 99 17q-3 0 -9.5 -0.5t-9.5 -0.5 q-92 0 -170.5 50t-118.5 133q-17 36 -3.5 73.5t49.5 54.5q18 9 39 9q-21 0 -39 9q-36 17 -49.5 54.5t3.5 73.5q40 83 118.5 133t170.5 50h6h1q14 -2 42 -4l291 -27l239 120q14 7 29 7q40 0 57 -35z" /> +<glyph unicode="" horiz-adv-x="1792" d="M1056 704q0 -26 19 -45t45 -19t45 19t19 45q0 146 -103 249t-249 103t-249 -103t-103 -249q0 -26 19 -45t45 -19t45 19t19 45q0 93 66 158.5t158 65.5t158 -65.5t66 -158.5zM835 1280q-117 0 -223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5q0 -26 19 -45t45 -19t45 19 t19 45q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -55 -18 -103.5t-37.5 -74.5t-59.5 -72q-34 -39 -52 -63t-43.5 -66.5t-37 -91t-11.5 -105.5q0 -106 -75 -181t-181 -75q-26 0 -45 -19t-19 -45t19 -45t45 -19q159 0 271.5 112.5t112.5 271.5q0 41 7.5 74 t26.5 64t33.5 50t45.5 54q35 41 53 64.5t44 67.5t37.5 93.5t11.5 108.5q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5zM591 561l226 -226l-579 -579q-12 -12 -29 -12t-29 12l-168 168q-12 12 -12 29t12 29zM1612 1524l168 -168q12 -12 12 -29t-12 -30l-233 -233 l-26 -25l-71 -71q-66 153 -195 258l91 91l207 207q13 12 30 12t29 -12z" /> +<glyph unicode="" d="M866 1021q0 -27 -13 -94q-11 -50 -31.5 -150t-30.5 -150q-2 -11 -4.5 -12.5t-13.5 -2.5q-20 -2 -31 -2q-58 0 -84 49.5t-26 113.5q0 88 35 174t103 124q28 14 51 14q28 0 36.5 -16.5t8.5 -47.5zM1352 597q0 14 -39 75.5t-52 66.5q-21 8 -34 8q-91 0 -226 -77l-2 2 q3 22 27.5 135t24.5 178q0 233 -242 233q-24 0 -68 -6q-94 -17 -168.5 -89.5t-111.5 -166.5t-37 -189q0 -146 80.5 -225t227.5 -79q25 0 25 -3t-1 -5q-4 -34 -26 -117q-14 -52 -51.5 -101t-82.5 -49q-42 0 -42 47q0 24 10.5 47.5t25 39.5t29.5 28.5t26 20t11 8.5q0 3 -7 10 q-24 22 -58.5 36.5t-65.5 14.5q-35 0 -63.5 -34t-41 -75t-12.5 -75q0 -88 51.5 -142t138.5 -54q82 0 155 53t117.5 126t65.5 153q6 22 15.5 66.5t14.5 66.5q3 12 14 18q118 60 227 60q48 0 127 -18q1 -1 4 -1q5 0 9.5 4.5t4.5 8.5zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="1535" d="M744 1231q0 24 -2 38.5t-8.5 30t-21 23t-37.5 7.5q-39 0 -78 -23q-105 -58 -159 -190.5t-54 -269.5q0 -44 8.5 -85.5t26.5 -80.5t52.5 -62.5t81.5 -23.5q4 0 18 -0.5t20 0t16 3t15 8.5t7 16q16 77 48 231.5t48 231.5q19 91 19 146zM1498 575q0 -7 -7.5 -13.5t-15.5 -6.5 l-6 1q-22 3 -62 11t-72 12.5t-63 4.5q-167 0 -351 -93q-15 -8 -21 -27q-10 -36 -24.5 -105.5t-22.5 -100.5q-23 -91 -70 -179.5t-112.5 -164.5t-154.5 -123t-185 -47q-135 0 -214.5 83.5t-79.5 219.5q0 53 19.5 117t63 116.5t97.5 52.5q38 0 120 -33.5t83 -61.5 q0 -1 -16.5 -12.5t-39.5 -31t-46 -44.5t-39 -61t-16 -74q0 -33 16.5 -53t48.5 -20q45 0 85 31.5t66.5 78t48 105.5t32.5 107t16 90v9q0 2 -3.5 3.5t-8.5 1.5h-10t-10 -0.5t-6 -0.5q-227 0 -352 122.5t-125 348.5q0 108 34.5 221t96 210t156 167.5t204.5 89.5q52 9 106 9 q374 0 374 -360q0 -98 -38 -273t-43 -211l3 -3q101 57 182.5 88t167.5 31q22 0 53 -13q19 -7 80 -102.5t61 -116.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M831 863q32 0 59 -18l222 -148q61 -40 110 -97l146 -170q40 -46 29 -106l-72 -413q-6 -32 -29.5 -53.5t-55.5 -25.5l-527 -56l-352 -32h-9q-39 0 -67.5 28t-28.5 68q0 37 27 64t65 32l260 32h-448q-41 0 -69.5 30t-26.5 71q2 39 32 65t69 26l442 1l-521 64q-41 5 -66 37 t-19 73q6 35 34.5 57.5t65.5 22.5h10l481 -60l-351 94q-38 10 -62 41.5t-18 68.5q6 36 33 58.5t62 22.5q6 0 20 -2l448 -96l217 -37q1 0 3 -0.5t3 -0.5q23 0 30.5 23t-12.5 36l-186 125q-35 23 -42 63.5t18 73.5q27 38 76 38zM761 661l186 -125l-218 37l-5 2l-36 38 l-238 262q-1 1 -2.5 3.5t-2.5 3.5q-24 31 -18.5 70t37.5 64q31 23 68 17.5t64 -33.5l142 -147l-4 -4t-5 -4q-32 -45 -23 -99t55 -85zM1648 1115l15 -266q4 -73 -11 -147l-48 -219q-12 -59 -67 -87l-106 -54q2 62 -39 109l-146 170q-53 61 -117 103l-222 148q-34 23 -76 23 q-51 0 -88 -37l-235 312q-25 33 -18 73.5t41 63.5q33 22 71.5 14t62.5 -40l266 -352l-262 455q-21 35 -10.5 75t47.5 59q35 18 72.5 6t57.5 -46l241 -420l-136 337q-15 35 -4.5 74t44.5 56q37 19 76 6t56 -51l193 -415l101 -196q8 -15 23 -17.5t27 7.5t11 26l-12 224 q-2 41 26 71t69 31q39 0 67 -28.5t30 -67.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M335 180q-2 0 -6 2q-86 57 -168.5 145t-139.5 180q-21 30 -21 69q0 9 2 19t4 18t7 18t8.5 16t10.5 17t10 15t12 15.5t11 14.5q184 251 452 365q-110 198 -110 211q0 19 17 29q116 64 128 64q18 0 28 -16l124 -229q92 19 192 19q266 0 497.5 -137.5t378.5 -369.5 q20 -31 20 -69t-20 -69q-91 -142 -218.5 -253.5t-278.5 -175.5q110 -198 110 -211q0 -20 -17 -29q-116 -64 -127 -64q-19 0 -29 16l-124 229l-64 119l-444 820l7 7q-58 -24 -99 -47q3 -5 127 -234t243 -449t119 -223q0 -7 -9 -9q-13 -3 -72 -3q-57 0 -60 7l-456 841 q-39 -28 -82 -68q24 -43 214 -393.5t190 -354.5q0 -10 -11 -10q-14 0 -82.5 22t-72.5 28l-106 197l-224 413q-44 -53 -78 -106q2 -3 18 -25t23 -34l176 -327q0 -10 -10 -10zM1165 282l49 -91q273 111 450 385q-180 277 -459 389q67 -64 103 -148.5t36 -176.5 q0 -106 -47 -200.5t-132 -157.5zM848 896q0 -20 14 -34t34 -14q86 0 147 -61t61 -147q0 -20 14 -34t34 -14t34 14t14 34q0 126 -89 215t-215 89q-20 0 -34 -14t-14 -34zM1214 961l-9 4l7 -7z" /> +<glyph unicode="" horiz-adv-x="1280" d="M1050 430q0 -215 -147 -374q-148 -161 -378 -161q-232 0 -378 161q-147 159 -147 374q0 147 68 270.5t189 196.5t268 73q96 0 182 -31q-32 -62 -39 -126q-66 28 -143 28q-167 0 -280.5 -123t-113.5 -291q0 -170 112.5 -288.5t281.5 -118.5t281 118.5t112 288.5 q0 89 -32 166q66 13 123 49q41 -98 41 -212zM846 619q0 -192 -79.5 -345t-238.5 -253l-14 -1q-29 0 -62 5q83 32 146.5 102.5t99.5 154.5t58.5 189t30 192.5t7.5 178.5q0 69 -3 103q55 -160 55 -326zM791 947v-2q-73 214 -206 440q88 -59 142.5 -186.5t63.5 -251.5z M1035 744q-83 0 -160 75q218 120 290 247q19 37 21 56q-42 -94 -139.5 -166.5t-204.5 -97.5q-35 54 -35 113q0 37 17 79t43 68q46 44 157 74q59 16 106 58.5t74 100.5q74 -105 74 -253q0 -109 -24 -170q-32 -77 -88.5 -130.5t-130.5 -53.5z" /> +<glyph unicode="" d="M1050 495q0 78 -28 147q-41 -25 -85 -34q22 -50 22 -114q0 -117 -77 -198.5t-193 -81.5t-193.5 81.5t-77.5 198.5q0 115 78 199.5t193 84.5q53 0 98 -19q4 43 27 87q-60 21 -125 21q-154 0 -257.5 -108.5t-103.5 -263.5t103.5 -261t257.5 -106t257.5 106.5t103.5 260.5z M872 850q2 -24 2 -71q0 -63 -5 -123t-20.5 -132.5t-40.5 -130t-68.5 -106t-100.5 -70.5q21 -3 42 -3h10q219 139 219 411q0 116 -38 225zM872 850q-4 80 -44 171.5t-98 130.5q92 -156 142 -302zM1207 955q0 102 -51 174q-41 -86 -124 -109q-69 -19 -109 -53.5t-40 -99.5 q0 -40 24 -77q74 17 140.5 67t95.5 115q-4 -52 -74.5 -111.5t-138.5 -97.5q52 -52 110 -52q51 0 90 37t60 90q17 43 17 117zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> +<glyph unicode="" d="M1279 388q0 22 -22 27q-67 15 -118 59t-80 108q-7 19 -7 25q0 15 19.5 26t43 17t43 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-12 0 -32 -8t-31 -8q-4 0 -12 2q5 95 5 114q0 79 -17 114q-36 78 -103 121.5t-152 43.5q-199 0 -275 -165q-17 -35 -17 -114q0 -19 5 -114 q-4 -2 -14 -2q-12 0 -32 7.5t-30 7.5q-21 0 -38.5 -12t-17.5 -32q0 -21 19.5 -35.5t43 -20.5t43 -17t19.5 -26q0 -6 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -46 137 -68q2 -5 6 -26t11.5 -30.5t23.5 -9.5q12 0 37.5 4.5t39.5 4.5q35 0 67 -15t54 -32.5t57.5 -32.5 t76.5 -15q43 0 79 15t57.5 32.5t53.5 32.5t67 15q14 0 39.5 -4t38.5 -4q16 0 23 10t11 30t6 25q137 22 137 68zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" /> +<glyph unicode="" horiz-adv-x="1664" d="M848 1408q134 1 240.5 -68.5t163.5 -192.5q27 -58 27 -179q0 -47 -9 -191q14 -7 28 -7q18 0 51 13.5t51 13.5q29 0 56 -18t27 -46q0 -32 -31.5 -54t-69 -31.5t-69 -29t-31.5 -47.5q0 -15 12 -43q37 -82 102.5 -150t144.5 -101q28 -12 80 -23q28 -6 28 -35 q0 -70 -219 -103q-7 -11 -11 -39t-14 -46.5t-33 -18.5q-20 0 -62 6.5t-64 6.5q-37 0 -62 -5q-32 -5 -63 -22.5t-58 -38t-58 -40.5t-76 -33.5t-99 -13.5q-52 0 -96.5 13.5t-75 33.5t-57.5 40.5t-58 38t-62 22.5q-26 5 -63 5q-24 0 -65.5 -7.5t-58.5 -7.5q-25 0 -35 18.5 t-14 47.5t-11 40q-219 33 -219 103q0 29 28 35q52 11 80 23q78 32 144.5 101t102.5 150q12 28 12 43q0 28 -31.5 47.5t-69.5 29.5t-69.5 31.5t-31.5 52.5q0 27 26 45.5t55 18.5q15 0 48 -13t53 -13q18 0 32 7q-9 142 -9 190q0 122 27 180q64 137 172 198t264 63z" /> +<glyph unicode="" d="M1280 388q0 22 -22 27q-67 14 -118 58t-80 109q-7 14 -7 25q0 15 19.5 26t42.5 17t42.5 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-11 0 -31 -8t-32 -8q-4 0 -12 2q5 63 5 115q0 78 -17 114q-36 78 -102.5 121.5t-152.5 43.5q-198 0 -275 -165q-18 -38 -18 -115 q0 -38 6 -114q-10 -2 -15 -2q-11 0 -31.5 8t-30.5 8q-20 0 -37.5 -12.5t-17.5 -32.5q0 -21 19.5 -35.5t42.5 -20.5t42.5 -17t19.5 -26q0 -11 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -47 138 -69q2 -5 6 -26t11 -30.5t23 -9.5q13 0 38.5 5t38.5 5q35 0 67.5 -15 t54.5 -32.5t57.5 -32.5t76.5 -15q43 0 79 15t57.5 32.5t54 32.5t67.5 15q13 0 39 -4.5t39 -4.5q15 0 22.5 9.5t11.5 31t5 24.5q138 22 138 69zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" /> +<glyph unicode="" horiz-adv-x="2304" d="M2304 1536q-69 -46 -125 -92t-89 -81t-59.5 -71.5t-37.5 -57.5t-22 -44.5t-14 -29.5q-10 -18 -35.5 -136.5t-48.5 -164.5q-15 -29 -50 -60.5t-67.5 -50.5t-72.5 -41t-48 -28q-47 -31 -151 -231q-341 14 -630 -158q-92 -53 -303 -179q47 16 86 31t55 22l15 7 q71 27 163 64.5t133.5 53.5t108 34.5t142.5 31.5q186 31 465 -7q1 0 10 -3q11 -6 14 -17t-3 -22l-194 -345q-15 -29 -47 -22q-128 24 -354 24q-146 0 -402 -44.5t-392 -46.5q-82 -1 -149 13t-107 37t-61 40t-33 34l-1 1v2q0 6 6 6q138 0 371 55q192 366 374.5 524t383.5 158 q5 0 14.5 -0.5t38 -5t55 -12t61.5 -24.5t63 -39.5t54 -59t40 -82.5l102 177q2 4 21 42.5t44.5 86.5t61 109.5t84 133.5t100.5 137q66 82 128 141.5t121.5 96.5t92.5 53.5t88 39.5z" /> +<glyph unicode="" d="M1322 640q0 -45 -5 -76l-236 14l224 -78q-19 -73 -58 -141l-214 103l177 -158q-44 -61 -107 -108l-157 178l103 -215q-61 -37 -140 -59l-79 228l14 -240q-38 -6 -76 -6t-76 6l14 238l-78 -226q-74 19 -140 59l103 215l-157 -178q-59 43 -108 108l178 158l-214 -104 q-39 69 -58 141l224 79l-237 -14q-5 42 -5 76q0 35 5 77l238 -14l-225 79q19 73 58 140l214 -104l-177 159q46 61 107 108l158 -178l-103 215q67 39 140 58l77 -224l-13 236q36 6 75 6q38 0 76 -6l-14 -237l78 225q74 -19 140 -59l-103 -214l158 178q61 -47 107 -108 l-177 -159l213 104q37 -62 58 -141l-224 -78l237 14q5 -31 5 -77zM1352 640q0 160 -78.5 295.5t-213 214t-292.5 78.5q-119 0 -227 -46.5t-186.5 -125t-124.5 -187.5t-46 -229q0 -119 46 -228t124.5 -187.5t186.5 -125t227 -46.5q158 0 292.5 78.5t213 214t78.5 294.5z M1425 1023v-766l-657 -383l-657 383v766l657 383zM768 -183l708 412v823l-708 411l-708 -411v-823zM1536 1088v-896l-768 -448l-768 448v896l768 448z" /> +<glyph unicode="" horiz-adv-x="1664" d="M339 1318h691l-26 -72h-665q-110 0 -188.5 -79t-78.5 -189v-771q0 -95 60.5 -169.5t153.5 -93.5q23 -5 98 -5v-72h-45q-140 0 -239.5 100t-99.5 240v771q0 140 99.5 240t239.5 100zM1190 1536h247l-482 -1294q-23 -61 -40.5 -103.5t-45 -98t-54 -93.5t-64.5 -78.5 t-79.5 -65t-95.5 -41t-116 -18.5v195q163 26 220 182q20 52 20 105q0 54 -20 106l-285 733h228l187 -585zM1664 978v-1111h-795q37 55 45 73h678v1038q0 85 -49.5 155t-129.5 99l25 67q101 -34 163.5 -123.5t62.5 -197.5z" /> +<glyph unicode="" horiz-adv-x="1792" d="M852 1227q0 -29 -17 -52.5t-45 -23.5t-45 23.5t-17 52.5t17 52.5t45 23.5t45 -23.5t17 -52.5zM688 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50 -21.5t-20 -51.5v-114q0 -30 20.5 -52t49.5 -22q30 0 50.5 22t20.5 52zM860 -149v114q0 30 -20 51.5t-50 21.5t-50.5 -21.5 t-20.5 -51.5v-114q0 -30 20.5 -52t50.5 -22q29 0 49.5 22t20.5 52zM1034 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50.5 -21.5t-20.5 -51.5v-114q0 -30 20.5 -52t50.5 -22t50.5 22t20.5 52zM1208 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50.5 -21.5t-20.5 -51.5v-114 q0 -30 20.5 -52t50.5 -22t50.5 22t20.5 52zM1476 535q-84 -160 -232 -259.5t-323 -99.5q-123 0 -229.5 51.5t-178.5 137t-113 197.5t-41 232q0 88 21 174q-104 -175 -104 -390q0 -162 65 -312t185 -251q30 57 91 57q56 0 86 -50q32 50 87 50q56 0 86 -50q32 50 87 50t87 -50 q30 50 86 50q28 0 52.5 -15.5t37.5 -40.5q112 94 177 231.5t73 287.5zM1326 564q0 75 -72 75q-17 0 -47 -6q-95 -19 -149 -19q-226 0 -226 243q0 86 30 204q-83 -127 -83 -275q0 -150 89 -260.5t235 -110.5q111 0 210 70q13 48 13 79zM884 1223q0 50 -32 89.5t-81 39.5 t-81 -39.5t-32 -89.5q0 -51 31.5 -90.5t81.5 -39.5t81.5 39.5t31.5 90.5zM1513 884q0 96 -37.5 179t-113 137t-173.5 54q-77 0 -149 -35t-127 -94q-48 -159 -48 -268q0 -104 45.5 -157t147.5 -53q53 0 142 19q36 6 53 6q51 0 77.5 -28t26.5 -80q0 -26 -4 -46 q75 68 117.5 165.5t42.5 200.5zM1792 667q0 -111 -33.5 -249.5t-93.5 -204.5q-58 -64 -195 -142.5t-228 -104.5l-4 -1v-114q0 -43 -29.5 -75t-72.5 -32q-56 0 -86 50q-32 -50 -87 -50t-87 50q-30 -50 -86 -50q-55 0 -87 50q-30 -50 -86 -50q-47 0 -75 33.5t-28 81.5 q-90 -68 -198 -68q-118 0 -211 80q54 1 106 20q-113 31 -182 127q32 -7 71 -7q89 0 164 46q-192 192 -240 306q-24 56 -24 160q0 57 9 125.5t31.5 146.5t55 141t86.5 105t120 42q59 0 81 -52q19 29 42 54q2 3 12 13t13 16q10 15 23 38t25 42t28 39q87 111 211.5 177 t260.5 66q35 0 62 -4q59 64 146 64q83 0 140 -57q5 -5 5 -12q0 -5 -6 -13.5t-12.5 -16t-16 -17l-10.5 -10.5q17 -6 36 -18t19 -24q0 -6 -16 -25q157 -138 197 -378q25 30 60 30q45 0 100 -49q90 -80 90 -279z" /> +<glyph unicode="" d="M917 631q0 33 -6 64h-362v-132h217q-12 -76 -74.5 -120.5t-142.5 -44.5q-99 0 -169 71.5t-70 170.5t70 170.5t169 71.5q93 0 153 -59l104 101q-108 100 -257 100q-160 0 -272 -112.5t-112 -271.5t112 -271.5t272 -112.5q165 0 266.5 105t101.5 270zM1262 585h109v110 h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> +<glyph unicode="" d="M1536 1024v-839q0 -48 -49 -62q-174 -52 -338 -52q-73 0 -215.5 29.5t-227.5 29.5q-164 0 -370 -48v-338h-160v1368q-63 25 -101 81t-38 124q0 91 64 155t155 64t155 -64t64 -155q0 -68 -38 -124t-101 -81v-68q190 44 343 44q99 0 198 -15q14 -2 111.5 -22.5t149.5 -20.5 q77 0 165 18q11 2 80 21t89 19q26 0 45 -19t19 -45z" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> +<glyph unicode="" horiz-adv-x="1792" /> <glyph unicode="" horiz-adv-x="1792" /> </font> </defs></svg> \ No newline at end of file diff --git a/public/vendor/fontawesome/fonts/fontawesome-webfont.ttf b/public/vendor/fontawesome/fonts/fontawesome-webfont.ttf index 26dea7951a..f221e50a2e 100644 Binary files a/public/vendor/fontawesome/fonts/fontawesome-webfont.ttf and b/public/vendor/fontawesome/fonts/fontawesome-webfont.ttf differ diff --git a/public/vendor/fontawesome/fonts/fontawesome-webfont.woff b/public/vendor/fontawesome/fonts/fontawesome-webfont.woff index dc35ce3c2c..6e7483cf61 100644 Binary files a/public/vendor/fontawesome/fonts/fontawesome-webfont.woff and b/public/vendor/fontawesome/fonts/fontawesome-webfont.woff differ diff --git a/public/vendor/fontawesome/fonts/fontawesome-webfont.woff2 b/public/vendor/fontawesome/fonts/fontawesome-webfont.woff2 index 500e517253..7eb74fd127 100644 Binary files a/public/vendor/fontawesome/fonts/fontawesome-webfont.woff2 and b/public/vendor/fontawesome/fonts/fontawesome-webfont.woff2 differ diff --git a/public/vendor/fontawesome/less/font-awesome.less b/public/vendor/fontawesome/less/font-awesome.less index c35d3eeb90..c44e5f466a 100644 --- a/public/vendor/fontawesome/less/font-awesome.less +++ b/public/vendor/fontawesome/less/font-awesome.less @@ -1,5 +1,5 @@ /*! - * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ @@ -15,3 +15,4 @@ @import "rotated-flipped.less"; @import "stacked.less"; @import "icons.less"; +@import "screen-reader.less"; diff --git a/public/vendor/fontawesome/less/icons.less b/public/vendor/fontawesome/less/icons.less index ca60abd7e1..ba21b222d6 100644 --- a/public/vendor/fontawesome/less/icons.less +++ b/public/vendor/fontawesome/less/icons.less @@ -438,7 +438,7 @@ .@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } .@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } .@{fa-css-prefix}-digg:before { content: @fa-var-digg; } -.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } +.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; } .@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } .@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } .@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } @@ -488,6 +488,7 @@ .@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } .@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } .@{fa-css-prefix}-ra:before, +.@{fa-css-prefix}-resistance:before, .@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } .@{fa-css-prefix}-ge:before, .@{fa-css-prefix}-empire:before { content: @fa-var-empire; } @@ -695,3 +696,38 @@ .@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; } .@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; } .@{fa-css-prefix}-percent:before { content: @fa-var-percent; } +.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; } +.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; } +.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; } +.@{fa-css-prefix}-envira:before { content: @fa-var-envira; } +.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; } +.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; } +.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; } +.@{fa-css-prefix}-blind:before { content: @fa-var-blind; } +.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; } +.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; } +.@{fa-css-prefix}-braille:before { content: @fa-var-braille; } +.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; } +.@{fa-css-prefix}-asl-interpreting:before, +.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; } +.@{fa-css-prefix}-deafness:before, +.@{fa-css-prefix}-hard-of-hearing:before, +.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; } +.@{fa-css-prefix}-glide:before { content: @fa-var-glide; } +.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; } +.@{fa-css-prefix}-signing:before, +.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; } +.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; } +.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; } +.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; } +.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; } +.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; } +.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; } +.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } +.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; } +.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; } +.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; } +.@{fa-css-prefix}-google-plus-circle:before, +.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; } +.@{fa-css-prefix}-fa:before, +.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; } diff --git a/public/vendor/fontawesome/less/mixins.less b/public/vendor/fontawesome/less/mixins.less index d5a43a145a..beef231d0e 100644 --- a/public/vendor/fontawesome/less/mixins.less +++ b/public/vendor/fontawesome/less/mixins.less @@ -12,15 +12,49 @@ } .fa-icon-rotate(@degrees, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; -webkit-transform: rotate(@degrees); -ms-transform: rotate(@degrees); transform: rotate(@degrees); } .fa-icon-flip(@horiz, @vert, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; -webkit-transform: scale(@horiz, @vert); -ms-transform: scale(@horiz, @vert); transform: scale(@horiz, @vert); } + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only() { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +.sr-only-focusable() { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/public/vendor/fontawesome/less/path.less b/public/vendor/fontawesome/less/path.less index 9211e66597..835be41f81 100644 --- a/public/vendor/fontawesome/less/path.less +++ b/public/vendor/fontawesome/less/path.less @@ -9,7 +9,7 @@ url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); -// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts font-weight: normal; font-style: normal; } diff --git a/public/vendor/fontawesome/less/screen-reader.less b/public/vendor/fontawesome/less/screen-reader.less new file mode 100644 index 0000000000..11c188196d --- /dev/null +++ b/public/vendor/fontawesome/less/screen-reader.less @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { .sr-only(); } +.sr-only-focusable { .sr-only-focusable(); } diff --git a/public/vendor/fontawesome/less/variables.less b/public/vendor/fontawesome/less/variables.less index 34d4041cc7..a2019dcadc 100644 --- a/public/vendor/fontawesome/less/variables.less +++ b/public/vendor/fontawesome/less/variables.less @@ -4,9 +4,9 @@ @fa-font-path: "./vendor/fontawesome/fonts"; @fa-font-size-base: 14px; @fa-line-height-base: 1; -//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts"; // for referencing Bootstrap CDN font files directly +//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.6.3/fonts"; // for referencing Bootstrap CDN font files directly @fa-css-prefix: fa; -@fa-version: "4.5.0"; +@fa-version: "4.6.3"; @fa-border-color: #eee; @fa-inverse: #fff; @fa-li-width: (30em / 14); @@ -20,6 +20,7 @@ @fa-var-align-right: "\f038"; @fa-var-amazon: "\f270"; @fa-var-ambulance: "\f0f9"; +@fa-var-american-sign-language-interpreting: "\f2a3"; @fa-var-anchor: "\f13d"; @fa-var-android: "\f17b"; @fa-var-angellist: "\f209"; @@ -50,8 +51,11 @@ @fa-var-arrows-alt: "\f0b2"; @fa-var-arrows-h: "\f07e"; @fa-var-arrows-v: "\f07d"; +@fa-var-asl-interpreting: "\f2a3"; +@fa-var-assistive-listening-systems: "\f2a2"; @fa-var-asterisk: "\f069"; @fa-var-at: "\f1fa"; +@fa-var-audio-description: "\f29e"; @fa-var-automobile: "\f1b9"; @fa-var-backward: "\f04a"; @fa-var-balance-scale: "\f24e"; @@ -86,6 +90,7 @@ @fa-var-bitbucket-square: "\f172"; @fa-var-bitcoin: "\f15a"; @fa-var-black-tie: "\f27e"; +@fa-var-blind: "\f29d"; @fa-var-bluetooth: "\f293"; @fa-var-bluetooth-b: "\f294"; @fa-var-bold: "\f032"; @@ -94,6 +99,7 @@ @fa-var-book: "\f02d"; @fa-var-bookmark: "\f02e"; @fa-var-bookmark-o: "\f097"; +@fa-var-braille: "\f2a1"; @fa-var-briefcase: "\f0b1"; @fa-var-btc: "\f15a"; @fa-var-bug: "\f188"; @@ -196,6 +202,8 @@ @fa-var-dashboard: "\f0e4"; @fa-var-dashcube: "\f210"; @fa-var-database: "\f1c0"; +@fa-var-deaf: "\f2a4"; +@fa-var-deafness: "\f2a4"; @fa-var-dedent: "\f03b"; @fa-var-delicious: "\f1a5"; @fa-var-desktop: "\f108"; @@ -217,6 +225,7 @@ @fa-var-envelope: "\f0e0"; @fa-var-envelope-o: "\f003"; @fa-var-envelope-square: "\f199"; +@fa-var-envira: "\f299"; @fa-var-eraser: "\f12d"; @fa-var-eur: "\f153"; @fa-var-euro: "\f153"; @@ -231,6 +240,7 @@ @fa-var-eye: "\f06e"; @fa-var-eye-slash: "\f070"; @fa-var-eyedropper: "\f1fb"; +@fa-var-fa: "\f2b4"; @fa-var-facebook: "\f09a"; @fa-var-facebook-f: "\f09a"; @fa-var-facebook-official: "\f230"; @@ -265,6 +275,7 @@ @fa-var-fire: "\f06d"; @fa-var-fire-extinguisher: "\f134"; @fa-var-firefox: "\f269"; +@fa-var-first-order: "\f2b0"; @fa-var-flag: "\f024"; @fa-var-flag-checkered: "\f11e"; @fa-var-flag-o: "\f11d"; @@ -277,6 +288,7 @@ @fa-var-folder-open: "\f07c"; @fa-var-folder-open-o: "\f115"; @fa-var-font: "\f031"; +@fa-var-font-awesome: "\f2b4"; @fa-var-fonticons: "\f280"; @fa-var-fort-awesome: "\f286"; @fa-var-forumbee: "\f211"; @@ -300,11 +312,16 @@ @fa-var-github: "\f09b"; @fa-var-github-alt: "\f113"; @fa-var-github-square: "\f092"; +@fa-var-gitlab: "\f296"; @fa-var-gittip: "\f184"; @fa-var-glass: "\f000"; +@fa-var-glide: "\f2a5"; +@fa-var-glide-g: "\f2a6"; @fa-var-globe: "\f0ac"; @fa-var-google: "\f1a0"; @fa-var-google-plus: "\f0d5"; +@fa-var-google-plus-circle: "\f2b3"; +@fa-var-google-plus-official: "\f2b3"; @fa-var-google-plus-square: "\f0d4"; @fa-var-google-wallet: "\f1ee"; @fa-var-graduation-cap: "\f19d"; @@ -325,6 +342,7 @@ @fa-var-hand-scissors-o: "\f257"; @fa-var-hand-spock-o: "\f259"; @fa-var-hand-stop-o: "\f256"; +@fa-var-hard-of-hearing: "\f2a4"; @fa-var-hashtag: "\f292"; @fa-var-hdd-o: "\f0a0"; @fa-var-header: "\f1dc"; @@ -397,6 +415,7 @@ @fa-var-long-arrow-left: "\f177"; @fa-var-long-arrow-right: "\f178"; @fa-var-long-arrow-up: "\f176"; +@fa-var-low-vision: "\f2a8"; @fa-var-magic: "\f0d0"; @fa-var-magnet: "\f076"; @fa-var-mail-forward: "\f064"; @@ -468,8 +487,9 @@ @fa-var-photo: "\f03e"; @fa-var-picture-o: "\f03e"; @fa-var-pie-chart: "\f200"; -@fa-var-pied-piper: "\f1a7"; +@fa-var-pied-piper: "\f2ae"; @fa-var-pied-piper-alt: "\f1a8"; +@fa-var-pied-piper-pp: "\f1a7"; @fa-var-pinterest: "\f0d2"; @fa-var-pinterest-p: "\f231"; @fa-var-pinterest-square: "\f0d3"; @@ -490,6 +510,7 @@ @fa-var-qrcode: "\f029"; @fa-var-question: "\f128"; @fa-var-question-circle: "\f059"; +@fa-var-question-circle-o: "\f29c"; @fa-var-quote-left: "\f10d"; @fa-var-quote-right: "\f10e"; @fa-var-ra: "\f1d0"; @@ -507,6 +528,7 @@ @fa-var-repeat: "\f01e"; @fa-var-reply: "\f112"; @fa-var-reply-all: "\f122"; +@fa-var-resistance: "\f1d0"; @fa-var-retweet: "\f079"; @fa-var-rmb: "\f157"; @fa-var-road: "\f018"; @@ -544,8 +566,10 @@ @fa-var-shopping-basket: "\f291"; @fa-var-shopping-cart: "\f07a"; @fa-var-sign-in: "\f090"; +@fa-var-sign-language: "\f2a7"; @fa-var-sign-out: "\f08b"; @fa-var-signal: "\f012"; +@fa-var-signing: "\f2a7"; @fa-var-simplybuilt: "\f215"; @fa-var-sitemap: "\f0e8"; @fa-var-skyatlas: "\f216"; @@ -554,6 +578,9 @@ @fa-var-sliders: "\f1de"; @fa-var-slideshare: "\f1e7"; @fa-var-smile-o: "\f118"; +@fa-var-snapchat: "\f2ab"; +@fa-var-snapchat-ghost: "\f2ac"; +@fa-var-snapchat-square: "\f2ad"; @fa-var-soccer-ball-o: "\f1e3"; @fa-var-sort: "\f0dc"; @fa-var-sort-alpha-asc: "\f15d"; @@ -616,6 +643,7 @@ @fa-var-th: "\f00a"; @fa-var-th-large: "\f009"; @fa-var-th-list: "\f00b"; +@fa-var-themeisle: "\f2b2"; @fa-var-thumb-tack: "\f08d"; @fa-var-thumbs-down: "\f165"; @fa-var-thumbs-o-down: "\f088"; @@ -655,6 +683,7 @@ @fa-var-umbrella: "\f0e9"; @fa-var-underline: "\f0cd"; @fa-var-undo: "\f0e2"; +@fa-var-universal-access: "\f29a"; @fa-var-university: "\f19c"; @fa-var-unlink: "\f127"; @fa-var-unlock: "\f09c"; @@ -673,11 +702,14 @@ @fa-var-venus-double: "\f226"; @fa-var-venus-mars: "\f228"; @fa-var-viacoin: "\f237"; +@fa-var-viadeo: "\f2a9"; +@fa-var-viadeo-square: "\f2aa"; @fa-var-video-camera: "\f03d"; @fa-var-vimeo: "\f27d"; @fa-var-vimeo-square: "\f194"; @fa-var-vine: "\f1ca"; @fa-var-vk: "\f189"; +@fa-var-volume-control-phone: "\f2a0"; @fa-var-volume-down: "\f027"; @fa-var-volume-off: "\f026"; @fa-var-volume-up: "\f028"; @@ -687,11 +719,14 @@ @fa-var-weixin: "\f1d7"; @fa-var-whatsapp: "\f232"; @fa-var-wheelchair: "\f193"; +@fa-var-wheelchair-alt: "\f29b"; @fa-var-wifi: "\f1eb"; @fa-var-wikipedia-w: "\f266"; @fa-var-windows: "\f17a"; @fa-var-won: "\f159"; @fa-var-wordpress: "\f19a"; +@fa-var-wpbeginner: "\f297"; +@fa-var-wpforms: "\f298"; @fa-var-wrench: "\f0ad"; @fa-var-xing: "\f168"; @fa-var-xing-square: "\f169"; @@ -702,6 +737,7 @@ @fa-var-yc-square: "\f1d4"; @fa-var-yelp: "\f1e9"; @fa-var-yen: "\f157"; +@fa-var-yoast: "\f2b1"; @fa-var-youtube: "\f167"; @fa-var-youtube-play: "\f16a"; @fa-var-youtube-square: "\f166"; diff --git a/public/vendor/hammer/hammer.min.js b/public/vendor/hammer/hammer.min.js deleted file mode 100644 index 575b810f47..0000000000 --- a/public/vendor/hammer/hammer.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ -!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(k(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e<a.length;)b.call(c,a[e],e,a),e++;else for(e in a)a.hasOwnProperty(e)&&b.call(c,a[e],e,a)}function h(a,b,c){for(var e=Object.keys(b),f=0;f<e.length;)(!c||c&&a[e[f]]===d)&&(a[e[f]]=b[e[f]]),f++;return a}function i(a,b){return h(a,b,!0)}function j(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&h(d,c)}function k(a,b){return function(){return a.apply(b,arguments)}}function l(a,b){return typeof a==kb?a.apply(b?b[0]||d:d,b):a}function m(a,b){return a===d?b:a}function n(a,b,c){g(r(b),function(b){a.addEventListener(b,c,!1)})}function o(a,b,c){g(r(b),function(b){a.removeEventListener(b,c,!1)})}function p(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function q(a,b){return a.indexOf(b)>-1}function r(a){return a.trim().split(/\s+/g)}function s(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;d<a.length;){if(c&&a[d][c]==b||!c&&a[d]===b)return d;d++}return-1}function t(a){return Array.prototype.slice.call(a,0)}function u(a,b,c){for(var d=[],e=[],f=0;f<a.length;){var g=b?a[f][b]:a[f];s(e,g)<0&&d.push(a[f]),e[f]=g,f++}return c&&(d=b?d.sort(function(a,c){return a[b]>c[b]}):d.sort()),d}function v(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g<ib.length;){if(c=ib[g],e=c?c+f:b,e in a)return e;g++}return d}function w(){return ob++}function x(a){var b=a.ownerDocument;return b.defaultView||b.parentWindow}function y(a,b){var c=this;this.manager=a,this.callback=b,this.element=a.element,this.target=a.options.inputTarget,this.domHandler=function(b){l(a.options.enable,[a])&&c.handler(b)},this.init()}function z(a){var b,c=a.options.inputClass;return new(b=c?c:rb?N:sb?Q:qb?S:M)(a,A)}function A(a,b,c){var d=c.pointers.length,e=c.changedPointers.length,f=b&yb&&d-e===0,g=b&(Ab|Bb)&&d-e===0;c.isFirst=!!f,c.isFinal=!!g,f&&(a.session={}),c.eventType=b,B(a,c),a.emit("hammer.input",c),a.recognize(c),a.session.prevInput=c}function B(a,b){var c=a.session,d=b.pointers,e=d.length;c.firstInput||(c.firstInput=E(b)),e>1&&!c.firstMultiple?c.firstMultiple=E(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=F(d);b.timeStamp=nb(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=J(h,i),b.distance=I(h,i),C(c,b),b.offsetDirection=H(b.deltaX,b.deltaY),b.scale=g?L(g.pointers,d):1,b.rotation=g?K(g.pointers,d):0,D(c,b);var j=a.element;p(b.srcEvent.target,j)&&(j=b.srcEvent.target),b.target=j}function C(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};(b.eventType===yb||f.eventType===Ab)&&(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function D(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Bb&&(i>xb||h.velocity===d)){var j=h.deltaX-b.deltaX,k=h.deltaY-b.deltaY,l=G(i,j,k);e=l.x,f=l.y,c=mb(l.x)>mb(l.y)?l.x:l.y,g=H(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function E(a){for(var b=[],c=0;c<a.pointers.length;)b[c]={clientX:lb(a.pointers[c].clientX),clientY:lb(a.pointers[c].clientY)},c++;return{timeStamp:nb(),pointers:b,center:F(b),deltaX:a.deltaX,deltaY:a.deltaY}}function F(a){var b=a.length;if(1===b)return{x:lb(a[0].clientX),y:lb(a[0].clientY)};for(var c=0,d=0,e=0;b>e;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:lb(c/b),y:lb(d/b)}}function G(a,b,c){return{x:b/a||0,y:c/a||0}}function H(a,b){return a===b?Cb:mb(a)>=mb(b)?a>0?Db:Eb:b>0?Fb:Gb}function I(a,b,c){c||(c=Kb);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function J(a,b,c){c||(c=Kb);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function K(a,b){return J(b[1],b[0],Lb)-J(a[1],a[0],Lb)}function L(a,b){return I(b[0],b[1],Lb)/I(a[0],a[1],Lb)}function M(){this.evEl=Nb,this.evWin=Ob,this.allow=!0,this.pressed=!1,y.apply(this,arguments)}function N(){this.evEl=Rb,this.evWin=Sb,y.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function O(){this.evTarget=Ub,this.evWin=Vb,this.started=!1,y.apply(this,arguments)}function P(a,b){var c=t(a.touches),d=t(a.changedTouches);return b&(Ab|Bb)&&(c=u(c.concat(d),"identifier",!0)),[c,d]}function Q(){this.evTarget=Xb,this.targetIds={},y.apply(this,arguments)}function R(a,b){var c=t(a.touches),d=this.targetIds;if(b&(yb|zb)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=t(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return p(a.target,i)}),b===yb)for(e=0;e<f.length;)d[f[e].identifier]=!0,e++;for(e=0;e<g.length;)d[g[e].identifier]&&h.push(g[e]),b&(Ab|Bb)&&delete d[g[e].identifier],e++;return h.length?[u(f.concat(h),"identifier",!0),h]:void 0}function S(){y.apply(this,arguments);var a=k(this.handler,this);this.touch=new Q(this.manager,a),this.mouse=new M(this.manager,a)}function T(a,b){this.manager=a,this.set(b)}function U(a){if(q(a,bc))return bc;var b=q(a,cc),c=q(a,dc);return b&&c?cc+" "+dc:b||c?b?cc:dc:q(a,ac)?ac:_b}function V(a){this.id=w(),this.manager=null,this.options=i(a||{},this.defaults),this.options.enable=m(this.options.enable,!0),this.state=ec,this.simultaneous={},this.requireFail=[]}function W(a){return a&jc?"cancel":a&hc?"end":a&gc?"move":a&fc?"start":""}function X(a){return a==Gb?"down":a==Fb?"up":a==Db?"left":a==Eb?"right":""}function Y(a,b){var c=b.manager;return c?c.get(a):a}function Z(){V.apply(this,arguments)}function $(){Z.apply(this,arguments),this.pX=null,this.pY=null}function _(){Z.apply(this,arguments)}function ab(){V.apply(this,arguments),this._timer=null,this._input=null}function bb(){Z.apply(this,arguments)}function cb(){Z.apply(this,arguments)}function db(){V.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function eb(a,b){return b=b||{},b.recognizers=m(b.recognizers,eb.defaults.preset),new fb(a,b)}function fb(a,b){b=b||{},this.options=i(b,eb.defaults),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.element=a,this.input=z(this),this.touchAction=new T(this,this.options.touchAction),gb(this,!0),g(b.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function gb(a,b){var c=a.element;g(a.options.cssProps,function(a,d){c.style[v(c.style,d)]=b?a:""})}function hb(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var ib=["","webkit","moz","MS","ms","o"],jb=b.createElement("div"),kb="function",lb=Math.round,mb=Math.abs,nb=Date.now,ob=1,pb=/mobile|tablet|ip(ad|hone|od)|android/i,qb="ontouchstart"in a,rb=v(a,"PointerEvent")!==d,sb=qb&&pb.test(navigator.userAgent),tb="touch",ub="pen",vb="mouse",wb="kinect",xb=25,yb=1,zb=2,Ab=4,Bb=8,Cb=1,Db=2,Eb=4,Fb=8,Gb=16,Hb=Db|Eb,Ib=Fb|Gb,Jb=Hb|Ib,Kb=["x","y"],Lb=["clientX","clientY"];y.prototype={handler:function(){},init:function(){this.evEl&&n(this.element,this.evEl,this.domHandler),this.evTarget&&n(this.target,this.evTarget,this.domHandler),this.evWin&&n(x(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&o(this.element,this.evEl,this.domHandler),this.evTarget&&o(this.target,this.evTarget,this.domHandler),this.evWin&&o(x(this.element),this.evWin,this.domHandler)}};var Mb={mousedown:yb,mousemove:zb,mouseup:Ab},Nb="mousedown",Ob="mousemove mouseup";j(M,y,{handler:function(a){var b=Mb[a.type];b&yb&&0===a.button&&(this.pressed=!0),b&zb&&1!==a.which&&(b=Ab),this.pressed&&this.allow&&(b&Ab&&(this.pressed=!1),this.callback(this.manager,b,{pointers:[a],changedPointers:[a],pointerType:vb,srcEvent:a}))}});var Pb={pointerdown:yb,pointermove:zb,pointerup:Ab,pointercancel:Bb,pointerout:Bb},Qb={2:tb,3:ub,4:vb,5:wb},Rb="pointerdown",Sb="pointermove pointerup pointercancel";a.MSPointerEvent&&(Rb="MSPointerDown",Sb="MSPointerMove MSPointerUp MSPointerCancel"),j(N,y,{handler:function(a){var b=this.store,c=!1,d=a.type.toLowerCase().replace("ms",""),e=Pb[d],f=Qb[a.pointerType]||a.pointerType,g=f==tb,h=s(b,a.pointerId,"pointerId");e&yb&&(0===a.button||g)?0>h&&(b.push(a),h=b.length-1):e&(Ab|Bb)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Tb={touchstart:yb,touchmove:zb,touchend:Ab,touchcancel:Bb},Ub="touchstart",Vb="touchstart touchmove touchend touchcancel";j(O,y,{handler:function(a){var b=Tb[a.type];if(b===yb&&(this.started=!0),this.started){var c=P.call(this,a,b);b&(Ab|Bb)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:tb,srcEvent:a})}}});var Wb={touchstart:yb,touchmove:zb,touchend:Ab,touchcancel:Bb},Xb="touchstart touchmove touchend touchcancel";j(Q,y,{handler:function(a){var b=Wb[a.type],c=R.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:tb,srcEvent:a})}}),j(S,y,{handler:function(a,b,c){var d=c.pointerType==tb,e=c.pointerType==vb;if(d)this.mouse.allow=!1;else if(e&&!this.mouse.allow)return;b&(Ab|Bb)&&(this.mouse.allow=!0),this.callback(a,b,c)},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var Yb=v(jb.style,"touchAction"),Zb=Yb!==d,$b="compute",_b="auto",ac="manipulation",bc="none",cc="pan-x",dc="pan-y";T.prototype={set:function(a){a==$b&&(a=this.compute()),Zb&&(this.manager.element.style[Yb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){l(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),U(a.join(" "))},preventDefaults:function(a){if(!Zb){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=q(d,bc),f=q(d,dc),g=q(d,cc);return e||f&&c&Hb||g&&c&Ib?this.preventSrc(b):void 0}},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var ec=1,fc=2,gc=4,hc=8,ic=hc,jc=16,kc=32;V.prototype={defaults:{},set:function(a){return h(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=Y(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=Y(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=Y(a,this),-1===s(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=Y(a,this);var b=s(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(c.options.event+(b?W(d):""),a)}var c=this,d=this.state;hc>d&&b(!0),b(),d>=hc&&b(!0)},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=kc)},canEmit:function(){for(var a=0;a<this.requireFail.length;){if(!(this.requireFail[a].state&(kc|ec)))return!1;a++}return!0},recognize:function(a){var b=h({},a);return l(this.options.enable,[this,b])?(this.state&(ic|jc|kc)&&(this.state=ec),this.state=this.process(b),void(this.state&(fc|gc|hc|jc)&&this.tryEmit(b))):(this.reset(),void(this.state=kc))},process:function(){},getTouchAction:function(){},reset:function(){}},j(Z,V,{defaults:{pointers:1},attrTest:function(a){var b=this.options.pointers;return 0===b||a.pointers.length===b},process:function(a){var b=this.state,c=a.eventType,d=b&(fc|gc),e=this.attrTest(a);return d&&(c&Bb||!e)?b|jc:d||e?c&Ab?b|hc:b&fc?b|gc:fc:kc}}),j($,Z,{defaults:{event:"pan",threshold:10,pointers:1,direction:Jb},getTouchAction:function(){var a=this.options.direction,b=[];return a&Hb&&b.push(dc),a&Ib&&b.push(cc),b},directionTest:function(a){var b=this.options,c=!0,d=a.distance,e=a.direction,f=a.deltaX,g=a.deltaY;return e&b.direction||(b.direction&Hb?(e=0===f?Cb:0>f?Db:Eb,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Cb:0>g?Fb:Gb,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return Z.prototype.attrTest.call(this,a)&&(this.state&fc||!(this.state&fc)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=X(a.direction);b&&this.manager.emit(this.options.event+b,a),this._super.emit.call(this,a)}}),j(_,Z,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[bc]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&fc)},emit:function(a){if(this._super.emit.call(this,a),1!==a.scale){var b=a.scale<1?"in":"out";this.manager.emit(this.options.event+b,a)}}}),j(ab,V,{defaults:{event:"press",pointers:1,time:500,threshold:5},getTouchAction:function(){return[_b]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance<b.threshold,f=a.deltaTime>b.time;if(this._input=a,!d||!c||a.eventType&(Ab|Bb)&&!f)this.reset();else if(a.eventType&yb)this.reset(),this._timer=e(function(){this.state=ic,this.tryEmit()},b.time,this);else if(a.eventType&Ab)return ic;return kc},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===ic&&(a&&a.eventType&Ab?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=nb(),this.manager.emit(this.options.event,this._input)))}}),j(bb,Z,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[bc]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&fc)}}),j(cb,Z,{defaults:{event:"swipe",threshold:10,velocity:.65,direction:Hb|Ib,pointers:1},getTouchAction:function(){return $.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Hb|Ib)?b=a.velocity:c&Hb?b=a.velocityX:c&Ib&&(b=a.velocityY),this._super.attrTest.call(this,a)&&c&a.direction&&a.distance>this.options.threshold&&mb(b)>this.options.velocity&&a.eventType&Ab},emit:function(a){var b=X(a.direction);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),j(db,V,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:2,posThreshold:10},getTouchAction:function(){return[ac]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance<b.threshold,f=a.deltaTime<b.time;if(this.reset(),a.eventType&yb&&0===this.count)return this.failTimeout();if(d&&f&&c){if(a.eventType!=Ab)return this.failTimeout();var g=this.pTime?a.timeStamp-this.pTime<b.interval:!0,h=!this.pCenter||I(this.pCenter,a.center)<b.posThreshold;this.pTime=a.timeStamp,this.pCenter=a.center,h&&g?this.count+=1:this.count=1,this._input=a;var i=this.count%b.taps;if(0===i)return this.hasRequireFailures()?(this._timer=e(function(){this.state=ic,this.tryEmit()},b.interval,this),fc):ic}return kc},failTimeout:function(){return this._timer=e(function(){this.state=kc},this.options.interval,this),kc},reset:function(){clearTimeout(this._timer)},emit:function(){this.state==ic&&(this._input.tapCount=this.count,this.manager.emit(this.options.event,this._input))}}),eb.VERSION="2.0.4",eb.defaults={domEvents:!1,touchAction:$b,enable:!0,inputTarget:null,inputClass:null,preset:[[bb,{enable:!1}],[_,{enable:!1},["rotate"]],[cb,{direction:Hb}],[$,{direction:Hb},["swipe"]],[db],[db,{event:"doubletap",taps:2},["tap"]],[ab]],cssProps:{userSelect:"none",touchSelect:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}};var lc=1,mc=2;fb.prototype={set:function(a){return h(this.options,a),a.touchAction&&this.touchAction.update(),a.inputTarget&&(this.input.destroy(),this.input.target=a.inputTarget,this.input.init()),this},stop:function(a){this.session.stopped=a?mc:lc},recognize:function(a){var b=this.session;if(!b.stopped){this.touchAction.preventDefaults(a);var c,d=this.recognizers,e=b.curRecognizer;(!e||e&&e.state&ic)&&(e=b.curRecognizer=null);for(var f=0;f<d.length;)c=d[f],b.stopped===mc||e&&c!=e&&!c.canRecognizeWith(e)?c.reset():c.recognize(a),!e&&c.state&(fc|gc|hc)&&(e=b.curRecognizer=c),f++}},get:function(a){if(a instanceof V)return a;for(var b=this.recognizers,c=0;c<b.length;c++)if(b[c].options.event==a)return b[c];return null},add:function(a){if(f(a,"add",this))return this;var b=this.get(a.options.event);return b&&this.remove(b),this.recognizers.push(a),a.manager=this,this.touchAction.update(),a},remove:function(a){if(f(a,"remove",this))return this;var b=this.recognizers;return a=this.get(a),b.splice(s(b,a),1),this.touchAction.update(),this},on:function(a,b){var c=this.handlers;return g(r(a),function(a){c[a]=c[a]||[],c[a].push(b)}),this},off:function(a,b){var c=this.handlers;return g(r(a),function(a){b?c[a].splice(s(c[a],b),1):delete c[a]}),this},emit:function(a,b){this.options.domEvents&&hb(a,b);var c=this.handlers[a]&&this.handlers[a].slice();if(c&&c.length){b.type=a,b.preventDefault=function(){b.srcEvent.preventDefault()};for(var d=0;d<c.length;)c[d](b),d++}},destroy:function(){this.element&&gb(this,!1),this.handlers={},this.session={},this.input.destroy(),this.element=null}},h(eb,{INPUT_START:yb,INPUT_MOVE:zb,INPUT_END:Ab,INPUT_CANCEL:Bb,STATE_POSSIBLE:ec,STATE_BEGAN:fc,STATE_CHANGED:gc,STATE_ENDED:hc,STATE_RECOGNIZED:ic,STATE_CANCELLED:jc,STATE_FAILED:kc,DIRECTION_NONE:Cb,DIRECTION_LEFT:Db,DIRECTION_RIGHT:Eb,DIRECTION_UP:Fb,DIRECTION_DOWN:Gb,DIRECTION_HORIZONTAL:Hb,DIRECTION_VERTICAL:Ib,DIRECTION_ALL:Jb,Manager:fb,Input:y,TouchAction:T,TouchInput:Q,MouseInput:M,PointerEventInput:N,TouchMouseInput:S,SingleTouchInput:O,Recognizer:V,AttrRecognizer:Z,Tap:db,Pan:$,Swipe:cb,Pinch:_,Rotate:bb,Press:ab,on:n,off:o,each:g,merge:i,extend:h,inherit:j,bindFn:k,prefixed:v}),typeof define==kb&&define.amd?define(function(){return eb}):"undefined"!=typeof module&&module.exports?module.exports=eb:a[c]=eb}(window,document,"Hammer"); \ No newline at end of file diff --git a/public/vendor/jquery/css/smoothness/images/animated-overlay.gif b/public/vendor/jquery/css/smoothness/images/animated-overlay.gif deleted file mode 100644 index d441f75ebf..0000000000 Binary files a/public/vendor/jquery/css/smoothness/images/animated-overlay.gif and /dev/null differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png b/public/vendor/jquery/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png deleted file mode 100644 index 42319a6877..0000000000 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png and /dev/null differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png b/public/vendor/jquery/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png deleted file mode 100644 index bfa1f3da61..0000000000 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png and /dev/null differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png index 9ed29e7f7f..3e87efd7e9 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png index f0e86f1a76..e1602e3b8b 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png index 15d191d79b..278b8b23fe 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png index f1e3607e16..6e84a8137f 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png index 31b9eb03dd..443633de42 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/public/vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png index 75831b5348..cd72b628dc 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png and b/public/vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png b/public/vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png index c1cb1170c8..e723e17cb5 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png and b/public/vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png b/public/vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png index 84b601bf0f..1f5f49756c 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png and b/public/vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png b/public/vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png index b6db1acdd4..618f5b0ca7 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png and b/public/vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png b/public/vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png index feea0e2026..ee5e33f272 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png and b/public/vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png differ diff --git a/public/vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png b/public/vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png index ed5b6b0930..7e8ebc180a 100644 Binary files a/public/vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png and b/public/vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png differ diff --git a/public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css b/public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css deleted file mode 100644 index 4dbde7a38a..0000000000 --- a/public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-05-02 -* http://jqueryui.com -* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.autocomplete.css, jquery.ui.datepicker.css, jquery.ui.menu.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:49%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url("vendor/jquery/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url("vendor/jquery/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url("vendor/jquery/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/public/vendor/jquery/css/smoothness/jquery-ui.css b/public/vendor/jquery/css/smoothness/jquery-ui.css new file mode 100644 index 0000000000..c41b03e1f6 --- /dev/null +++ b/public/vendor/jquery/css/smoothness/jquery-ui.css @@ -0,0 +1,845 @@ +/*! jQuery UI - v1.12.0 - 2016-08-26 +* http://jqueryui.com +* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css, autocomplete.css, menu.css, datepicker.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=smoothness&cornerRadiusShadow=8px&offsetLeftShadow=-8px&offsetTopShadow=-8px&thicknessShadow=8px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=aaaaaa&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cd0a0a&fcError=cd0a0a&borderColorError=cd0a0a&bgImgOpacityError=95&bgTextureError=glass&bgColorError=fef1ec&iconColorHighlight=2e83ff&fcHighlight=363636&borderColorHighlight=fcefa1&bgImgOpacityHighlight=55&bgTextureHighlight=glass&bgColorHighlight=fbf9ee&iconColorActive=454545&fcActive=212121&borderColorActive=aaaaaa&bgImgOpacityActive=65&bgTextureActive=glass&bgColorActive=ffffff&iconColorHover=454545&fcHover=212121&borderColorHover=999999&bgImgOpacityHover=75&bgTextureHover=glass&bgColorHover=dadada&iconColorDefault=888888&fcDefault=555555&borderColorDefault=d3d3d3&bgImgOpacityDefault=75&bgTextureDefault=glass&bgColorDefault=e6e6e6&iconColorContent=222222&fcContent=222222&borderColorContent=aaaaaa&bgImgOpacityContent=75&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=222222&fcHeader=222222&borderColorHeader=aaaaaa&bgImgOpacityHeader=75&bgTextureHeader=highlight_soft&bgColorHeader=cccccc&cornerRadius=4px&fsDefault=1.1em&fwDefault=normal&ffDefault=Verdana%2CArial%2Csans-serif +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:before, +.ui-helper-clearfix:after { + content: ""; + display: table; + border-collapse: collapse; +} +.ui-helper-clearfix:after { + clear: both; +} +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); /* support: IE8 */ +} + +.ui-front { + z-index: 100; +} + + +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; + pointer-events: none; +} + + +/* Icons +----------------------------------*/ +.ui-icon { + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} + +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} + +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + -ms-touch-action: none; + touch-action: none; +} +.ui-resizable-disabled .ui-resizable-handle, +.ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +} +.ui-selectable { + -ms-touch-action: none; + touch-action: none; +} +.ui-selectable-helper { + position: absolute; + z-index: 100; + border: 1px dotted black; +} +.ui-sortable-handle { + -ms-touch-action: none; + touch-action: none; +} +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; +} +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: 0; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + cursor: pointer; + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-item-wrapper { + position: relative; + padding: 3px 1em 3px .4em; +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item-wrapper { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; + display: none; +} +.ui-datepicker .ui-datepicker-header { + position: relative; + padding: .2em 0; +} +.ui-datepicker .ui-datepicker-prev, +.ui-datepicker .ui-datepicker-next { + position: absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, +.ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left: 2px; +} +.ui-datepicker .ui-datepicker-next { + right: 2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left: 1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right: 1px; +} +.ui-datepicker .ui-datepicker-prev span, +.ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size: 1em; + margin: 1px 0; +} +.ui-datepicker select.ui-datepicker-month, +.ui-datepicker select.ui-datepicker-year { + width: 45%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin: 0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, +.ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding: 0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width: auto; + overflow: visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float: left; +} + +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width: auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float: left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width: 95%; + margin: 0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width: 50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width: 33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width: 25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width: 0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear: left; +} +.ui-datepicker-row-break { + clear: both; + width: 100%; + font-size: 0; +} + +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear: right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, +.ui-datepicker-rtl .ui-datepicker-group { + float: right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width: 0; + border-left-width: 1px; +} + +/* Icons */ +.ui-datepicker .ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; +} + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Verdana,Arial,sans-serif; + font-size: 1.1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Verdana,Arial,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #d3d3d3; +} +.ui-widget-content { + border: 1px solid #aaaaaa; + background: #ffffff; + color: #222222; +} +.ui-widget-content a { + color: #222222; +} +.ui-widget-header { + border: 1px solid #aaaaaa; + background: #cccccc url("./vendor/jquery/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x; + color: #222222; + font-weight: bold; +} +.ui-widget-header a { + color: #222222; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #d3d3d3; + background: #e6e6e6 url("./vendor/jquery/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #555555; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #555555; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #999999; + background: #dadada url("./vendor/jquery/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #212121; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #aaaaaa; + background: #ffffff url("./vendor/jquery/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x; + font-weight: normal; + color: #212121; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #aaaaaa; + background-color: #212121; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #212121; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #fcefa1; + background: #fbf9ee url("./vendor/jquery/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x; + color: #363636; +} +.ui-state-checked { + border: 1px solid #fcefa1; + background: #fbf9ee; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #363636; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #cd0a0a; + background: #fef1ec url("./vendor/jquery/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x; + color: #cd0a0a; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #cd0a0a; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #cd0a0a; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_222222_256x240.png"); +} +.ui-button .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_888888_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon, +.ui-state-default .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png"); +} +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_454545_256x240.png"); +} +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_2e83ff_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("./vendor/jquery/css/smoothness/images/ui-icons_cd0a0a_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 4px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 4px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .3; + filter: Alpha(Opacity=30); /* support: IE8 */ +} +.ui-widget-shadow { + -webkit-box-shadow: -8px -8px 8px #aaaaaa; + box-shadow: -8px -8px 8px #aaaaaa; +} diff --git a/public/vendor/jquery/js/jquery-ui-1.10.4.custom.js b/public/vendor/jquery/js/jquery-ui-1.10.4.custom.js deleted file mode 100644 index 60eb942eb2..0000000000 --- a/public/vendor/jquery/js/jquery-ui-1.10.4.custom.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-05-02 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.sortable.js, jquery.ui.autocomplete.js, jquery.ui.datepicker.js, jquery.ui.menu.js -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,a="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:a?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType;return{element:i,isWindow:s,isDocument:n,offset:i.offset()||{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:s?i.width():i.outerWidth(),height:s?i.height():i.outerHeight()}}},t.fn.position=function(e){if(!e||!e.of)return f.apply(this,arguments);e=t.extend({},e);var a,p,g,m,v,_,b=t(e.of),y=t.position.getWithinInfo(e.within),k=t.position.getScrollInfo(y),w=(e.collision||"flip").split(" "),D={};return _=n(b),b[0].preventDefault&&(e.at="left top"),p=_.width,g=_.height,m=_.offset,v=t.extend({},m),t.each(["my","at"],function(){var t,i,s=(e[this]||"").split(" ");1===s.length&&(s=h.test(s[0])?s.concat(["center"]):c.test(s[0])?["center"].concat(s):["center","center"]),s[0]=h.test(s[0])?s[0]:"center",s[1]=c.test(s[1])?s[1]:"center",t=u.exec(s[0]),i=u.exec(s[1]),D[this]=[t?t[0]:0,i?i[0]:0],e[this]=[d.exec(s[0])[0],d.exec(s[1])[0]]}),1===w.length&&(w[1]=w[0]),"right"===e.at[0]?v.left+=p:"center"===e.at[0]&&(v.left+=p/2),"bottom"===e.at[1]?v.top+=g:"center"===e.at[1]&&(v.top+=g/2),a=i(D.at,p,g),v.left+=a[0],v.top+=a[1],this.each(function(){var n,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=s(this,"marginLeft"),_=s(this,"marginTop"),x=u+f+s(this,"marginRight")+k.width,C=d+_+s(this,"marginBottom")+k.height,M=t.extend({},v),T=i(D.my,c.outerWidth(),c.outerHeight());"right"===e.my[0]?M.left-=u:"center"===e.my[0]&&(M.left-=u/2),"bottom"===e.my[1]?M.top-=d:"center"===e.my[1]&&(M.top-=d/2),M.left+=T[0],M.top+=T[1],t.support.offsetFractions||(M.left=l(M.left),M.top=l(M.top)),n={marginLeft:f,marginTop:_},t.each(["left","top"],function(i,s){t.ui.position[w[i]]&&t.ui.position[w[i]][s](M,{targetWidth:p,targetHeight:g,elemWidth:u,elemHeight:d,collisionPosition:n,collisionWidth:x,collisionHeight:C,offset:[a[0]+T[0],a[1]+T[1]],my:e.my,at:e.at,within:y,elem:c})}),e.using&&(h=function(t){var i=m.left-M.left,s=i+p-u,n=m.top-M.top,a=n+g-d,l={target:{element:b,left:m.left,top:m.top,width:p,height:g},element:{element:c,left:M.left,top:M.top,width:u,height:d},horizontal:0>s?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(t){t.widget("ui.draggable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"!==this.options.helper||/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(t(i.iframeFix===!0?"iframe":i.iframeFix).each(function(){t("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(t(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offsetParent=this.helper.offsetParent(),this.offsetParentCssPosition=this.offsetParent.css("position"),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.offset.scroll=!1,t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_mouseDrag:function(e,i){if("fixed"===this.offsetParentCssPosition&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp({}),!1;this.position=s.position}return this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"original"!==this.options.helper||t.contains(this.element[0].ownerDocument,this.element[0])?("invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1):!1},_mouseUp:function(e){return t("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return s.parents("body").length||s.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s[0]===this.element[0]||/(fixed|absolute)/.test(s.css("position"))||s.css("position","absolute"),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.element.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;return n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):"document"===n.containment?(this.containment=[0,0,t(document).width()-this.helperProportions.width-this.margins.left,(t(document).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],undefined):n.containment.constructor===Array?(this.containment=n.containment,undefined):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e="hidden"!==i.css("overflow"),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=i),undefined):(this.containment=null,undefined)},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent;return this.offset.scroll||(this.offset.scroll={top:n.scrollTop(),left:n.scrollLeft()}),{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top)*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)*s}},_generatePosition:function(e){var i,s,n,a,o=this.options,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=e.pageX,h=e.pageY;return this.offset.scroll||(this.offset.scroll={top:r.scrollTop(),left:r.scrollLeft()}),this.originalPosition&&(this.containment&&(this.relative_container?(s=this.relative_container.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,e.pageX-this.offset.click.left<i[0]&&(l=i[0]+this.offset.click.left),e.pageY-this.offset.click.top<i[1]&&(h=i[1]+this.offset.click.top),e.pageX-this.offset.click.left>i[2]&&(l=i[2]+this.offset.click.left),e.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),o.grid&&(n=o.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-o.grid[1]:n+o.grid[1]:n,a=o.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,l=i?a-this.offset.click.left>=i[0]||a-this.offset.click.left>i[2]?a:a-this.offset.click.left>=i[0]?a-o.grid[0]:a+o.grid[0]:a)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():this.offset.scroll.left)}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s]),"drag"===e&&(this.positionAbs=this._convertPositionTo("absolute")),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i){var s=t(this).data("ui-draggable"),n=s.options,a=t.extend({},i,{item:s.element});s.sortables=[],t(n.connectToSortable).each(function(){var i=t.data(this,"ui-sortable");i&&!i.options.disabled&&(s.sortables.push({instance:i,shouldRevert:i.options.revert}),i.refreshPositions(),i._trigger("activate",e,a))})},stop:function(e,i){var s=t(this).data("ui-draggable"),n=t.extend({},i,{item:s.element});t.each(s.sortables,function(){this.instance.isOver?(this.instance.isOver=0,s.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=this.shouldRevert),this.instance._mouseStop(e),this.instance.options.helper=this.instance.options._helper,"original"===s.options.helper&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",e,n))})},drag:function(e,i){var s=t(this).data("ui-draggable"),n=this;t.each(s.sortables,function(){var a=!1,o=this;this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(a=!0,t.each(s.sortables,function(){return this.instance.positionAbs=s.positionAbs,this.instance.helperProportions=s.helperProportions,this.instance.offset.click=s.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&t.contains(o.instance.element[0],this.instance.element[0])&&(a=!1),a})),a?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=t(n).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return i.helper[0]},e.target=this.instance.currentItem[0],this.instance._mouseCapture(e,!0),this.instance._mouseStart(e,!0,!0),this.instance.offset.click.top=s.offset.click.top,this.instance.offset.click.left=s.offset.click.left,this.instance.offset.parent.left-=s.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=s.offset.parent.top-this.instance.offset.parent.top,s._trigger("toSortable",e),s.dropped=this.instance.element,s.currentItem=s.element,this.instance.fromOutside=s),this.instance.currentItem&&this.instance._mouseDrag(e)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",e,this.instance._uiHash(this.instance)),this.instance._mouseStop(e,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),s._trigger("fromSortable",e),s.dropped=!1)})}}),t.ui.plugin.add("draggable","cursor",{start:function(){var e=t("body"),i=t(this).data("ui-draggable").options;e.css("cursor")&&(i._cursor=e.css("cursor")),e.css("cursor",i.cursor)},stop:function(){var e=t(this).data("ui-draggable").options;e._cursor&&t("body").css("cursor",e._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("opacity")&&(n._opacity=s.css("opacity")),s.css("opacity",n.opacity)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._opacity&&t(i.helper).css("opacity",s._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(){var e=t(this).data("ui-draggable");e.scrollParent[0]!==document&&"HTML"!==e.scrollParent[0].tagName&&(e.overflowOffset=e.scrollParent.offset())},drag:function(e){var i=t(this).data("ui-draggable"),s=i.options,n=!1;i.scrollParent[0]!==document&&"HTML"!==i.scrollParent[0].tagName?(s.axis&&"x"===s.axis||(i.overflowOffset.top+i.scrollParent[0].offsetHeight-e.pageY<s.scrollSensitivity?i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop+s.scrollSpeed:e.pageY-i.overflowOffset.top<s.scrollSensitivity&&(i.scrollParent[0].scrollTop=n=i.scrollParent[0].scrollTop-s.scrollSpeed)),s.axis&&"y"===s.axis||(i.overflowOffset.left+i.scrollParent[0].offsetWidth-e.pageX<s.scrollSensitivity?i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft+s.scrollSpeed:e.pageX-i.overflowOffset.left<s.scrollSensitivity&&(i.scrollParent[0].scrollLeft=n=i.scrollParent[0].scrollLeft-s.scrollSpeed))):(s.axis&&"x"===s.axis||(e.pageY-t(document).scrollTop()<s.scrollSensitivity?n=t(document).scrollTop(t(document).scrollTop()-s.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<s.scrollSensitivity&&(n=t(document).scrollTop(t(document).scrollTop()+s.scrollSpeed))),s.axis&&"y"===s.axis||(e.pageX-t(document).scrollLeft()<s.scrollSensitivity?n=t(document).scrollLeft(t(document).scrollLeft()-s.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<s.scrollSensitivity&&(n=t(document).scrollLeft(t(document).scrollLeft()+s.scrollSpeed)))),n!==!1&&t.ui.ddmanager&&!s.dropBehaviour&&t.ui.ddmanager.prepareOffsets(i,e)}}),t.ui.plugin.add("draggable","snap",{start:function(){var e=t(this).data("ui-draggable"),i=e.options;e.snapElements=[],t(i.snap.constructor!==String?i.snap.items||":data(ui-draggable)":i.snap).each(function(){var i=t(this),s=i.offset();this!==e.element[0]&&e.snapElements.push({item:this,width:i.outerWidth(),height:i.outerHeight(),top:s.top,left:s.left})})},drag:function(e,i){var s,n,a,o,r,l,h,c,u,d,p=t(this).data("ui-draggable"),g=p.options,f=g.snapTolerance,m=i.offset.left,_=m+p.helperProportions.width,v=i.offset.top,b=v+p.helperProportions.height;for(u=p.snapElements.length-1;u>=0;u--)r=p.snapElements[u].left,l=r+p.snapElements[u].width,h=p.snapElements[u].top,c=h+p.snapElements[u].height,r-f>_||m>l+f||h-f>b||v>c+f||!t.contains(p.snapElements[u].item.ownerDocument,p.snapElements[u].item)?(p.snapElements[u].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=!1):("inner"!==g.snapMode&&(s=f>=Math.abs(h-b),n=f>=Math.abs(c-v),a=f>=Math.abs(r-_),o=f>=Math.abs(l-m),s&&(i.position.top=p._convertPositionTo("relative",{top:h-p.helperProportions.height,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r-p.helperProportions.width}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l}).left-p.margins.left)),d=s||n||a||o,"outer"!==g.snapMode&&(s=f>=Math.abs(h-v),n=f>=Math.abs(c-b),a=f>=Math.abs(r-m),o=f>=Math.abs(l-_),s&&(i.position.top=p._convertPositionTo("relative",{top:h,left:0}).top-p.margins.top),n&&(i.position.top=p._convertPositionTo("relative",{top:c-p.helperProportions.height,left:0}).top-p.margins.top),a&&(i.position.left=p._convertPositionTo("relative",{top:0,left:r}).left-p.margins.left),o&&(i.position.left=p._convertPositionTo("relative",{top:0,left:l-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[u].snapping&&(s||n||a||o||d)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,e,t.extend(p._uiHash(),{snapItem:p.snapElements[u].item})),p.snapElements[u].snapping=s||n||a||o||d)}}),t.ui.plugin.add("draggable","stack",{start:function(){var e,i=this.data("ui-draggable").options,s=t.makeArray(t(i.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});s.length&&(e=parseInt(t(s[0]).css("zIndex"),10)||0,t(s).each(function(i){t(this).css("zIndex",e+i)}),this.css("zIndex",e+s.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i){var s=t(i.helper),n=t(this).data("ui-draggable").options;s.css("zIndex")&&(n._zIndex=s.css("zIndex")),s.css("zIndex",n.zIndex)},stop:function(e,i){var s=t(this).data("ui-draggable").options;s._zIndex&&t(i.helper).css("zIndex",s._zIndex)}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}t.widget("ui.droppable",{version:"1.10.4",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],undefined):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},t.ui.ddmanager.droppables[i.scope]=t.ui.ddmanager.droppables[i.scope]||[],t.ui.ddmanager.droppables[i.scope].push(this),i.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){for(var e=0,i=t.ui.ddmanager.droppables[this.options.scope];i.length>e;e++)i[e]===this&&i.splice(e,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(e,i){"accept"===e&&(this.accept=t.isFunction(i)?i:function(t){return t.is(i)}),t.Widget.prototype._setOption.apply(this,arguments)},_activate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var e=t.data(this,"ui-droppable");return e.options.greedy&&!e.options.disabled&&e.options.scope===s.options.scope&&e.accept.call(e.element[0],s.currentItem||s.element)&&t.ui.intersect(s,t.extend(e,{offset:e.element.offset()}),e.options.tolerance)?(n=!0,!1):undefined}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}}}),t.ui.intersect=function(t,i,s){if(!i.offset)return!1;var n,a,o=(t.positionAbs||t.position.absolute).left,r=(t.positionAbs||t.position.absolute).top,l=o+t.helperProportions.width,h=r+t.helperProportions.height,c=i.offset.left,u=i.offset.top,d=c+i.proportions().width,p=u+i.proportions().height;switch(s){case"fit":return o>=c&&d>=l&&r>=u&&p>=h;case"intersect":return o+t.helperProportions.width/2>c&&d>l-t.helperProportions.width/2&&r+t.helperProportions.height/2>u&&p>h-t.helperProportions.height/2;case"pointer":return n=(t.positionAbs||t.position.absolute).left+(t.clickOffset||t.offset.click).left,a=(t.positionAbs||t.position.absolute).top+(t.clickOffset||t.offset.click).top,e(a,u,i.proportions().height)&&e(n,c,i.proportions().width);case"touch":return(r>=u&&p>=r||h>=u&&p>=h||u>r&&h>p)&&(o>=c&&d>=o||l>=c&&d>=l||c>o&&l>d);default:return!1}},t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,a=t.ui.ddmanager.droppables[e.options.scope]||[],o=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;a.length>s;s++)if(!(a[s].options.disabled||e&&!a[s].accept.call(a[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===a[s].element[0]){a[s].proportions().height=0;continue t}a[s].visible="none"!==a[s].element.css("display"),a[s].visible&&("mousedown"===o&&a[s]._activate.call(a[s],i),a[s].offset=a[s].element.offset(),a[s].proportions({width:a[s].element[0].offsetWidth,height:a[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&t.ui.intersect(e,this,this.options.tolerance)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").bind("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,a,o=t.ui.intersect(e,this,this.options.tolerance),r=!o&&this.isover?"isout":o&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return t.data(this,"ui-droppable").options.scope===n}),a.length&&(s=t.data(a[0],"ui-droppable"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").unbind("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}}})(jQuery);(function(t){function e(t){return parseInt(t,10)||0}function i(t){return!isNaN(parseInt(t,10))}t.widget("ui.resizable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_create:function(){var e,i,s,n,a,o=this,r=this.options;if(this.element.addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!r.aspectRatio,aspectRatio:r.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:r.helper||r.ghost||r.animate?r.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(t("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.data("ui-resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=r.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),e=this.handles.split(","),this.handles={},i=0;e.length>i;i++)s=t.trim(e[i]),a="ui-resizable-"+s,n=t("<div class='ui-resizable-handle "+a+"'></div>"),n.css({zIndex:r.zIndex}),"se"===s&&n.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[s]=".ui-resizable-"+s,this.element.append(n);this._renderAxis=function(e){var i,s,n,a;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String&&(this.handles[i]=t(this.handles[i],this.element).show()),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)&&(s=t(this.handles[i],this.element),a=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,a),this._proportionallyResize()),t(this.handles[i]).length},this._renderAxis(this.element),this._handles=t(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){o.resizing||(this.className&&(n=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),o.axis=n&&n[1]?n[1]:"se")}),r.autoHide&&(this._handles.hide(),t(this.element).addClass("ui-resizable-autohide").mouseenter(function(){r.disabled||(t(this).removeClass("ui-resizable-autohide"),o._handles.show())}).mouseleave(function(){r.disabled||o.resizing||(t(this).addClass("ui-resizable-autohide"),o._handles.hide())})),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").removeData("ui-resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(i){var s,n,a,o=this.options,r=this.element.position(),h=this.element;return this.resizing=!0,/absolute/.test(h.css("position"))?h.css({position:"absolute",top:h.css("top"),left:h.css("left")}):h.is(".ui-draggable")&&h.css({position:"absolute",top:r.top,left:r.left}),this._renderProxy(),s=e(this.helper.css("left")),n=e(this.helper.css("top")),o.containment&&(s+=t(o.containment).scrollLeft()||0,n+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:s,top:n},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:h.width(),height:h.height()},this.originalSize=this._helper?{width:h.outerWidth(),height:h.outerHeight()}:{width:h.width(),height:h.height()},this.originalPosition={left:s,top:n},this.sizeDiff={width:h.outerWidth()-h.width(),height:h.outerHeight()-h.height()},this.originalMousePosition={left:i.pageX,top:i.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,a=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===a?this.axis+"-resize":a),h.addClass("ui-resizable-resizing"),this._propagate("start",i),!0},_mouseDrag:function(e){var i,s=this.helper,n={},a=this.originalMousePosition,o=this.axis,r=this.position.top,h=this.position.left,l=this.size.width,c=this.size.height,u=e.pageX-a.left||0,d=e.pageY-a.top||0,p=this._change[o];return p?(i=p.apply(this,[e,u,d]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),this.position.top!==r&&(n.top=this.position.top+"px"),this.position.left!==h&&(n.left=this.position.left+"px"),this.size.width!==l&&(n.width=this.size.width+"px"),this.size.height!==c&&(n.height=this.size.height+"px"),s.css(n),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(n)||this._trigger("resize",e,this.ui()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,a,o,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&t.ui.hasScroll(i[0],"left")?0:c.sizeDiff.height,a=s?0:c.sizeDiff.width,o={width:c.helper.width()-a,height:c.helper.height()-n},r=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null,h=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(o,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(t){var e,s,n,a,o,r=this.options;o={minWidth:i(r.minWidth)?r.minWidth:0,maxWidth:i(r.maxWidth)?r.maxWidth:1/0,minHeight:i(r.minHeight)?r.minHeight:0,maxHeight:i(r.maxHeight)?r.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,n=o.minWidth/this.aspectRatio,s=o.maxHeight*this.aspectRatio,a=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),n>o.minHeight&&(o.minHeight=n),o.maxWidth>s&&(o.maxWidth=s),o.maxHeight>a&&(o.maxHeight=a)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),i(t.left)&&(this.position.left=t.left),i(t.top)&&(this.position.top=t.top),i(t.height)&&(this.size.height=t.height),i(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,s=this.size,n=this.axis;return i(t.height)?t.width=t.height*this.aspectRatio:i(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===n&&(t.left=e.left+(s.width-t.width),t.top=null),"nw"===n&&(t.top=e.top+(s.height-t.height),t.left=e.left+(s.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,s=this.axis,n=i(t.width)&&e.maxWidth&&e.maxWidth<t.width,a=i(t.height)&&e.maxHeight&&e.maxHeight<t.height,o=i(t.width)&&e.minWidth&&e.minWidth>t.width,r=i(t.height)&&e.minHeight&&e.minHeight>t.height,h=this.originalPosition.left+this.originalSize.width,l=this.position.top+this.size.height,c=/sw|nw|w/.test(s),u=/nw|ne|n/.test(s);return o&&(t.width=e.minWidth),r&&(t.height=e.minHeight),n&&(t.width=e.maxWidth),a&&(t.height=e.maxHeight),o&&c&&(t.left=h-e.minWidth),n&&c&&(t.left=h-e.maxWidth),r&&u&&(t.top=l-e.minHeight),a&&u&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_proportionallyResize:function(){if(this._proportionallyResizeElements.length){var t,e,i,s,n,a=this.helper||this.element;for(t=0;this._proportionallyResizeElements.length>t;t++){if(n=this._proportionallyResizeElements[t],!this.borderDif)for(this.borderDif=[],i=[n.css("borderTopWidth"),n.css("borderRightWidth"),n.css("borderBottomWidth"),n.css("borderLeftWidth")],s=[n.css("paddingTop"),n.css("paddingRight"),n.css("paddingBottom"),n.css("paddingLeft")],e=0;i.length>e;e++)this.borderDif[e]=(parseInt(i[e],10)||0)+(parseInt(s[e],10)||0);n.css({height:a.height()-this.borderDif[0]-this.borderDif[2]||0,width:a.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("<div style='overflow:hidden;'></div>"),this.helper.addClass(this._helper).css({width:this.element.outerWidth()-1,height:this.element.outerHeight()-1,position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).data("ui-resizable"),s=i.options,n=i._proportionallyResizeElements,a=n.length&&/textarea/i.test(n[0].nodeName),o=a&&t.ui.hasScroll(n[0],"left")?0:i.sizeDiff.height,r=a?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-o},l=parseInt(i.element.css("left"),10)+(i.position.left-i.originalPosition.left)||null,c=parseInt(i.element.css("top"),10)+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseInt(i.element.css("width"),10),height:parseInt(i.element.css("height"),10),top:parseInt(i.element.css("top"),10),left:parseInt(i.element.css("left"),10)};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var i,s,n,a,o,r,h,l=t(this).data("ui-resizable"),c=l.options,u=l.element,d=c.containment,p=d instanceof t?d.get(0):/parent/.test(d)?u.parent().get(0):d;p&&(l.containerElement=t(p),/document/.test(d)||d===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(i=t(p),s=[],t(["Top","Right","Left","Bottom"]).each(function(t,n){s[t]=e(i.css("padding"+n))}),l.containerOffset=i.offset(),l.containerPosition=i.position(),l.containerSize={height:i.innerHeight()-s[3],width:i.innerWidth()-s[1]},n=l.containerOffset,a=l.containerSize.height,o=l.containerSize.width,r=t.ui.hasScroll(p,"left")?p.scrollWidth:o,h=t.ui.hasScroll(p)?p.scrollHeight:a,l.parentData={element:p,left:n.left,top:n.top,width:r,height:h}))},resize:function(e){var i,s,n,a,o=t(this).data("ui-resizable"),r=o.options,h=o.containerOffset,l=o.position,c=o._aspectRatio||e.shiftKey,u={top:0,left:0},d=o.containerElement;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(o._helper?h.left:0)&&(o.size.width=o.size.width+(o._helper?o.position.left-h.left:o.position.left-u.left),c&&(o.size.height=o.size.width/o.aspectRatio),o.position.left=r.helper?h.left:0),l.top<(o._helper?h.top:0)&&(o.size.height=o.size.height+(o._helper?o.position.top-h.top:o.position.top),c&&(o.size.width=o.size.height*o.aspectRatio),o.position.top=o._helper?h.top:0),o.offset.left=o.parentData.left+o.position.left,o.offset.top=o.parentData.top+o.position.top,i=Math.abs((o._helper?o.offset.left-u.left:o.offset.left-u.left)+o.sizeDiff.width),s=Math.abs((o._helper?o.offset.top-u.top:o.offset.top-h.top)+o.sizeDiff.height),n=o.containerElement.get(0)===o.element.parent().get(0),a=/relative|absolute/.test(o.containerElement.css("position")),n&&a&&(i-=Math.abs(o.parentData.left)),i+o.size.width>=o.parentData.width&&(o.size.width=o.parentData.width-i,c&&(o.size.height=o.size.width/o.aspectRatio)),s+o.size.height>=o.parentData.height&&(o.size.height=o.parentData.height-s,c&&(o.size.width=o.size.height*o.aspectRatio))},stop:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.containerOffset,n=e.containerPosition,a=e.containerElement,o=t(e.helper),r=o.offset(),h=o.outerWidth()-e.sizeDiff.width,l=o.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(a.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=function(e){t(e).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseInt(e.width(),10),height:parseInt(e.height(),10),left:parseInt(e.css("left"),10),top:parseInt(e.css("top"),10)})})};"object"!=typeof i.alsoResize||i.alsoResize.parentNode?s(i.alsoResize):i.alsoResize.length?(i.alsoResize=i.alsoResize[0],s(i.alsoResize)):t.each(i.alsoResize,function(t){s(t)})},resize:function(e,i){var s=t(this).data("ui-resizable"),n=s.options,a=s.originalSize,o=s.originalPosition,r={height:s.size.height-a.height||0,width:s.size.width-a.width||0,top:s.position.top-o.top||0,left:s.position.left-o.left||0},h=function(e,s){t(e).each(function(){var e=t(this),n=t(this).data("ui-resizable-alsoresize"),a={},o=s&&s.length?s:e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(n[e]||0)+(r[e]||0);i&&i>=0&&(a[e]=i||null)}),e.css(a)})};"object"!=typeof n.alsoResize||n.alsoResize.nodeType?h(n.alsoResize):t.each(n.alsoResize,function(t,e){h(t,e)})},stop:function(){t(this).removeData("resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:s.height,width:s.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass("string"==typeof i.ghost?i.ghost:""),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).data("ui-resizable");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).data("ui-resizable");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e=t(this).data("ui-resizable"),i=e.options,s=e.size,n=e.originalSize,a=e.originalPosition,o=e.axis,r="number"==typeof i.grid?[i.grid,i.grid]:i.grid,h=r[0]||1,l=r[1]||1,c=Math.round((s.width-n.width)/h)*h,u=Math.round((s.height-n.height)/l)*l,d=n.width+c,p=n.height+u,f=i.maxWidth&&d>i.maxWidth,g=i.maxHeight&&p>i.maxHeight,m=i.minWidth&&i.minWidth>d,v=i.minHeight&&i.minHeight>p;i.grid=r,m&&(d+=h),v&&(p+=l),f&&(d-=h),g&&(p-=l),/^(se|s|e)$/.test(o)?(e.size.width=d,e.size.height=p):/^(ne)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.top=a.top-u):/^(sw)$/.test(o)?(e.size.width=d,e.size.height=p,e.position.left=a.left-c):(p-l>0?(e.size.height=p,e.position.top=a.top-u):(e.size.height=l,e.position.top=a.top+n.height-l),d-h>0?(e.size.width=d,e.position.left=a.left-c):(e.size.width=h,e.position.left=a.left+n.width-h))}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):undefined}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("<style>*{ cursor: "+a.cursor+" !important; }</style>").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<a.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+a.scrollSpeed:e.pageY-this.overflowOffset.top<a.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-a.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<a.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+a.scrollSpeed:e.pageX-this.overflowOffset.left<a.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-a.scrollSpeed)):(e.pageY-t(document).scrollTop()<a.scrollSensitivity?r=t(document).scrollTop(t(document).scrollTop()-a.scrollSpeed):t(window).height()-(e.pageY-t(document).scrollTop())<a.scrollSensitivity&&(r=t(document).scrollTop(t(document).scrollTop()+a.scrollSpeed)),e.pageX-t(document).scrollLeft()<a.scrollSensitivity?r=t(document).scrollLeft(t(document).scrollLeft()-a.scrollSpeed):t(window).width()-(e.pageX-t(document).scrollLeft())<a.scrollSensitivity&&(r=t(document).scrollLeft(t(document).scrollLeft()+a.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t("<td> </td>",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(o=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(a=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("<ul>").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("<span>",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length<this.options.minLength?this.close(t):this._trigger("search",t)!==!1?this._search(e):undefined},_search:function(e){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:e},this._response())},_response:function(){var t=++this.requestIndex;return e.proxy(function(e){t===this.requestIndex&&this.__response(e),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},this)},__response:function(e){e&&(e=this._normalize(e)),this._trigger("response",null,{content:e}),!this.options.disabled&&e&&e.length&&!this.cancelSearch?(this._suggest(e),this._trigger("open")):this._close()},close:function(e){this.cancelSearch=!0,this._close(e)},_close:function(e){this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",e))},_change:function(e){this.previous!==this._value()&&this._trigger("change",e,{item:this.selectedItem})},_normalize:function(t){return t.length&&t[0].label&&t[0].value?t:e.map(t,function(t){return"string"==typeof t?{label:t,value:t}:e.extend({label:t.label||t.value,value:t.value||t.label},t)})},_suggest:function(t){var i=this.menu.element.empty();this._renderMenu(i,t),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(e.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next()},_resizeMenu:function(){var e=this.menu.element;e.outerWidth(Math.max(e.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(t,i){var s=this;e.each(i,function(e,i){s._renderItemData(t,i)})},_renderItemData:function(e,t){return this._renderItem(e,t).data("ui-autocomplete-item",t)},_renderItem:function(t,i){return e("<li>").append(e("<a>").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(e,t){function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},e.extend(this._defaults,this.regional[""]),this.dpDiv=a(e("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function a(t){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return t.delegate(i,"mouseout",function(){e(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).removeClass("ui-datepicker-next-hover")}).delegate(i,"mouseover",function(){e.datepicker._isDisabledDatepicker(n.inline?t.parent()[0]:n.input[0])||(e(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),e(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&e(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&e(this).addClass("ui-datepicker-next-hover"))})}function s(t,i){e.extend(t,i);for(var a in i)null==i[a]&&(t[a]=i[a]);return t}e.extend(e.ui,{datepicker:{version:"1.10.4"}});var n,r="datepicker";e.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(e){return s(this._defaults,e||{}),this},_attachDatepicker:function(t,i){var a,s,n;a=t.nodeName.toLowerCase(),s="div"===a||"span"===a,t.id||(this.uuid+=1,t.id="dp"+this.uuid),n=this._newInst(e(t),s),n.settings=e.extend({},i||{}),"input"===a?this._connectDatepicker(t,n):s&&this._inlineDatepicker(t,n)},_newInst:function(t,i){var s=t[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:s,input:t,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?a(e("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(t,i){var a=e(t);i.append=e([]),i.trigger=e([]),a.hasClass(this.markerClassName)||(this._attachments(a,i),a.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp),this._autoSize(i),e.data(t,r,i),i.settings.disabled&&this._disableDatepicker(t))},_attachments:function(t,i){var a,s,n,r=this._get(i,"appendText"),o=this._get(i,"isRTL");i.append&&i.append.remove(),r&&(i.append=e("<span class='"+this._appendClass+"'>"+r+"</span>"),t[o?"before":"after"](i.append)),t.unbind("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),a=this._get(i,"showOn"),("focus"===a||"both"===a)&&t.focus(this._showDatepicker),("button"===a||"both"===a)&&(s=this._get(i,"buttonText"),n=this._get(i,"buttonImage"),i.trigger=e(this._get(i,"buttonImageOnly")?e("<img/>").addClass(this._triggerClass).attr({src:n,alt:s,title:s}):e("<button type='button'></button>").addClass(this._triggerClass).html(n?e("<img/>").attr({src:n,alt:s,title:s}):s)),t[o?"before":"after"](i.trigger),i.trigger.click(function(){return e.datepicker._datepickerShowing&&e.datepicker._lastInput===t[0]?e.datepicker._hideDatepicker():e.datepicker._datepickerShowing&&e.datepicker._lastInput!==t[0]?(e.datepicker._hideDatepicker(),e.datepicker._showDatepicker(t[0])):e.datepicker._showDatepicker(t[0]),!1}))},_autoSize:function(e){if(this._get(e,"autoSize")&&!e.inline){var t,i,a,s,n=new Date(2009,11,20),r=this._get(e,"dateFormat");r.match(/[DM]/)&&(t=function(e){for(i=0,a=0,s=0;e.length>s;s++)e[s].length>i&&(i=e[s].length,a=s);return a},n.setMonth(t(this._get(e,r.match(/MM/)?"monthNames":"monthNamesShort"))),n.setDate(t(this._get(e,r.match(/DD/)?"dayNames":"dayNamesShort"))+20-n.getDay())),e.input.attr("size",this._formatDate(e,n).length)}},_inlineDatepicker:function(t,i){var a=e(t);a.hasClass(this.markerClassName)||(a.addClass(this.markerClassName).append(i.dpDiv),e.data(t,r,i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(t),i.dpDiv.css("display","block"))},_dialogDatepicker:function(t,i,a,n,o){var u,c,h,l,d,p=this._dialogInst;return p||(this.uuid+=1,u="dp"+this.uuid,this._dialogInput=e("<input type='text' id='"+u+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.keydown(this._doKeyDown),e("body").append(this._dialogInput),p=this._dialogInst=this._newInst(this._dialogInput,!1),p.settings={},e.data(this._dialogInput[0],r,p)),s(p.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(p,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(c=document.documentElement.clientWidth,h=document.documentElement.clientHeight,l=document.documentElement.scrollLeft||document.body.scrollLeft,d=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[c/2-100+l,h/2-150+d]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),p.settings.onSelect=a,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),e.blockUI&&e.blockUI(this.dpDiv),e.data(this._dialogInput[0],r,p),this},_destroyDatepicker:function(t){var i,a=e(t),s=e.data(t,r);a.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),e.removeData(t,r),"input"===i?(s.append.remove(),s.trigger.remove(),a.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):("div"===i||"span"===i)&&a.removeClass(this.markerClassName).empty())},_enableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!1,n.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().removeClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}))},_disableDatepicker:function(t){var i,a,s=e(t),n=e.data(t,r);s.hasClass(this.markerClassName)&&(i=t.nodeName.toLowerCase(),"input"===i?(t.disabled=!0,n.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(a=s.children("."+this._inlineClass),a.children().addClass("ui-state-disabled"),a.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=e.map(this._disabledInputs,function(e){return e===t?null:e}),this._disabledInputs[this._disabledInputs.length]=t)},_isDisabledDatepicker:function(e){if(!e)return!1;for(var t=0;this._disabledInputs.length>t;t++)if(this._disabledInputs[t]===e)return!0;return!1},_getInst:function(t){try{return e.data(t,r)}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(i,a,n){var r,o,u,c,h=this._getInst(i);return 2===arguments.length&&"string"==typeof a?"defaults"===a?e.extend({},e.datepicker._defaults):h?"all"===a?e.extend({},h.settings):this._get(h,a):null:(r=a||{},"string"==typeof a&&(r={},r[a]=n),h&&(this._curInst===h&&this._hideDatepicker(),o=this._getDateDatepicker(i,!0),u=this._getMinMaxDate(h,"min"),c=this._getMinMaxDate(h,"max"),s(h.settings,r),null!==u&&r.dateFormat!==t&&r.minDate===t&&(h.settings.minDate=this._formatDate(h,u)),null!==c&&r.dateFormat!==t&&r.maxDate===t&&(h.settings.maxDate=this._formatDate(h,c)),"disabled"in r&&(r.disabled?this._disableDatepicker(i):this._enableDatepicker(i)),this._attachments(e(i),h),this._autoSize(h),this._setDate(h,o),this._updateAlternate(h),this._updateDatepicker(h)),t)},_changeDatepicker:function(e,t,i){this._optionDatepicker(e,t,i)},_refreshDatepicker:function(e){var t=this._getInst(e);t&&this._updateDatepicker(t)},_setDateDatepicker:function(e,t){var i=this._getInst(e);i&&(this._setDate(i,t),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(e,t){var i=this._getInst(e);return i&&!i.inline&&this._setDateFromField(i,t),i?this._getDate(i):null},_doKeyDown:function(t){var i,a,s,n=e.datepicker._getInst(t.target),r=!0,o=n.dpDiv.is(".ui-datepicker-rtl");if(n._keyEvent=!0,e.datepicker._datepickerShowing)switch(t.keyCode){case 9:e.datepicker._hideDatepicker(),r=!1;break;case 13:return s=e("td."+e.datepicker._dayOverClass+":not(."+e.datepicker._currentClass+")",n.dpDiv),s[0]&&e.datepicker._selectDay(t.target,n.selectedMonth,n.selectedYear,s[0]),i=e.datepicker._get(n,"onSelect"),i?(a=e.datepicker._formatDate(n),i.apply(n.input?n.input[0]:null,[a,n])):e.datepicker._hideDatepicker(),!1;case 27:e.datepicker._hideDatepicker();break;case 33:e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 34:e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 35:(t.ctrlKey||t.metaKey)&&e.datepicker._clearDate(t.target),r=t.ctrlKey||t.metaKey;break;case 36:(t.ctrlKey||t.metaKey)&&e.datepicker._gotoToday(t.target),r=t.ctrlKey||t.metaKey;break;case 37:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?1:-1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?-e.datepicker._get(n,"stepBigMonths"):-e.datepicker._get(n,"stepMonths"),"M");break;case 38:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,-7,"D"),r=t.ctrlKey||t.metaKey;break;case 39:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,o?-1:1,"D"),r=t.ctrlKey||t.metaKey,t.originalEvent.altKey&&e.datepicker._adjustDate(t.target,t.ctrlKey?+e.datepicker._get(n,"stepBigMonths"):+e.datepicker._get(n,"stepMonths"),"M");break;case 40:(t.ctrlKey||t.metaKey)&&e.datepicker._adjustDate(t.target,7,"D"),r=t.ctrlKey||t.metaKey;break;default:r=!1}else 36===t.keyCode&&t.ctrlKey?e.datepicker._showDatepicker(this):r=!1;r&&(t.preventDefault(),t.stopPropagation())},_doKeyPress:function(i){var a,s,n=e.datepicker._getInst(i.target);return e.datepicker._get(n,"constrainInput")?(a=e.datepicker._possibleChars(e.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==i.charCode?i.keyCode:i.charCode),i.ctrlKey||i.metaKey||" ">s||!a||a.indexOf(s)>-1):t},_doKeyUp:function(t){var i,a=e.datepicker._getInst(t.target);if(a.input.val()!==a.lastVal)try{i=e.datepicker.parseDate(e.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,e.datepicker._getFormatConfig(a)),i&&(e.datepicker._setDateFromField(a),e.datepicker._updateAlternate(a),e.datepicker._updateDatepicker(a))}catch(s){}return!0},_showDatepicker:function(t){if(t=t.target||t,"input"!==t.nodeName.toLowerCase()&&(t=e("input",t.parentNode)[0]),!e.datepicker._isDisabledDatepicker(t)&&e.datepicker._lastInput!==t){var i,a,n,r,o,u,c;i=e.datepicker._getInst(t),e.datepicker._curInst&&e.datepicker._curInst!==i&&(e.datepicker._curInst.dpDiv.stop(!0,!0),i&&e.datepicker._datepickerShowing&&e.datepicker._hideDatepicker(e.datepicker._curInst.input[0])),a=e.datepicker._get(i,"beforeShow"),n=a?a.apply(t,[t,i]):{},n!==!1&&(s(i.settings,n),i.lastVal=null,e.datepicker._lastInput=t,e.datepicker._setDateFromField(i),e.datepicker._inDialog&&(t.value=""),e.datepicker._pos||(e.datepicker._pos=e.datepicker._findPos(t),e.datepicker._pos[1]+=t.offsetHeight),r=!1,e(t).parents().each(function(){return r|="fixed"===e(this).css("position"),!r}),o={left:e.datepicker._pos[0],top:e.datepicker._pos[1]},e.datepicker._pos=null,i.dpDiv.empty(),i.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),e.datepicker._updateDatepicker(i),o=e.datepicker._checkOffset(i,o,r),i.dpDiv.css({position:e.datepicker._inDialog&&e.blockUI?"static":r?"fixed":"absolute",display:"none",left:o.left+"px",top:o.top+"px"}),i.inline||(u=e.datepicker._get(i,"showAnim"),c=e.datepicker._get(i,"duration"),i.dpDiv.zIndex(e(t).zIndex()+1),e.datepicker._datepickerShowing=!0,e.effects&&e.effects.effect[u]?i.dpDiv.show(u,e.datepicker._get(i,"showOptions"),c):i.dpDiv[u||"show"](u?c:null),e.datepicker._shouldFocusInput(i)&&i.input.focus(),e.datepicker._curInst=i))}},_updateDatepicker:function(t){this.maxRows=4,n=t,t.dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t),t.dpDiv.find("."+this._dayOverClass+" a").mouseover();var i,a=this._getNumberOfMonths(t),s=a[1],r=17;t.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),s>1&&t.dpDiv.addClass("ui-datepicker-multi-"+s).css("width",r*s+"em"),t.dpDiv[(1!==a[0]||1!==a[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),t.dpDiv[(this._get(t,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),t===e.datepicker._curInst&&e.datepicker._datepickerShowing&&e.datepicker._shouldFocusInput(t)&&t.input.focus(),t.yearshtml&&(i=t.yearshtml,setTimeout(function(){i===t.yearshtml&&t.yearshtml&&t.dpDiv.find("select.ui-datepicker-year:first").replaceWith(t.yearshtml),i=t.yearshtml=null},0))},_shouldFocusInput:function(e){return e.input&&e.input.is(":visible")&&!e.input.is(":disabled")&&!e.input.is(":focus")},_checkOffset:function(t,i,a){var s=t.dpDiv.outerWidth(),n=t.dpDiv.outerHeight(),r=t.input?t.input.outerWidth():0,o=t.input?t.input.outerHeight():0,u=document.documentElement.clientWidth+(a?0:e(document).scrollLeft()),c=document.documentElement.clientHeight+(a?0:e(document).scrollTop());return i.left-=this._get(t,"isRTL")?s-r:0,i.left-=a&&i.left===t.input.offset().left?e(document).scrollLeft():0,i.top-=a&&i.top===t.input.offset().top+o?e(document).scrollTop():0,i.left-=Math.min(i.left,i.left+s>u&&u>s?Math.abs(i.left+s-u):0),i.top-=Math.min(i.top,i.top+n>c&&c>n?Math.abs(n+o):0),i},_findPos:function(t){for(var i,a=this._getInst(t),s=this._get(a,"isRTL");t&&("hidden"===t.type||1!==t.nodeType||e.expr.filters.hidden(t));)t=t[s?"previousSibling":"nextSibling"];return i=e(t).offset(),[i.left,i.top]},_hideDatepicker:function(t){var i,a,s,n,o=this._curInst;!o||t&&o!==e.data(t,r)||this._datepickerShowing&&(i=this._get(o,"showAnim"),a=this._get(o,"duration"),s=function(){e.datepicker._tidyDialog(o)},e.effects&&(e.effects.effect[i]||e.effects[i])?o.dpDiv.hide(i,e.datepicker._get(o,"showOptions"),a,s):o.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?a:null,s),i||s(),this._datepickerShowing=!1,n=this._get(o,"onClose"),n&&n.apply(o.input?o.input[0]:null,[o.input?o.input.val():"",o]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),e.blockUI&&(e.unblockUI(),e("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(e){e.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(t){if(e.datepicker._curInst){var i=e(t.target),a=e.datepicker._getInst(i[0]);(i[0].id!==e.datepicker._mainDivId&&0===i.parents("#"+e.datepicker._mainDivId).length&&!i.hasClass(e.datepicker.markerClassName)&&!i.closest("."+e.datepicker._triggerClass).length&&e.datepicker._datepickerShowing&&(!e.datepicker._inDialog||!e.blockUI)||i.hasClass(e.datepicker.markerClassName)&&e.datepicker._curInst!==a)&&e.datepicker._hideDatepicker()}},_adjustDate:function(t,i,a){var s=e(t),n=this._getInst(s[0]);this._isDisabledDatepicker(s[0])||(this._adjustInstDate(n,i+("M"===a?this._get(n,"showCurrentAtPos"):0),a),this._updateDatepicker(n))},_gotoToday:function(t){var i,a=e(t),s=this._getInst(a[0]);this._get(s,"gotoCurrent")&&s.currentDay?(s.selectedDay=s.currentDay,s.drawMonth=s.selectedMonth=s.currentMonth,s.drawYear=s.selectedYear=s.currentYear):(i=new Date,s.selectedDay=i.getDate(),s.drawMonth=s.selectedMonth=i.getMonth(),s.drawYear=s.selectedYear=i.getFullYear()),this._notifyChange(s),this._adjustDate(a)},_selectMonthYear:function(t,i,a){var s=e(t),n=this._getInst(s[0]);n["selected"+("M"===a?"Month":"Year")]=n["draw"+("M"===a?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(n),this._adjustDate(s)},_selectDay:function(t,i,a,s){var n,r=e(t);e(s).hasClass(this._unselectableClass)||this._isDisabledDatepicker(r[0])||(n=this._getInst(r[0]),n.selectedDay=n.currentDay=e("a",s).html(),n.selectedMonth=n.currentMonth=i,n.selectedYear=n.currentYear=a,this._selectDate(t,this._formatDate(n,n.currentDay,n.currentMonth,n.currentYear)))},_clearDate:function(t){var i=e(t);this._selectDate(i,"")},_selectDate:function(t,i){var a,s=e(t),n=this._getInst(s[0]);i=null!=i?i:this._formatDate(n),n.input&&n.input.val(i),this._updateAlternate(n),a=this._get(n,"onSelect"),a?a.apply(n.input?n.input[0]:null,[i,n]):n.input&&n.input.trigger("change"),n.inline?this._updateDatepicker(n):(this._hideDatepicker(),this._lastInput=n.input[0],"object"!=typeof n.input[0]&&n.input.focus(),this._lastInput=null)},_updateAlternate:function(t){var i,a,s,n=this._get(t,"altField");n&&(i=this._get(t,"altFormat")||this._get(t,"dateFormat"),a=this._getDate(t),s=this.formatDate(i,a,this._getFormatConfig(t)),e(n).each(function(){e(this).val(s)}))},noWeekends:function(e){var t=e.getDay();return[t>0&&6>t,""]},iso8601Week:function(e){var t,i=new Date(e.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),t=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((t-i)/864e5)/7)+1},parseDate:function(i,a,s){if(null==i||null==a)throw"Invalid arguments";if(a="object"==typeof a?""+a:a+"",""===a)return null;var n,r,o,u,c=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,l="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),d=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,p=(s?s.dayNames:null)||this._defaults.dayNames,g=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,m=(s?s.monthNames:null)||this._defaults.monthNames,f=-1,_=-1,v=-1,k=-1,y=!1,b=function(e){var t=i.length>n+1&&i.charAt(n+1)===e;return t&&n++,t},D=function(e){var t=b(e),i="@"===e?14:"!"===e?20:"y"===e&&t?4:"o"===e?3:2,s=RegExp("^\\d{1,"+i+"}"),n=a.substring(c).match(s);if(!n)throw"Missing number at position "+c;return c+=n[0].length,parseInt(n[0],10)},w=function(i,s,n){var r=-1,o=e.map(b(i)?n:s,function(e,t){return[[t,e]]}).sort(function(e,t){return-(e[1].length-t[1].length)});if(e.each(o,function(e,i){var s=i[1];return a.substr(c,s.length).toLowerCase()===s.toLowerCase()?(r=i[0],c+=s.length,!1):t}),-1!==r)return r+1;throw"Unknown name at position "+c},M=function(){if(a.charAt(c)!==i.charAt(n))throw"Unexpected literal at position "+c;c++};for(n=0;i.length>n;n++)if(y)"'"!==i.charAt(n)||b("'")?M():y=!1;else switch(i.charAt(n)){case"d":v=D("d");break;case"D":w("D",d,p);break;case"o":k=D("o");break;case"m":_=D("m");break;case"M":_=w("M",g,m);break;case"y":f=D("y");break;case"@":u=new Date(D("@")),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"!":u=new Date((D("!")-this._ticksTo1970)/1e4),f=u.getFullYear(),_=u.getMonth()+1,v=u.getDate();break;case"'":b("'")?M():y=!0;break;default:M()}if(a.length>c&&(o=a.substr(c),!/^\s+/.test(o)))throw"Extra/unparsed characters found in date: "+o;if(-1===f?f=(new Date).getFullYear():100>f&&(f+=(new Date).getFullYear()-(new Date).getFullYear()%100+(l>=f?0:-100)),k>-1)for(_=1,v=k;;){if(r=this._getDaysInMonth(f,_-1),r>=v)break;_++,v-=r}if(u=this._daylightSavingAdjust(new Date(f,_-1,v)),u.getFullYear()!==f||u.getMonth()+1!==_||u.getDate()!==v)throw"Invalid date";return u},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(e,t,i){if(!t)return"";var a,s=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,n=(i?i.dayNames:null)||this._defaults.dayNames,r=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,o=(i?i.monthNames:null)||this._defaults.monthNames,u=function(t){var i=e.length>a+1&&e.charAt(a+1)===t;return i&&a++,i},c=function(e,t,i){var a=""+t;if(u(e))for(;i>a.length;)a="0"+a;return a},h=function(e,t,i,a){return u(e)?a[t]:i[t]},l="",d=!1;if(t)for(a=0;e.length>a;a++)if(d)"'"!==e.charAt(a)||u("'")?l+=e.charAt(a):d=!1;else switch(e.charAt(a)){case"d":l+=c("d",t.getDate(),2);break;case"D":l+=h("D",t.getDay(),s,n);break;case"o":l+=c("o",Math.round((new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime()-new Date(t.getFullYear(),0,0).getTime())/864e5),3);break;case"m":l+=c("m",t.getMonth()+1,2);break;case"M":l+=h("M",t.getMonth(),r,o);break;case"y":l+=u("y")?t.getFullYear():(10>t.getYear()%100?"0":"")+t.getYear()%100;break;case"@":l+=t.getTime();break;case"!":l+=1e4*t.getTime()+this._ticksTo1970;break;case"'":u("'")?l+="'":d=!0;break;default:l+=e.charAt(a)}return l},_possibleChars:function(e){var t,i="",a=!1,s=function(i){var a=e.length>t+1&&e.charAt(t+1)===i;return a&&t++,a};for(t=0;e.length>t;t++)if(a)"'"!==e.charAt(t)||s("'")?i+=e.charAt(t):a=!1;else switch(e.charAt(t)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":s("'")?i+="'":a=!0;break;default:i+=e.charAt(t)}return i},_get:function(e,i){return e.settings[i]!==t?e.settings[i]:this._defaults[i]},_setDateFromField:function(e,t){if(e.input.val()!==e.lastVal){var i=this._get(e,"dateFormat"),a=e.lastVal=e.input?e.input.val():null,s=this._getDefaultDate(e),n=s,r=this._getFormatConfig(e);try{n=this.parseDate(i,a,r)||s}catch(o){a=t?"":a}e.selectedDay=n.getDate(),e.drawMonth=e.selectedMonth=n.getMonth(),e.drawYear=e.selectedYear=n.getFullYear(),e.currentDay=a?n.getDate():0,e.currentMonth=a?n.getMonth():0,e.currentYear=a?n.getFullYear():0,this._adjustInstDate(e)}},_getDefaultDate:function(e){return this._restrictMinMax(e,this._determineDate(e,this._get(e,"defaultDate"),new Date))},_determineDate:function(t,i,a){var s=function(e){var t=new Date;return t.setDate(t.getDate()+e),t},n=function(i){try{return e.datepicker.parseDate(e.datepicker._get(t,"dateFormat"),i,e.datepicker._getFormatConfig(t))}catch(a){}for(var s=(i.toLowerCase().match(/^c/)?e.datepicker._getDate(t):null)||new Date,n=s.getFullYear(),r=s.getMonth(),o=s.getDate(),u=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,c=u.exec(i);c;){switch(c[2]||"d"){case"d":case"D":o+=parseInt(c[1],10);break;case"w":case"W":o+=7*parseInt(c[1],10);break;case"m":case"M":r+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r));break;case"y":case"Y":n+=parseInt(c[1],10),o=Math.min(o,e.datepicker._getDaysInMonth(n,r))}c=u.exec(i)}return new Date(n,r,o)},r=null==i||""===i?a:"string"==typeof i?n(i):"number"==typeof i?isNaN(i)?a:s(i):new Date(i.getTime());return r=r&&"Invalid Date"==""+r?a:r,r&&(r.setHours(0),r.setMinutes(0),r.setSeconds(0),r.setMilliseconds(0)),this._daylightSavingAdjust(r)},_daylightSavingAdjust:function(e){return e?(e.setHours(e.getHours()>12?e.getHours()+2:0),e):null},_setDate:function(e,t,i){var a=!t,s=e.selectedMonth,n=e.selectedYear,r=this._restrictMinMax(e,this._determineDate(e,t,new Date));e.selectedDay=e.currentDay=r.getDate(),e.drawMonth=e.selectedMonth=e.currentMonth=r.getMonth(),e.drawYear=e.selectedYear=e.currentYear=r.getFullYear(),s===e.selectedMonth&&n===e.selectedYear||i||this._notifyChange(e),this._adjustInstDate(e),e.input&&e.input.val(a?"":this._formatDate(e))},_getDate:function(e){var t=!e.currentYear||e.input&&""===e.input.val()?null:this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return t},_attachHandlers:function(t){var i=this._get(t,"stepMonths"),a="#"+t.id.replace(/\\\\/g,"\\");t.dpDiv.find("[data-handler]").map(function(){var t={prev:function(){e.datepicker._adjustDate(a,-i,"M")},next:function(){e.datepicker._adjustDate(a,+i,"M")},hide:function(){e.datepicker._hideDatepicker()},today:function(){e.datepicker._gotoToday(a)},selectDay:function(){return e.datepicker._selectDay(a,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return e.datepicker._selectMonthYear(a,this,"M"),!1},selectYear:function(){return e.datepicker._selectMonthYear(a,this,"Y"),!1}};e(this).bind(this.getAttribute("data-event"),t[this.getAttribute("data-handler")])})},_generateHTML:function(e){var t,i,a,s,n,r,o,u,c,h,l,d,p,g,m,f,_,v,k,y,b,D,w,M,C,x,I,N,T,A,E,S,Y,F,P,O,j,K,R,H=new Date,W=this._daylightSavingAdjust(new Date(H.getFullYear(),H.getMonth(),H.getDate())),L=this._get(e,"isRTL"),U=this._get(e,"showButtonPanel"),B=this._get(e,"hideIfNoPrevNext"),z=this._get(e,"navigationAsDateFormat"),q=this._getNumberOfMonths(e),G=this._get(e,"showCurrentAtPos"),J=this._get(e,"stepMonths"),Q=1!==q[0]||1!==q[1],V=this._daylightSavingAdjust(e.currentDay?new Date(e.currentYear,e.currentMonth,e.currentDay):new Date(9999,9,9)),$=this._getMinMaxDate(e,"min"),X=this._getMinMaxDate(e,"max"),Z=e.drawMonth-G,et=e.drawYear;if(0>Z&&(Z+=12,et--),X)for(t=this._daylightSavingAdjust(new Date(X.getFullYear(),X.getMonth()-q[0]*q[1]+1,X.getDate())),t=$&&$>t?$:t;this._daylightSavingAdjust(new Date(et,Z,1))>t;)Z--,0>Z&&(Z=11,et--);for(e.drawMonth=Z,e.drawYear=et,i=this._get(e,"prevText"),i=z?this.formatDate(i,this._daylightSavingAdjust(new Date(et,Z-J,1)),this._getFormatConfig(e)):i,a=this._canAdjustMonth(e,-1,et,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>":B?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"e":"w")+"'>"+i+"</span></a>",s=this._get(e,"nextText"),s=z?this.formatDate(s,this._daylightSavingAdjust(new Date(et,Z+J,1)),this._getFormatConfig(e)):s,n=this._canAdjustMonth(e,1,et,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>":B?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+s+"'><span class='ui-icon ui-icon-circle-triangle-"+(L?"w":"e")+"'>"+s+"</span></a>",r=this._get(e,"currentText"),o=this._get(e,"gotoCurrent")&&e.currentDay?V:W,r=z?this.formatDate(r,o,this._getFormatConfig(e)):r,u=e.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(e,"closeText")+"</button>",c=U?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(L?u:"")+(this._isInRange(e,o)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+r+"</button>":"")+(L?"":u)+"</div>":"",h=parseInt(this._get(e,"firstDay"),10),h=isNaN(h)?0:h,l=this._get(e,"showWeek"),d=this._get(e,"dayNames"),p=this._get(e,"dayNamesMin"),g=this._get(e,"monthNames"),m=this._get(e,"monthNamesShort"),f=this._get(e,"beforeShowDay"),_=this._get(e,"showOtherMonths"),v=this._get(e,"selectOtherMonths"),k=this._getDefaultDate(e),y="",D=0;q[0]>D;D++){for(w="",this.maxRows=4,M=0;q[1]>M;M++){if(C=this._daylightSavingAdjust(new Date(et,Z,e.selectedDay)),x=" ui-corner-all",I="",Q){if(I+="<div class='ui-datepicker-group",q[1]>1)switch(M){case 0:I+=" ui-datepicker-group-first",x=" ui-corner-"+(L?"right":"left");break;case q[1]-1:I+=" ui-datepicker-group-last",x=" ui-corner-"+(L?"left":"right");break;default:I+=" ui-datepicker-group-middle",x=""}I+="'>"}for(I+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+x+"'>"+(/all|left/.test(x)&&0===D?L?n:a:"")+(/all|right/.test(x)&&0===D?L?a:n:"")+this._generateMonthYearHeader(e,Z,et,$,X,D>0||M>0,g,m)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",N=l?"<th class='ui-datepicker-week-col'>"+this._get(e,"weekHeader")+"</th>":"",b=0;7>b;b++)T=(b+h)%7,N+="<th"+((b+h+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[T]+"'>"+p[T]+"</span></th>";for(I+=N+"</tr></thead><tbody>",A=this._getDaysInMonth(et,Z),et===e.selectedYear&&Z===e.selectedMonth&&(e.selectedDay=Math.min(e.selectedDay,A)),E=(this._getFirstDayOfMonth(et,Z)-h+7)%7,S=Math.ceil((E+A)/7),Y=Q?this.maxRows>S?this.maxRows:S:S,this.maxRows=Y,F=this._daylightSavingAdjust(new Date(et,Z,1-E)),P=0;Y>P;P++){for(I+="<tr>",O=l?"<td class='ui-datepicker-week-col'>"+this._get(e,"calculateWeek")(F)+"</td>":"",b=0;7>b;b++)j=f?f.apply(e.input?e.input[0]:null,[F]):[!0,""],K=F.getMonth()!==Z,R=K&&!v||!j[0]||$&&$>F||X&&F>X,O+="<td class='"+((b+h+6)%7>=5?" ui-datepicker-week-end":"")+(K?" ui-datepicker-other-month":"")+(F.getTime()===C.getTime()&&Z===e.selectedMonth&&e._keyEvent||k.getTime()===F.getTime()&&k.getTime()===C.getTime()?" "+this._dayOverClass:"")+(R?" "+this._unselectableClass+" ui-state-disabled":"")+(K&&!_?"":" "+j[1]+(F.getTime()===V.getTime()?" "+this._currentClass:"")+(F.getTime()===W.getTime()?" ui-datepicker-today":""))+"'"+(K&&!_||!j[2]?"":" title='"+j[2].replace(/'/g,"'")+"'")+(R?"":" data-handler='selectDay' data-event='click' data-month='"+F.getMonth()+"' data-year='"+F.getFullYear()+"'")+">"+(K&&!_?" ":R?"<span class='ui-state-default'>"+F.getDate()+"</span>":"<a class='ui-state-default"+(F.getTime()===W.getTime()?" ui-state-highlight":"")+(F.getTime()===V.getTime()?" ui-state-active":"")+(K?" ui-priority-secondary":"")+"' href='#'>"+F.getDate()+"</a>")+"</td>",F.setDate(F.getDate()+1),F=this._daylightSavingAdjust(F);I+=O+"</tr>"}Z++,Z>11&&(Z=0,et++),I+="</tbody></table>"+(Q?"</div>"+(q[0]>0&&M===q[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),w+=I}y+=w}return y+=c,e._keyEvent=!1,y},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var u,c,h,l,d,p,g,m,f=this._get(e,"changeMonth"),_=this._get(e,"changeYear"),v=this._get(e,"showMonthAfterYear"),k="<div class='ui-datepicker-title'>",y="";if(n||!f)y+="<span class='ui-datepicker-month'>"+r[t]+"</span>";else{for(u=a&&a.getFullYear()===i,c=s&&s.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",h=0;12>h;h++)(!u||h>=a.getMonth())&&(!c||s.getMonth()>=h)&&(y+="<option value='"+h+"'"+(h===t?" selected='selected'":"")+">"+o[h]+"</option>");y+="</select>"}if(v||(k+=y+(!n&&f&&_?"":" ")),!e.yearshtml)if(e.yearshtml="",n||!_)k+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(l=this._get(e,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(e){var t=e.match(/c[+\-].*/)?i+parseInt(e.substring(1),10):e.match(/[+\-].*/)?d+parseInt(e,10):parseInt(e,10); -return isNaN(t)?d:t},g=p(l[0]),m=Math.max(g,p(l[1]||"")),g=a?Math.max(g,a.getFullYear()):g,m=s?Math.min(m,s.getFullYear()):m,e.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";m>=g;g++)e.yearshtml+="<option value='"+g+"'"+(g===i?" selected='selected'":"")+">"+g+"</option>";e.yearshtml+="</select>",k+=e.yearshtml,e.yearshtml=null}return k+=this._get(e,"yearSuffix"),v&&(k+=(!n&&f&&_?"":" ")+y),k+="</div>"},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"===i?t:0),s=e.drawMonth+("M"===i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"===i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i,a,s=this._getMinMaxDate(e,"min"),n=this._getMinMaxDate(e,"max"),r=null,o=null,u=this._get(e,"yearRange");return u&&(i=u.split(":"),a=(new Date).getFullYear(),r=parseInt(i[0],10),o=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(r+=a),i[1].match(/[+\-].*/)&&(o+=a)),(!s||t.getTime()>=s.getTime())&&(!n||t.getTime()<=n.getTime())&&(!r||t.getFullYear()>=r)&&(!o||o>=t.getFullYear())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),e.fn.datepicker=function(t){if(!this.length)return this;e.datepicker.initialized||(e(document).mousedown(e.datepicker._checkExternalClick),e.datepicker.initialized=!0),0===e("#"+e.datepicker._mainDivId).length&&e("body").append(e.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof t||"isDisabled"!==t&&"getDate"!==t&&"widget"!==t?"option"===t&&2===arguments.length&&"string"==typeof arguments[1]?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof t?e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this].concat(i)):e.datepicker._attachDatepicker(this,t)}):e.datepicker["_"+t+"Datepicker"].apply(e.datepicker,[this[0]].concat(i))},e.datepicker=new i,e.datepicker.initialized=!1,e.datepicker.uuid=(new Date).getTime(),e.datepicker.version="1.10.4"})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("<span>").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)}})})(jQuery); \ No newline at end of file diff --git a/public/vendor/jquery/js/jquery-ui.js b/public/vendor/jquery/js/jquery-ui.js new file mode 100644 index 0000000000..5d02c332f0 --- /dev/null +++ b/public/vendor/jquery/js/jquery-ui.js @@ -0,0 +1,10 @@ +/*! jQuery UI - v1.12.0 - 2016-08-26 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, keycode.js, scroll-parent.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/autocomplete.js, widgets/datepicker.js, widgets/menu.js, widgets/mouse.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function(t){t(jQuery)})(function(t){function e(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function i(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=s(t("<div id='"+this._mainDivId+"' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"))}function s(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,n)}function n(){t.datepicker._isDisabledDatepicker(c.inline?c.dpDiv.parent()[0]:c.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function o(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}t.ui=t.ui||{},t.ui.version="1.12.0";var a=0,r=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},l=e.split(".")[0];e=e.split(".")[1];var h=l+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][h.toLowerCase()]=function(e){return!!t.data(e,h)},t[l]=t[l]||{},n=t[l][e],o=t[l][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:l,widgetName:e,widgetFullName:h}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=r.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=r.call(arguments,1),l=this;return o?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(l=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(l=i&&i.jquery?l.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=a++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var l=s.match(/^([\w:-]*)\s*(.*)$/),h=l[1]+o.eventNamespace,c=l[2];c?n.on(h,c,r):i.on(h,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o,a=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;o=function(){var e=t("<div>").css("position","absolute").appendTo("body").offset({top:1.5,left:1.5}),i=1.5===e.offset().top;return e.remove(),o=function(){return i},i},t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,o="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:o?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType,o=!s&&!n;return{element:i,isWindow:s,isDocument:n,offset:o?t(e).offset():{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:i.outerWidth(),height:i.outerHeight()}}},t.fn.position=function(n){if(!n||!n.of)return f.apply(this,arguments);n=t.extend({},n);var p,g,m,_,v,b,y=t(n.of),w=t.position.getWithinInfo(n.within),k=t.position.getScrollInfo(w),x=(n.collision||"flip").split(" "),C={};return b=s(y),y[0].preventDefault&&(n.at="left top"),g=b.width,m=b.height,_=b.offset,v=t.extend({},_),t.each(["my","at"],function(){var t,e,i=(n[this]||"").split(" ");1===i.length&&(i=h.test(i[0])?i.concat(["center"]):c.test(i[0])?["center"].concat(i):["center","center"]),i[0]=h.test(i[0])?i[0]:"center",i[1]=c.test(i[1])?i[1]:"center",t=u.exec(i[0]),e=u.exec(i[1]),C[this]=[t?t[0]:0,e?e[0]:0],n[this]=[d.exec(i[0])[0],d.exec(i[1])[0]]}),1===x.length&&(x[1]=x[0]),"right"===n.at[0]?v.left+=g:"center"===n.at[0]&&(v.left+=g/2),"bottom"===n.at[1]?v.top+=m:"center"===n.at[1]&&(v.top+=m/2),p=e(C.at,g,m),v.left+=p[0],v.top+=p[1],this.each(function(){var s,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=i(this,"marginLeft"),b=i(this,"marginTop"),D=u+f+i(this,"marginRight")+k.width,T=d+b+i(this,"marginBottom")+k.height,I=t.extend({},v),M=e(C.my,c.outerWidth(),c.outerHeight());"right"===n.my[0]?I.left-=u:"center"===n.my[0]&&(I.left-=u/2),"bottom"===n.my[1]?I.top-=d:"center"===n.my[1]&&(I.top-=d/2),I.left+=M[0],I.top+=M[1],o()||(I.left=l(I.left),I.top=l(I.top)),s={marginLeft:f,marginTop:b},t.each(["left","top"],function(e,i){t.ui.position[x[e]]&&t.ui.position[x[e]][i](I,{targetWidth:g,targetHeight:m,elemWidth:u,elemHeight:d,collisionPosition:s,collisionWidth:D,collisionHeight:T,offset:[p[0]+M[0],p[1]+M[1]],my:n.my,at:n.at,within:w,elem:c})}),n.using&&(h=function(t){var e=_.left-I.left,i=e+g-u,s=_.top-I.top,o=s+m-d,l={target:{element:y,left:_.left,top:_.top,width:g,height:m},element:{element:c,left:I.left,top:I.top,width:u,height:d},horizontal:0>i?"left":e>0?"right":"center",vertical:0>o?"top":s>0?"bottom":"middle"};u>g&&g>r(e+i)&&(l.horizontal="center"),d>m&&m>r(s+o)&&(l.vertical="middle"),l.important=a(r(e),r(i))>a(r(s),r(o))?"horizontal":"vertical",n.using.call(this,t,l)}),c.offset(t.extend(I,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,o=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-o-n;e.collisionWidth>o?l>0&&0>=h?(i=t.left+l+e.collisionWidth-o-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+o-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=a(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,o=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-o-n;e.collisionHeight>o?l>0&&0>=h?(i=t.top+l+e.collisionHeight-o-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+o-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=a(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,a=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-a-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-a-o,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,a=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-a-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-a-o,(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var l=!1;t(document).on("mouseup",function(){l=!1}),t.widget("ui.mouse",{version:"1.12.0",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!l){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,s=1===e.which,n="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return s&&!n&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),l=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,l=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.ui.plugin={add:function(e,i,s){var n,o=t.ui[e].prototype;for(n in s)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([i,s[n]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;o.length>n;n++)t.options[o[n][0]]&&o[n][1].apply(t.element,i)}},t.ui.safeActiveElement=function(t){var e;try{e=t.activeElement}catch(i){e=t.body}return e||(e=t.body),e.nodeName||(e=t.body),e},t.ui.safeBlur=function(e){e&&"body"!==e.nodeName.toLowerCase()&&t(e).trigger("blur")},t.widget("ui.draggable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"===this.options.helper&&this._setPositionRelative(),this.options.addClasses&&this._addClass("ui-draggable"),this._setHandleClassName(),this._mouseInit()},_setOption:function(t,e){this._super(t,e),"handle"===t&&(this._removeHandleClassName(),this._setHandleClassName())},_destroy:function(){return(this.helper||this.element).is(".ui-draggable-dragging")?(this.destroyOnClear=!0,void 0):(this._removeHandleClassName(),this._mouseDestroy(),void 0)},_mouseCapture:function(e){var i=this.options;return this._blurActiveElement(e),this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(this._blockFrames(i.iframeFix===!0?"iframe":i.iframeFix),!0):!1)},_blockFrames:function(e){this.iframeBlocks=this.document.find(e).map(function(){var e=t(this);return t("<div>").css("position","absolute").appendTo(e.parent()).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(e){var i=t.ui.safeActiveElement(this.document[0]),s=t(e.target);this._getHandle(e)&&s.closest(i).length||t.ui.safeBlur(i)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=this.helper.parents().filter(function(){return"fixed"===t(this).css("position")}).length>0,this.positionAbs=this.element.offset(),this._refreshOffsets(e),this.originalPosition=this.position=this._generatePosition(e,!1),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_refreshOffsets:function(t){this.offset={top:this.positionAbs.top-this.margins.top,left:this.positionAbs.left-this.margins.left,scroll:!1,parent:this._getParentOffset(),relative:this._getRelativeOffset()},this.offset.click={left:t.pageX-this.offset.left,top:t.pageY-this.offset.top}},_mouseDrag:function(e,i){if(this.hasFixedAncestor&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e,!0),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp(new t.Event("mouseup",e)),!1;this.position=s.position}return this.helper[0].style.left=this.position.left+"px",this.helper[0].style.top=this.position.top+"px",t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1},_mouseUp:function(e){return this._unblockFrames(),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),this.handleElement.is(e.target)&&this.element.trigger("focus"),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp(new t.Event("mouseup",{target:this.element[0]})):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_setHandleClassName:function(){this.handleElement=this.options.handle?this.element.find(this.options.handle):this.element,this._addClass(this.handleElement,"ui-draggable-handle")},_removeHandleClassName:function(){this._removeClass(this.handleElement,"ui-draggable-handle")},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper),n=s?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return n.parents("body").length||n.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s&&n[0]===this.element[0]&&this._setPositionRelative(),n[0]===this.element[0]||/(fixed|absolute)/.test(n.css("position"))||n.css("position","absolute"),n},_setPositionRelative:function(){/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative")},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_isRootNode:function(t){return/(html|body)/i.test(t.tagName)||t===this.document[0]},_getParentOffset:function(){var e=this.offsetParent.offset(),i=this.document[0];return"absolute"===this.cssPosition&&this.scrollParent[0]!==i&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),this._isRootNode(this.offsetParent[0])&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"!==this.cssPosition)return{top:0,left:0};var t=this.element.position(),e=this._isRootNode(this.scrollParent[0]);return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+(e?0:this.scrollParent.scrollTop()),left:t.left-(parseInt(this.helper.css("left"),10)||0)+(e?0:this.scrollParent.scrollLeft())}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options,o=this.document[0];return this.relativeContainer=null,n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):"document"===n.containment?(this.containment=[0,0,t(o).width()-this.helperProportions.width-this.margins.left,(t(o).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):n.containment.constructor===Array?(this.containment=n.containment,void 0):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e=/(scroll|auto)/.test(i.css("overflow")),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relativeContainer=i),void 0):(this.containment=null,void 0)},_convertPositionTo:function(t,e){e||(e=this.position);var i="absolute"===t?1:-1,s=this._isRootNode(this.scrollParent[0]);return{top:e.top+this.offset.relative.top*i+this.offset.parent.top*i-("fixed"===this.cssPosition?-this.offset.scroll.top:s?0:this.offset.scroll.top)*i,left:e.left+this.offset.relative.left*i+this.offset.parent.left*i-("fixed"===this.cssPosition?-this.offset.scroll.left:s?0:this.offset.scroll.left)*i}},_generatePosition:function(t,e){var i,s,n,o,a=this.options,r=this._isRootNode(this.scrollParent[0]),l=t.pageX,h=t.pageY;return r&&this.offset.scroll||(this.offset.scroll={top:this.scrollParent.scrollTop(),left:this.scrollParent.scrollLeft()}),e&&(this.containment&&(this.relativeContainer?(s=this.relativeContainer.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.left<i[0]&&(l=i[0]+this.offset.click.left),t.pageY-this.offset.click.top<i[1]&&(h=i[1]+this.offset.click.top),t.pageX-this.offset.click.left>i[2]&&(l=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),a.grid&&(n=a.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/a.grid[1])*a.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-a.grid[1]:n+a.grid[1]:n,o=a.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/a.grid[0])*a.grid[0]:this.originalPageX,l=i?o-this.offset.click.left>=i[0]||o-this.offset.click.left>i[2]?o:o-this.offset.click.left>=i[0]?o-a.grid[0]:o+a.grid[0]:o),"y"===a.axis&&(l=this.originalPageX),"x"===a.axis&&(h=this.originalPageY)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:r?0:this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:r?0:this.offset.scroll.left)} +},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s,this],!0),/^(drag|start|stop)/.test(e)&&(this.positionAbs=this._convertPositionTo("absolute"),s.offset=this.positionAbs),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i,s){var n=t.extend({},i,{item:s.element});s.sortables=[],t(s.options.connectToSortable).each(function(){var i=t(this).sortable("instance");i&&!i.options.disabled&&(s.sortables.push(i),i.refreshPositions(),i._trigger("activate",e,n))})},stop:function(e,i,s){var n=t.extend({},i,{item:s.element});s.cancelHelperRemoval=!1,t.each(s.sortables,function(){var t=this;t.isOver?(t.isOver=0,s.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,n))})},drag:function(e,i,s){t.each(s.sortables,function(){var n=!1,o=this;o.positionAbs=s.positionAbs,o.helperProportions=s.helperProportions,o.offset.click=s.offset.click,o._intersectsWith(o.containerCache)&&(n=!0,t.each(s.sortables,function(){return this.positionAbs=s.positionAbs,this.helperProportions=s.helperProportions,this.offset.click=s.offset.click,this!==o&&this._intersectsWith(this.containerCache)&&t.contains(o.element[0],this.element[0])&&(n=!1),n})),n?(o.isOver||(o.isOver=1,s._parent=i.helper.parent(),o.currentItem=i.helper.appendTo(o.element).data("ui-sortable-item",!0),o.options._helper=o.options.helper,o.options.helper=function(){return i.helper[0]},e.target=o.currentItem[0],o._mouseCapture(e,!0),o._mouseStart(e,!0,!0),o.offset.click.top=s.offset.click.top,o.offset.click.left=s.offset.click.left,o.offset.parent.left-=s.offset.parent.left-o.offset.parent.left,o.offset.parent.top-=s.offset.parent.top-o.offset.parent.top,s._trigger("toSortable",e),s.dropped=o.element,t.each(s.sortables,function(){this.refreshPositions()}),s.currentItem=s.element,o.fromOutside=s),o.currentItem&&(o._mouseDrag(e),i.position=o.position)):o.isOver&&(o.isOver=0,o.cancelHelperRemoval=!0,o.options._revert=o.options.revert,o.options.revert=!1,o._trigger("out",e,o._uiHash(o)),o._mouseStop(e,!0),o.options.revert=o.options._revert,o.options.helper=o.options._helper,o.placeholder&&o.placeholder.remove(),i.helper.appendTo(s._parent),s._refreshOffsets(e),i.position=s._generatePosition(e,!0),s._trigger("fromSortable",e),s.dropped=!1,t.each(s.sortables,function(){this.refreshPositions()}))})}}),t.ui.plugin.add("draggable","cursor",{start:function(e,i,s){var n=t("body"),o=s.options;n.css("cursor")&&(o._cursor=n.css("cursor")),n.css("cursor",o.cursor)},stop:function(e,i,s){var n=s.options;n._cursor&&t("body").css("cursor",n._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("opacity")&&(o._opacity=n.css("opacity")),n.css("opacity",o.opacity)},stop:function(e,i,s){var n=s.options;n._opacity&&t(i.helper).css("opacity",n._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(e,i,s){var n=s.options,o=!1,a=s.scrollParentNotHidden[0],r=s.document[0];a!==r&&"HTML"!==a.tagName?(n.axis&&"x"===n.axis||(s.overflowOffset.top+a.offsetHeight-e.pageY<n.scrollSensitivity?a.scrollTop=o=a.scrollTop+n.scrollSpeed:e.pageY-s.overflowOffset.top<n.scrollSensitivity&&(a.scrollTop=o=a.scrollTop-n.scrollSpeed)),n.axis&&"y"===n.axis||(s.overflowOffset.left+a.offsetWidth-e.pageX<n.scrollSensitivity?a.scrollLeft=o=a.scrollLeft+n.scrollSpeed:e.pageX-s.overflowOffset.left<n.scrollSensitivity&&(a.scrollLeft=o=a.scrollLeft-n.scrollSpeed))):(n.axis&&"x"===n.axis||(e.pageY-t(r).scrollTop()<n.scrollSensitivity?o=t(r).scrollTop(t(r).scrollTop()-n.scrollSpeed):t(window).height()-(e.pageY-t(r).scrollTop())<n.scrollSensitivity&&(o=t(r).scrollTop(t(r).scrollTop()+n.scrollSpeed))),n.axis&&"y"===n.axis||(e.pageX-t(r).scrollLeft()<n.scrollSensitivity?o=t(r).scrollLeft(t(r).scrollLeft()-n.scrollSpeed):t(window).width()-(e.pageX-t(r).scrollLeft())<n.scrollSensitivity&&(o=t(r).scrollLeft(t(r).scrollLeft()+n.scrollSpeed)))),o!==!1&&t.ui.ddmanager&&!n.dropBehaviour&&t.ui.ddmanager.prepareOffsets(s,e)}}),t.ui.plugin.add("draggable","snap",{start:function(e,i,s){var n=s.options;s.snapElements=[],t(n.snap.constructor!==String?n.snap.items||":data(ui-draggable)":n.snap).each(function(){var e=t(this),i=e.offset();this!==s.element[0]&&s.snapElements.push({item:this,width:e.outerWidth(),height:e.outerHeight(),top:i.top,left:i.left})})},drag:function(e,i,s){var n,o,a,r,l,h,c,u,d,p,f=s.options,g=f.snapTolerance,m=i.offset.left,_=m+s.helperProportions.width,v=i.offset.top,b=v+s.helperProportions.height;for(d=s.snapElements.length-1;d>=0;d--)l=s.snapElements[d].left-s.margins.left,h=l+s.snapElements[d].width,c=s.snapElements[d].top-s.margins.top,u=c+s.snapElements[d].height,l-g>_||m>h+g||c-g>b||v>u+g||!t.contains(s.snapElements[d].item.ownerDocument,s.snapElements[d].item)?(s.snapElements[d].snapping&&s.options.snap.release&&s.options.snap.release.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=!1):("inner"!==f.snapMode&&(n=g>=Math.abs(c-b),o=g>=Math.abs(u-v),a=g>=Math.abs(l-_),r=g>=Math.abs(h-m),n&&(i.position.top=s._convertPositionTo("relative",{top:c-s.helperProportions.height,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l-s.helperProportions.width}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h}).left)),p=n||o||a||r,"outer"!==f.snapMode&&(n=g>=Math.abs(c-v),o=g>=Math.abs(u-b),a=g>=Math.abs(l-m),r=g>=Math.abs(h-_),n&&(i.position.top=s._convertPositionTo("relative",{top:c,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u-s.helperProportions.height,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h-s.helperProportions.width}).left)),!s.snapElements[d].snapping&&(n||o||a||r||p)&&s.options.snap.snap&&s.options.snap.snap.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=n||o||a||r||p)}}),t.ui.plugin.add("draggable","stack",{start:function(e,i,s){var n,o=s.options,a=t.makeArray(t(o.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});a.length&&(n=parseInt(t(a[0]).css("zIndex"),10)||0,t(a).each(function(e){t(this).css("zIndex",n+e)}),this.css("zIndex",n+a.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("zIndex")&&(o._zIndex=n.css("zIndex")),n.css("zIndex",o.zIndex)},stop:function(e,i,s){var n=s.options;n._zIndex&&t(i.helper).css("zIndex",n._zIndex)}}),t.ui.draggable,t.widget("ui.droppable",{version:"1.12.0",widgetEventPrefix:"drop",options:{accept:"*",addClasses:!0,greedy:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],void 0):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},this._addToManager(i.scope),i.addClasses&&this._addClass("ui-droppable")},_addToManager:function(e){t.ui.ddmanager.droppables[e]=t.ui.ddmanager.droppables[e]||[],t.ui.ddmanager.droppables[e].push(this)},_splice:function(t){for(var e=0;t.length>e;e++)t[e]===this&&t.splice(e,1)},_destroy:function(){var e=t.ui.ddmanager.droppables[this.options.scope];this._splice(e)},_setOption:function(e,i){if("accept"===e)this.accept=t.isFunction(i)?i:function(t){return t.is(i)};else if("scope"===e){var s=t.ui.ddmanager.droppables[this.options.scope];this._splice(s),this._addToManager(i)}this._super(e,i)},_activate:function(e){var i=t.ui.ddmanager.current;this._addActiveClass(),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this._removeActiveClass(),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._addHoverClass(),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._removeHoverClass(),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var i=t(this).droppable("instance");return i.options.greedy&&!i.options.disabled&&i.options.scope===s.options.scope&&i.accept.call(i.element[0],s.currentItem||s.element)&&h(s,t.extend(i,{offset:i.element.offset()}),i.options.tolerance,e)?(n=!0,!1):void 0}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this._removeActiveClass(),this._removeHoverClass(),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}},_addHoverClass:function(){this._addClass("ui-droppable-hover")},_removeHoverClass:function(){this._removeClass("ui-droppable-hover")},_addActiveClass:function(){this._addClass("ui-droppable-active")},_removeActiveClass:function(){this._removeClass("ui-droppable-active")}});var h=t.ui.intersect=function(){function t(t,e,i){return t>=e&&e+i>t}return function(e,i,s,n){if(!i.offset)return!1;var o=(e.positionAbs||e.position.absolute).left+e.margins.left,a=(e.positionAbs||e.position.absolute).top+e.margins.top,r=o+e.helperProportions.width,l=a+e.helperProportions.height,h=i.offset.left,c=i.offset.top,u=h+i.proportions().width,d=c+i.proportions().height;switch(s){case"fit":return o>=h&&u>=r&&a>=c&&d>=l;case"intersect":return o+e.helperProportions.width/2>h&&u>r-e.helperProportions.width/2&&a+e.helperProportions.height/2>c&&d>l-e.helperProportions.height/2;case"pointer":return t(n.pageY,c,i.proportions().height)&&t(n.pageX,h,i.proportions().width);case"touch":return(a>=c&&d>=a||l>=c&&d>=l||c>a&&l>d)&&(o>=h&&u>=o||r>=h&&u>=r||h>o&&r>u);default:return!1}}}();t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,o=t.ui.ddmanager.droppables[e.options.scope]||[],a=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;o.length>s;s++)if(!(o[s].options.disabled||e&&!o[s].accept.call(o[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===o[s].element[0]){o[s].proportions().height=0;continue t}o[s].visible="none"!==o[s].element.css("display"),o[s].visible&&("mousedown"===a&&o[s]._activate.call(o[s],i),o[s].offset=o[s].element.offset(),o[s].proportions({width:o[s].element[0].offsetWidth,height:o[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&h(e,this,this.options.tolerance,i)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").on("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,o,a=h(e,this,this.options.tolerance,i),r=!a&&this.isover?"isout":a&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,o=this.element.parents(":data(ui-droppable)").filter(function(){return t(this).droppable("instance").options.scope===n}),o.length&&(s=t(o[0]).droppable("instance"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").off("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}},t.uiBackCompat!==!1&&t.widget("ui.droppable",t.ui.droppable,{options:{hoverClass:!1,activeClass:!1},_addActiveClass:function(){this._super(),this.options.activeClass&&this.element.addClass(this.options.activeClass)},_removeActiveClass:function(){this._super(),this.options.activeClass&&this.element.removeClass(this.options.activeClass)},_addHoverClass:function(){this._super(),this.options.hoverClass&&this.element.addClass(this.options.hoverClass)},_removeHoverClass:function(){this._super(),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass)}}),t.ui.droppable,t.widget("ui.resizable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,classes:{"ui-resizable-se":"ui-icon ui-icon-gripsmall-diagonal-se"},containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(t){return parseFloat(t)||0},_isNumber:function(t){return!isNaN(parseFloat(t))},_hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return e[s]>0?!0:(e[s]=1,n=e[s]>0,e[s]=0,n)},_create:function(){var e,i=this.options,s=this;this._addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!i.aspectRatio,aspectRatio:i.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:i.helper||i.ghost||i.animate?i.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i)&&(this.element.wrap(t("<div class='ui-wrapper' style='overflow: hidden;'></div>").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,e={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(e),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(e),this._proportionallyResize()),this._setupHandles(),i.autoHide&&t(this.element).on("mouseenter",function(){i.disabled||(s._removeClass("ui-resizable-autohide"),s._handles.show())}).on("mouseleave",function(){i.disabled||s.resizing||(s._addClass("ui-resizable-autohide"),s._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeData("resizable").removeData("ui-resizable").off(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;default:}},_setupHandles:function(){var e,i,s,n,o,a=this.options,r=this;if(this.handles=a.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=t(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),s=this.handles.split(","),this.handles={},i=0;s.length>i;i++)e=t.trim(s[i]),n="ui-resizable-"+e,o=t("<div>"),this._addClass(o,"ui-resizable-handle "+n),o.css({zIndex:a.zIndex}),this.handles[e]=".ui-resizable-"+e,this.element.append(o);this._renderAxis=function(e){var i,s,n,o;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String?this.handles[i]=this.element.children(this.handles[i]).first().show():(this.handles[i].jquery||this.handles[i].nodeType)&&(this.handles[i]=t(this.handles[i]),this._on(this.handles[i],{mousedown:r._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=t(this.handles[i],this.element),o=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,o),this._proportionallyResize()),this._handles=this._handles.add(this.handles[i])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){r.resizing||(this.className&&(o=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),r.axis=o&&o[1]?o[1]:"se")}),a.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._handles.remove()},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(e){var i,s,n,o=this.options,a=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),o.containment&&(i+=t(o.containment).scrollLeft()||0,s+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:a.width(),height:a.height()},this.originalSize=this._helper?{width:a.outerWidth(),height:a.outerHeight()}:{width:a.width(),height:a.height()},this.sizeDiff={width:a.outerWidth()-a.width(),height:a.outerHeight()-a.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:e.pageX,top:e.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===n?this.axis+"-resize":n),this._addClass("ui-resizable-resizing"),this._propagate("start",e),!0},_mouseDrag:function(e){var i,s,n=this.originalMousePosition,o=this.axis,a=e.pageX-n.left||0,r=e.pageY-n.top||0,l=this._change[o];return this._updatePrevProperties(),l?(i=l.apply(this,[e,a,r]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",e,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,o,a,r,l,h=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:c.sizeDiff.height,o=s?0:c.sizeDiff.width,a={width:c.helper.width()-o,height:c.helper.height()-n},r=parseFloat(c.element.css("left"))+(c.position.left-c.originalPosition.left)||null,l=parseFloat(c.element.css("top"))+(c.position.top-c.originalPosition.top)||null,h.animate||this.element.css(t.extend(a,{top:l,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!h.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s,n,o,a=this.options;o={minWidth:this._isNumber(a.minWidth)?a.minWidth:0,maxWidth:this._isNumber(a.maxWidth)?a.maxWidth:1/0,minHeight:this._isNumber(a.minHeight)?a.minHeight:0,maxHeight:this._isNumber(a.maxHeight)?a.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,s=o.minWidth/this.aspectRatio,i=o.maxHeight*this.aspectRatio,n=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),s>o.minHeight&&(o.minHeight=s),o.maxWidth>i&&(o.maxWidth=i),o.maxHeight>n&&(o.maxHeight=n)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),this._isNumber(t.left)&&(this.position.left=t.left),this._isNumber(t.top)&&(this.position.top=t.top),this._isNumber(t.height)&&(this.size.height=t.height),this._isNumber(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,i=this.size,s=this.axis;return this._isNumber(t.height)?t.width=t.height*this.aspectRatio:this._isNumber(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===s&&(t.left=e.left+(i.width-t.width),t.top=null),"nw"===s&&(t.top=e.top+(i.height-t.height),t.left=e.left+(i.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,i=this.axis,s=this._isNumber(t.width)&&e.maxWidth&&e.maxWidth<t.width,n=this._isNumber(t.height)&&e.maxHeight&&e.maxHeight<t.height,o=this._isNumber(t.width)&&e.minWidth&&e.minWidth>t.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,l=this.originalPosition.top+this.originalSize.height,h=/sw|nw|w/.test(i),c=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&h&&(t.left=r-e.minWidth),s&&h&&(t.left=r-e.maxWidth),a&&c&&(t.top=l-e.minHeight),n&&c&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];4>e;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;this._proportionallyResizeElements.length>e;e++)t=this._proportionallyResizeElements[e],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(t)),t.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("<div style='overflow:hidden;'></div>"),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,o=n.length&&/textarea/i.test(n[0].nodeName),a=o&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=o?0:i.sizeDiff.width,l={width:i.size.width-r,height:i.size.height-a},h=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,c=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(l,c&&h?{top:c,left:h}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var e,i,s,n,o,a,r,l=t(this).resizable("instance"),h=l.options,c=l.element,u=h.containment,d=u instanceof t?u.get(0):/parent/.test(u)?c.parent().get(0):u;d&&(l.containerElement=t(d),/document/.test(u)||u===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(e=t(d),i=[],t(["Top","Right","Left","Bottom"]).each(function(t,s){i[t]=l._num(e.css("padding"+s))}),l.containerOffset=e.offset(),l.containerPosition=e.position(),l.containerSize={height:e.innerHeight()-i[3],width:e.innerWidth()-i[1]},s=l.containerOffset,n=l.containerSize.height,o=l.containerSize.width,a=l._hasScroll(d,"left")?d.scrollWidth:o,r=l._hasScroll(d)?d.scrollHeight:n,l.parentData={element:d,left:s.left,top:s.top,width:a,height:r}))},resize:function(e){var i,s,n,o,a=t(this).resizable("instance"),r=a.options,l=a.containerOffset,h=a.position,c=a._aspectRatio||e.shiftKey,u={top:0,left:0},d=a.containerElement,p=!0;d[0]!==document&&/static/.test(d.css("position"))&&(u=l),h.left<(a._helper?l.left:0)&&(a.size.width=a.size.width+(a._helper?a.position.left-l.left:a.position.left-u.left),c&&(a.size.height=a.size.width/a.aspectRatio,p=!1),a.position.left=r.helper?l.left:0),h.top<(a._helper?l.top:0)&&(a.size.height=a.size.height+(a._helper?a.position.top-l.top:a.position.top),c&&(a.size.width=a.size.height*a.aspectRatio,p=!1),a.position.top=a._helper?l.top:0),n=a.containerElement.get(0)===a.element.parent().get(0),o=/relative|absolute/.test(a.containerElement.css("position")),n&&o?(a.offset.left=a.parentData.left+a.position.left,a.offset.top=a.parentData.top+a.position.top):(a.offset.left=a.element.offset().left,a.offset.top=a.element.offset().top),i=Math.abs(a.sizeDiff.width+(a._helper?a.offset.left-u.left:a.offset.left-l.left)),s=Math.abs(a.sizeDiff.height+(a._helper?a.offset.top-u.top:a.offset.top-l.top)),i+a.size.width>=a.parentData.width&&(a.size.width=a.parentData.width-i,c&&(a.size.height=a.size.width/a.aspectRatio,p=!1)),s+a.size.height>=a.parentData.height&&(a.size.height=a.parentData.height-s,c&&(a.size.width=a.size.height*a.aspectRatio,p=!1)),p||(a.position.left=a.prevPosition.left,a.position.top=a.prevPosition.top,a.size.width=a.prevSize.width,a.size.height=a.prevSize.height)},stop:function(){var e=t(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.containerPosition,o=e.containerElement,a=t(e.helper),r=a.offset(),l=a.outerWidth()-e.sizeDiff.width,h=a.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h}),e._helper&&!i.animate&&/static/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).resizable("instance"),i=e.options;t(i.alsoResize).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseFloat(e.width()),height:parseFloat(e.height()),left:parseFloat(e.css("left")),top:parseFloat(e.css("top"))})})},resize:function(e,i){var s=t(this).resizable("instance"),n=s.options,o=s.originalSize,a=s.originalPosition,r={height:s.size.height-o.height||0,width:s.size.width-o.width||0,top:s.position.top-a.top||0,left:s.position.left-a.left||0};t(n.alsoResize).each(function(){var e=t(this),s=t(this).data("ui-resizable-alsoresize"),n={},o=e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(s[e]||0)+(r[e]||0);i&&i>=0&&(n[e]=i||null)}),e.css(n)})},stop:function(){t(this).removeData("ui-resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).resizable("instance"),i=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:i.height,width:i.width,margin:0,left:0,top:0}),e._addClass(e.ghost,"ui-resizable-ghost"),t.uiBackCompat!==!1&&"string"==typeof e.options.ghost&&e.ghost.addClass(this.options.ghost),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).resizable("instance");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).resizable("instance");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e,i=t(this).resizable("instance"),s=i.options,n=i.size,o=i.originalSize,a=i.originalPosition,r=i.axis,l="number"==typeof s.grid?[s.grid,s.grid]:s.grid,h=l[0]||1,c=l[1]||1,u=Math.round((n.width-o.width)/h)*h,d=Math.round((n.height-o.height)/c)*c,p=o.width+u,f=o.height+d,g=s.maxWidth&&p>s.maxWidth,m=s.maxHeight&&f>s.maxHeight,_=s.minWidth&&s.minWidth>p,v=s.minHeight&&s.minHeight>f;s.grid=l,_&&(p+=h),v&&(f+=c),g&&(p-=h),m&&(f-=c),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=a.top-d):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=a.left-u):((0>=f-c||0>=p-h)&&(e=i._getPaddingPlusBorderDimensions(this)),f-c>0?(i.size.height=f,i.position.top=a.top-d):(f=c-e.height,i.size.height=f,i.position.top=a.top+o.height-f),p-h>0?(i.size.width=p,i.position.left=a.left-u):(p=h-e.width,i.size.width=p,i.position.left=a.left+o.width-p)) +}}),t.ui.resizable,t.widget("ui.selectable",t.ui.mouse,{version:"1.12.0",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e=this;this._addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e.elementPos=t(e.element[0]).offset(),e.selectees=t(e.options.filter,e.element[0]),e._addClass(e.selectees,"ui-selectee"),e.selectees.each(function(){var i=t(this),s=i.offset(),n={left:s.left-e.elementPos.left,top:s.top-e.elementPos.top};t.data(this,"selectable-item",{element:this,$element:i,left:n.left,top:n.top,right:n.left+i.outerWidth(),bottom:n.top+i.outerHeight(),startselected:!1,selected:i.hasClass("ui-selected"),selecting:i.hasClass("ui-selecting"),unselecting:i.hasClass("ui-unselecting")})})},this.refresh(),this._mouseInit(),this.helper=t("<div>"),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.elementPos=t(this.element[0]).offset(),this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(i._removeClass(s.$element,"ui-selected"),s.selected=!1,i._addClass(s.$element,"ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),i._removeClass(n.$element,s?"ui-unselecting":"ui-selected")._addClass(n.$element,s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):void 0}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,o=this.opos[0],a=this.opos[1],r=e.pageX,l=e.pageY;return o>r&&(i=r,r=o,o=i),a>l&&(i=l,l=a,a=i),this.helper.css({left:o,top:a,width:r-o,height:l-a}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1,c={};i&&i.element!==s.element[0]&&(c.left=i.left+s.elementPos.left,c.right=i.right+s.elementPos.left,c.top=i.top+s.elementPos.top,c.bottom=i.bottom+s.elementPos.top,"touch"===n.tolerance?h=!(c.left>r||o>c.right||c.top>l||a>c.bottom):"fit"===n.tolerance&&(h=c.left>o&&r>c.right&&c.top>a&&l>c.bottom),h?(i.selected&&(s._removeClass(i.$element,"ui-selected"),i.selected=!1),i.unselecting&&(s._removeClass(i.$element,"ui-unselecting"),i.unselecting=!1),i.selecting||(s._addClass(i.$element,"ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,s._addClass(i.$element,"ui-selected"),i.selected=!0):(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,i.startselected&&(s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(s._removeClass(i.$element,"ui-selected"),i.selected=!1,s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-selecting")._addClass(s.$element,"ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}}),t.widget("ui.sortable",t.ui.mouse,{version:"1.12.0",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return t>=e&&e+i>t},_isFloating:function(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))},_create:function(){this.containerCache={},this._addClass("ui-sortable"),this.refresh(),this.offset=this.element.offset(),this._mouseInit(),this._setHandleClassName(),this.ready=!0},_setOption:function(t,e){this._super(t,e),"handle"===t&&this._setHandleClassName()},_setHandleClassName:function(){var e=this;this._removeClass(this.element.find(".ui-sortable-handle"),"ui-sortable-handle"),t.each(this.items,function(){e._addClass(this.instance.options.handle?this.item.find(this.instance.options.handle):this.item,"ui-sortable-handle")})},_destroy:function(){this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):void 0}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("<style>*{ cursor: "+a.cursor+" !important; }</style>").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY<a.scrollSensitivity?this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop+a.scrollSpeed:e.pageY-this.overflowOffset.top<a.scrollSensitivity&&(this.scrollParent[0].scrollTop=r=this.scrollParent[0].scrollTop-a.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-e.pageX<a.scrollSensitivity?this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft+a.scrollSpeed:e.pageX-this.overflowOffset.left<a.scrollSensitivity&&(this.scrollParent[0].scrollLeft=r=this.scrollParent[0].scrollLeft-a.scrollSpeed)):(e.pageY-this.document.scrollTop()<a.scrollSensitivity?r=this.document.scrollTop(this.document.scrollTop()-a.scrollSpeed):this.window.height()-(e.pageY-this.document.scrollTop())<a.scrollSensitivity&&(r=this.document.scrollTop(this.document.scrollTop()+a.scrollSpeed)),e.pageX-this.document.scrollLeft()<a.scrollSensitivity?r=this.document.scrollLeft(this.document.scrollLeft()-a.scrollSpeed):this.window.width()-(e.pageX-this.document.scrollLeft())<a.scrollSensitivity&&(r=this.document.scrollLeft(this.document.scrollLeft()+a.scrollSpeed))),r!==!1&&t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e)),this.positionAbs=this._convertPositionTo("absolute"),this.options.axis&&"y"===this.options.axis||(this.helper[0].style.left=this.position.left+"px"),this.options.axis&&"x"===this.options.axis||(this.helper[0].style.top=this.position.top+"px"),i=this.items.length-1;i>=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?(this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,l=r+t.height,h=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+h>r&&l>s+h,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&l>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var e,i,s="x"===this.options.axis||this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top,t.height),n="y"===this.options.axis||this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left,t.width),o=s&&n;return o?(e=this._getDragVerticalDirection(),i=this._getDragHorizontalDirection(),this.floating?"right"===i||"down"===e?2:1:e&&("down"===e?2:1)):!1},_intersectsWithSides:function(t){var e=this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),i=this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),s=this._getDragVerticalDirection(),n=this._getDragHorizontalDirection();return this.floating&&n?"right"===n&&i||"left"===n&&!i:s&&("down"===s&&e||"up"===s&&!e)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this._setHandleClassName(),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],l=[],h=this._connectWith();if(h&&e)for(s=h.length-1;s>=0;s--)for(o=t(h[s],this.document[0]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&l.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(l.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=l.length-1;s>=0;s--)l[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,l,h,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i],this.document[0]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,h=r.length;h>s;s++)l=t(r[s]),l.data(this.widgetName+"-item",a),c.push({item:l,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.floating=this.items.length?"x"===this.options.axis||this._isFloating(this.items[0].item):!1,this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]);return e._addClass(n,"ui-sortable-placeholder",i||e.currentItem[0].className)._removeClass(n,"ui-sortable-helper"),"tbody"===s?e._createTrPlaceholder(e.currentItem.find("tr").eq(0),t("<tr>",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:function(e,i){var s=this;e.children().each(function(){t("<td> </td>",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,o,a,r,l,h,c,u,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,o=null,c=d.floating||this._isFloating(this.currentItem),a=c?"left":"top",r=c?"width":"height",u=c?"pageX":"pageY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],this.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(l=this.items[s].item.offset()[a],h=!1,e[u]-l>this.items[s][r]/2&&(h=!0),n>Math.abs(e[u]-l)&&(n=Math.abs(e[u]-l),o=this.items[s],this.direction=h?"up":"down"));if(!o&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;o?this._rearrange(e,o,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.height()||document.body.parentNode.scrollHeight:this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.left<this.containment[0]&&(o=this.containment[0]+this.offset.click.left),e.pageY-this.offset.click.top<this.containment[1]&&(a=this.containment[1]+this.offset.click.top),e.pageX-this.offset.click.left>this.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():l?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():l?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}}),t.widget("ui.menu",{version:"1.12.0",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",n=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t)) +},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,l=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=l.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("<span>").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=l.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)},_filterMenuItems:function(e){var i=e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&"),s=RegExp("^"+i,"i");return this.activeMenu.find(this.options.items).filter(".ui-menu-item").filter(function(){return s.test(t.trim(t(this).children(".ui-menu-item-wrapper").text()))})}}),t.widget("ui.autocomplete",{version:"1.12.0",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n;this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("<ul>").appendTo(this._appendTo()).menu({role:null}).hide().menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(e){e.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,this.element[0]!==t.ui.safeActiveElement(this.document[0])&&this.element.trigger("focus")})},menufocus:function(e,i){var s,n;return this.isNewMenu&&(this.isNewMenu=!1,e.originalEvent&&/^mouse/.test(e.originalEvent.type))?(this.menu.blur(),this.document.one("mousemove",function(){t(e.target).trigger(e.originalEvent)}),void 0):(n=i.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",e,{item:n})&&e.originalEvent&&/^key/.test(e.originalEvent.type)&&this._value(n.value),s=i.item.attr("aria-label")||n.value,s&&t.trim(s).length&&(this.liveRegion.children().hide(),t("<div>").text(s).appendTo(this.liveRegion)),void 0)},menuselect:function(e,i){var s=i.item.data("ui-autocomplete-item"),n=this.previous;this.element[0]!==t.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=n,this._delay(function(){this.previous=n,this.selectedItem=s})),!1!==this._trigger("select",e,{item:s})&&this._value(s.value),this.term=this._value(),this.close(e),this.selectedItem=s}}),this.liveRegion=t("<div>",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(e){var i=this.menu.element[0];return e.target===this.element[0]||e.target===i||t.contains(i,e.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var e=this.options.appendTo;return e&&(e=e.jquery||e.nodeType?t(e):this.document.find(e).eq(0)),e&&e[0]||(e=this.element.closest(".ui-front, dialog")),e.length||(e=this.document[0].body),e},_initSource:function(){var e,i,s=this;t.isArray(this.options.source)?(e=this.options.source,this.source=function(i,s){s(t.ui.autocomplete.filter(e,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(e,n){s.xhr&&s.xhr.abort(),s.xhr=t.ajax({url:i,data:e,dataType:"json",success:function(t){n(t)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(t){clearTimeout(this.searching),this.searching=this._delay(function(){var e=this.term===this._value(),i=this.menu.element.is(":visible"),s=t.altKey||t.ctrlKey||t.metaKey||t.shiftKey;(!e||e&&!i&&!s)&&(this.selectedItem=null,this.search(null,t))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length<this.options.minLength?this.close(e):this._trigger("search",e)!==!1?this._search(t):void 0},_search:function(t){this.pending++,this._addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:t},this._response())},_response:function(){var e=++this.requestIndex;return t.proxy(function(t){e===this.requestIndex&&this.__response(t),this.pending--,this.pending||this._removeClass("ui-autocomplete-loading")},this)},__response:function(t){t&&(t=this._normalize(t)),this._trigger("response",null,{content:t}),!this.options.disabled&&t&&t.length&&!this.cancelSearch?(this._suggest(t),this._trigger("open")):this._close()},close:function(t){this.cancelSearch=!0,this._close(t)},_close:function(t){this._off(this.document,"mousedown"),this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",t))},_change:function(t){this.previous!==this._value()&&this._trigger("change",t,{item:this.selectedItem})},_normalize:function(e){return e.length&&e[0].label&&e[0].value?e:t.map(e,function(e){return"string"==typeof e?{label:e,value:e}:t.extend({},e,{label:e.label||e.value,value:e.value||e.label})})},_suggest:function(e){var i=this.menu.element.empty();this._renderMenu(i,e),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(t.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next(),this._on(this.document,{mousedown:"_closeOnClickOutside"})},_resizeMenu:function(){var t=this.menu.element;t.outerWidth(Math.max(t.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(e,i){var s=this;t.each(i,function(t,i){s._renderItemData(e,i)})},_renderItemData:function(t,e){return this._renderItem(t,e).data("ui-autocomplete-item",e)},_renderItem:function(e,i){return t("<li>").append(t("<div>").text(i.label)).appendTo(e)},_move:function(t,e){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur(),void 0):(this.menu[t](e),void 0):(this.search(null,e),void 0)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),t.extend(t.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(e,i){var s=RegExp(t.ui.autocomplete.escapeRegex(i),"i");return t.grep(e,function(t){return s.test(t.label||t.value||t)})}}),t.widget("ui.autocomplete",t.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(t>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var i;this._superApply(arguments),this.options.disabled||this.cancelSearch||(i=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.children().hide(),t("<div>").text(i).appendTo(this.liveRegion))}}),t.ui.autocomplete,t.extend(t.ui,{datepicker:{version:"1.12.0"}});var c;t.extend(i.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(t){return o(this._defaults,t||{}),this},_attachDatepicker:function(e,i){var s,n,o;s=e.nodeName.toLowerCase(),n="div"===s||"span"===s,e.id||(this.uuid+=1,e.id="dp"+this.uuid),o=this._newInst(t(e),n),o.settings=t.extend({},i||{}),"input"===s?this._connectDatepicker(e,o):n&&this._inlineDatepicker(e,o)},_newInst:function(e,i){var n=e[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1");return{id:n,input:e,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:i,dpDiv:i?s(t("<div class='"+this._inlineClass+" ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")):this.dpDiv}},_connectDatepicker:function(e,i){var s=t(e);i.append=t([]),i.trigger=t([]),s.hasClass(this.markerClassName)||(this._attachments(s,i),s.addClass(this.markerClassName).on("keydown",this._doKeyDown).on("keypress",this._doKeyPress).on("keyup",this._doKeyUp),this._autoSize(i),t.data(e,"datepicker",i),i.settings.disabled&&this._disableDatepicker(e))},_attachments:function(e,i){var s,n,o,a=this._get(i,"appendText"),r=this._get(i,"isRTL");i.append&&i.append.remove(),a&&(i.append=t("<span class='"+this._appendClass+"'>"+a+"</span>"),e[r?"before":"after"](i.append)),e.off("focus",this._showDatepicker),i.trigger&&i.trigger.remove(),s=this._get(i,"showOn"),("focus"===s||"both"===s)&&e.on("focus",this._showDatepicker),("button"===s||"both"===s)&&(n=this._get(i,"buttonText"),o=this._get(i,"buttonImage"),i.trigger=t(this._get(i,"buttonImageOnly")?t("<img/>").addClass(this._triggerClass).attr({src:o,alt:n,title:n}):t("<button type='button'></button>").addClass(this._triggerClass).html(o?t("<img/>").attr({src:o,alt:n,title:n}):n)),e[r?"before":"after"](i.trigger),i.trigger.on("click",function(){return t.datepicker._datepickerShowing&&t.datepicker._lastInput===e[0]?t.datepicker._hideDatepicker():t.datepicker._datepickerShowing&&t.datepicker._lastInput!==e[0]?(t.datepicker._hideDatepicker(),t.datepicker._showDatepicker(e[0])):t.datepicker._showDatepicker(e[0]),!1}))},_autoSize:function(t){if(this._get(t,"autoSize")&&!t.inline){var e,i,s,n,o=new Date(2009,11,20),a=this._get(t,"dateFormat");a.match(/[DM]/)&&(e=function(t){for(i=0,s=0,n=0;t.length>n;n++)t[n].length>i&&(i=t[n].length,s=n);return s},o.setMonth(e(this._get(t,a.match(/MM/)?"monthNames":"monthNamesShort"))),o.setDate(e(this._get(t,a.match(/DD/)?"dayNames":"dayNamesShort"))+20-o.getDay())),t.input.attr("size",this._formatDate(t,o).length)}},_inlineDatepicker:function(e,i){var s=t(e);s.hasClass(this.markerClassName)||(s.addClass(this.markerClassName).append(i.dpDiv),t.data(e,"datepicker",i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(e),i.dpDiv.css("display","block"))},_dialogDatepicker:function(e,i,s,n,a){var r,l,h,c,u,d=this._dialogInst;return d||(this.uuid+=1,r="dp"+this.uuid,this._dialogInput=t("<input type='text' id='"+r+"' style='position: absolute; top: -100px; width: 0px;'/>"),this._dialogInput.on("keydown",this._doKeyDown),t("body").append(this._dialogInput),d=this._dialogInst=this._newInst(this._dialogInput,!1),d.settings={},t.data(this._dialogInput[0],"datepicker",d)),o(d.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(d,i):i,this._dialogInput.val(i),this._pos=a?a.length?a:[a.pageX,a.pageY]:null,this._pos||(l=document.documentElement.clientWidth,h=document.documentElement.clientHeight,c=document.documentElement.scrollLeft||document.body.scrollLeft,u=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[l/2-100+c,h/2-150+u]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),d.settings.onSelect=s,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),t.blockUI&&t.blockUI(this.dpDiv),t.data(this._dialogInput[0],"datepicker",d),this},_destroyDatepicker:function(e){var i,s=t(e),n=t.data(e,"datepicker");s.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),t.removeData(e,"datepicker"),"input"===i?(n.append.remove(),n.trigger.remove(),s.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):("div"===i||"span"===i)&&s.removeClass(this.markerClassName).empty(),c===n&&(c=null))},_enableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!1,o.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!0,o.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(!t)return!1;for(var e=0;this._disabledInputs.length>e;e++)if(this._disabledInputs[e]===t)return!0;return!1},_getInst:function(e){try{return t.data(e,"datepicker")}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,i,s){var n,a,r,l,h=this._getInst(e);return 2===arguments.length&&"string"==typeof i?"defaults"===i?t.extend({},t.datepicker._defaults):h?"all"===i?t.extend({},h.settings):this._get(h,i):null:(n=i||{},"string"==typeof i&&(n={},n[i]=s),h&&(this._curInst===h&&this._hideDatepicker(),a=this._getDateDatepicker(e,!0),r=this._getMinMaxDate(h,"min"),l=this._getMinMaxDate(h,"max"),o(h.settings,n),null!==r&&void 0!==n.dateFormat&&void 0===n.minDate&&(h.settings.minDate=this._formatDate(h,r)),null!==l&&void 0!==n.dateFormat&&void 0===n.maxDate&&(h.settings.maxDate=this._formatDate(h,l)),"disabled"in n&&(n.disabled?this._disableDatepicker(e):this._enableDatepicker(e)),this._attachments(t(e),h),this._autoSize(h),this._setDate(h,a),this._updateAlternate(h),this._updateDatepicker(h)),void 0)},_changeDatepicker:function(t,e,i){this._optionDatepicker(t,e,i)},_refreshDatepicker:function(t){var e=this._getInst(t);e&&this._updateDatepicker(e)},_setDateDatepicker:function(t,e){var i=this._getInst(t);i&&(this._setDate(i,e),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(t,e){var i=this._getInst(t);return i&&!i.inline&&this._setDateFromField(i,e),i?this._getDate(i):null},_doKeyDown:function(e){var i,s,n,o=t.datepicker._getInst(e.target),a=!0,r=o.dpDiv.is(".ui-datepicker-rtl");if(o._keyEvent=!0,t.datepicker._datepickerShowing)switch(e.keyCode){case 9:t.datepicker._hideDatepicker(),a=!1;break;case 13:return n=t("td."+t.datepicker._dayOverClass+":not(."+t.datepicker._currentClass+")",o.dpDiv),n[0]&&t.datepicker._selectDay(e.target,o.selectedMonth,o.selectedYear,n[0]),i=t.datepicker._get(o,"onSelect"),i?(s=t.datepicker._formatDate(o),i.apply(o.input?o.input[0]:null,[s,o])):t.datepicker._hideDatepicker(),!1;case 27:t.datepicker._hideDatepicker();break;case 33:t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 34:t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&t.datepicker._clearDate(e.target),a=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&t.datepicker._gotoToday(e.target),a=e.ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?1:-1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,-7,"D"),a=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?-1:1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,7,"D"),a=e.ctrlKey||e.metaKey;break;default:a=!1}else 36===e.keyCode&&e.ctrlKey?t.datepicker._showDatepicker(this):a=!1;a&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var i,s,n=t.datepicker._getInst(e.target);return t.datepicker._get(n,"constrainInput")?(i=t.datepicker._possibleChars(t.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),e.ctrlKey||e.metaKey||" ">s||!i||i.indexOf(s)>-1):void 0},_doKeyUp:function(e){var i,s=t.datepicker._getInst(e.target);if(s.input.val()!==s.lastVal)try{i=t.datepicker.parseDate(t.datepicker._get(s,"dateFormat"),s.input?s.input.val():null,t.datepicker._getFormatConfig(s)),i&&(t.datepicker._setDateFromField(s),t.datepicker._updateAlternate(s),t.datepicker._updateDatepicker(s))}catch(n){}return!0},_showDatepicker:function(i){if(i=i.target||i,"input"!==i.nodeName.toLowerCase()&&(i=t("input",i.parentNode)[0]),!t.datepicker._isDisabledDatepicker(i)&&t.datepicker._lastInput!==i){var s,n,a,r,l,h,c;s=t.datepicker._getInst(i),t.datepicker._curInst&&t.datepicker._curInst!==s&&(t.datepicker._curInst.dpDiv.stop(!0,!0),s&&t.datepicker._datepickerShowing&&t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])),n=t.datepicker._get(s,"beforeShow"),a=n?n.apply(i,[i,s]):{},a!==!1&&(o(s.settings,a),s.lastVal=null,t.datepicker._lastInput=i,t.datepicker._setDateFromField(s),t.datepicker._inDialog&&(i.value=""),t.datepicker._pos||(t.datepicker._pos=t.datepicker._findPos(i),t.datepicker._pos[1]+=i.offsetHeight),r=!1,t(i).parents().each(function(){return r|="fixed"===t(this).css("position"),!r}),l={left:t.datepicker._pos[0],top:t.datepicker._pos[1]},t.datepicker._pos=null,s.dpDiv.empty(),s.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),t.datepicker._updateDatepicker(s),l=t.datepicker._checkOffset(s,l,r),s.dpDiv.css({position:t.datepicker._inDialog&&t.blockUI?"static":r?"fixed":"absolute",display:"none",left:l.left+"px",top:l.top+"px"}),s.inline||(h=t.datepicker._get(s,"showAnim"),c=t.datepicker._get(s,"duration"),s.dpDiv.css("z-index",e(t(i))+1),t.datepicker._datepickerShowing=!0,t.effects&&t.effects.effect[h]?s.dpDiv.show(h,t.datepicker._get(s,"showOptions"),c):s.dpDiv[h||"show"](h?c:null),t.datepicker._shouldFocusInput(s)&&s.input.trigger("focus"),t.datepicker._curInst=s))}},_updateDatepicker:function(e){this.maxRows=4,c=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var i,s=this._getNumberOfMonths(e),o=s[1],a=17,r=e.dpDiv.find("."+this._dayOverClass+" a");r.length>0&&n.apply(r.get(0)),e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),o>1&&e.dpDiv.addClass("ui-datepicker-multi-"+o).css("width",a*o+"em"),e.dpDiv[(1!==s[0]||1!==s[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),e.dpDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e===t.datepicker._curInst&&t.datepicker._datepickerShowing&&t.datepicker._shouldFocusInput(e)&&e.input.trigger("focus"),e.yearshtml&&(i=e.yearshtml,setTimeout(function(){i===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),i=e.yearshtml=null},0))},_shouldFocusInput:function(t){return t.input&&t.input.is(":visible")&&!t.input.is(":disabled")&&!t.input.is(":focus")},_checkOffset:function(e,i,s){var n=e.dpDiv.outerWidth(),o=e.dpDiv.outerHeight(),a=e.input?e.input.outerWidth():0,r=e.input?e.input.outerHeight():0,l=document.documentElement.clientWidth+(s?0:t(document).scrollLeft()),h=document.documentElement.clientHeight+(s?0:t(document).scrollTop());return i.left-=this._get(e,"isRTL")?n-a:0,i.left-=s&&i.left===e.input.offset().left?t(document).scrollLeft():0,i.top-=s&&i.top===e.input.offset().top+r?t(document).scrollTop():0,i.left-=Math.min(i.left,i.left+n>l&&l>n?Math.abs(i.left+n-l):0),i.top-=Math.min(i.top,i.top+o>h&&h>o?Math.abs(o+r):0),i},_findPos:function(e){for(var i,s=this._getInst(e),n=this._get(s,"isRTL");e&&("hidden"===e.type||1!==e.nodeType||t.expr.filters.hidden(e));)e=e[n?"previousSibling":"nextSibling"];return i=t(e).offset(),[i.left,i.top]},_hideDatepicker:function(e){var i,s,n,o,a=this._curInst;!a||e&&a!==t.data(e,"datepicker")||this._datepickerShowing&&(i=this._get(a,"showAnim"),s=this._get(a,"duration"),n=function(){t.datepicker._tidyDialog(a)},t.effects&&(t.effects.effect[i]||t.effects[i])?a.dpDiv.hide(i,t.datepicker._get(a,"showOptions"),s,n):a.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?s:null,n),i||n(),this._datepickerShowing=!1,o=this._get(a,"onClose"),o&&o.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),t.blockUI&&(t.unblockUI(),t("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(t){t.dpDiv.removeClass(this._dialogClass).off(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(t.datepicker._curInst){var i=t(e.target),s=t.datepicker._getInst(i[0]);(i[0].id!==t.datepicker._mainDivId&&0===i.parents("#"+t.datepicker._mainDivId).length&&!i.hasClass(t.datepicker.markerClassName)&&!i.closest("."+t.datepicker._triggerClass).length&&t.datepicker._datepickerShowing&&(!t.datepicker._inDialog||!t.blockUI)||i.hasClass(t.datepicker.markerClassName)&&t.datepicker._curInst!==s)&&t.datepicker._hideDatepicker()}},_adjustDate:function(e,i,s){var n=t(e),o=this._getInst(n[0]);this._isDisabledDatepicker(n[0])||(this._adjustInstDate(o,i+("M"===s?this._get(o,"showCurrentAtPos"):0),s),this._updateDatepicker(o))},_gotoToday:function(e){var i,s=t(e),n=this._getInst(s[0]);this._get(n,"gotoCurrent")&&n.currentDay?(n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear):(i=new Date,n.selectedDay=i.getDate(),n.drawMonth=n.selectedMonth=i.getMonth(),n.drawYear=n.selectedYear=i.getFullYear()),this._notifyChange(n),this._adjustDate(s)},_selectMonthYear:function(e,i,s){var n=t(e),o=this._getInst(n[0]);o["selected"+("M"===s?"Month":"Year")]=o["draw"+("M"===s?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(o),this._adjustDate(n)},_selectDay:function(e,i,s,n){var o,a=t(e);t(n).hasClass(this._unselectableClass)||this._isDisabledDatepicker(a[0])||(o=this._getInst(a[0]),o.selectedDay=o.currentDay=t("a",n).html(),o.selectedMonth=o.currentMonth=i,o.selectedYear=o.currentYear=s,this._selectDate(e,this._formatDate(o,o.currentDay,o.currentMonth,o.currentYear)))},_clearDate:function(e){var i=t(e);this._selectDate(i,"")},_selectDate:function(e,i){var s,n=t(e),o=this._getInst(n[0]);i=null!=i?i:this._formatDate(o),o.input&&o.input.val(i),this._updateAlternate(o),s=this._get(o,"onSelect"),s?s.apply(o.input?o.input[0]:null,[i,o]):o.input&&o.input.trigger("change"),o.inline?this._updateDatepicker(o):(this._hideDatepicker(),this._lastInput=o.input[0],"object"!=typeof o.input[0]&&o.input.trigger("focus"),this._lastInput=null)},_updateAlternate:function(e){var i,s,n,o=this._get(e,"altField");o&&(i=this._get(e,"altFormat")||this._get(e,"dateFormat"),s=this._getDate(e),n=this.formatDate(i,s,this._getFormatConfig(e)),t(o).val(n))},noWeekends:function(t){var e=t.getDay();return[e>0&&6>e,""]},iso8601Week:function(t){var e,i=new Date(t.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),e=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((e-i)/864e5)/7)+1},parseDate:function(e,i,s){if(null==e||null==i)throw"Invalid arguments";if(i="object"==typeof i?""+i:i+"",""===i)return null;var n,o,a,r,l=0,h=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,c="string"!=typeof h?h:(new Date).getFullYear()%100+parseInt(h,10),u=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,d=(s?s.dayNames:null)||this._defaults.dayNames,p=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,f=(s?s.monthNames:null)||this._defaults.monthNames,g=-1,m=-1,_=-1,v=-1,b=!1,y=function(t){var i=e.length>n+1&&e.charAt(n+1)===t;return i&&n++,i},w=function(t){var e=y(t),s="@"===t?14:"!"===t?20:"y"===t&&e?4:"o"===t?3:2,n="y"===t?s:1,o=RegExp("^\\d{"+n+","+s+"}"),a=i.substring(l).match(o);if(!a)throw"Missing number at position "+l;return l+=a[0].length,parseInt(a[0],10)},k=function(e,s,n){var o=-1,a=t.map(y(e)?n:s,function(t,e){return[[e,t]]}).sort(function(t,e){return-(t[1].length-e[1].length)});if(t.each(a,function(t,e){var s=e[1];return i.substr(l,s.length).toLowerCase()===s.toLowerCase()?(o=e[0],l+=s.length,!1):void 0}),-1!==o)return o+1;throw"Unknown name at position "+l},x=function(){if(i.charAt(l)!==e.charAt(n))throw"Unexpected literal at position "+l;l++};for(n=0;e.length>n;n++)if(b)"'"!==e.charAt(n)||y("'")?x():b=!1;else switch(e.charAt(n)){case"d":_=w("d");break;case"D":k("D",u,d);break;case"o":v=w("o");break;case"m":m=w("m");break;case"M":m=k("M",p,f);break;case"y":g=w("y");break;case"@":r=new Date(w("@")),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate(); +break;case"!":r=new Date((w("!")-this._ticksTo1970)/1e4),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();break;case"'":y("'")?x():b=!0;break;default:x()}if(i.length>l&&(a=i.substr(l),!/^\s+/.test(a)))throw"Extra/unparsed characters found in date: "+a;if(-1===g?g=(new Date).getFullYear():100>g&&(g+=(new Date).getFullYear()-(new Date).getFullYear()%100+(c>=g?0:-100)),v>-1)for(m=1,_=v;;){if(o=this._getDaysInMonth(g,m-1),o>=_)break;m++,_-=o}if(r=this._daylightSavingAdjust(new Date(g,m-1,_)),r.getFullYear()!==g||r.getMonth()+1!==m||r.getDate()!==_)throw"Invalid date";return r},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(t,e,i){if(!e)return"";var s,n=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,o=(i?i.dayNames:null)||this._defaults.dayNames,a=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,r=(i?i.monthNames:null)||this._defaults.monthNames,l=function(e){var i=t.length>s+1&&t.charAt(s+1)===e;return i&&s++,i},h=function(t,e,i){var s=""+e;if(l(t))for(;i>s.length;)s="0"+s;return s},c=function(t,e,i,s){return l(t)?s[e]:i[e]},u="",d=!1;if(e)for(s=0;t.length>s;s++)if(d)"'"!==t.charAt(s)||l("'")?u+=t.charAt(s):d=!1;else switch(t.charAt(s)){case"d":u+=h("d",e.getDate(),2);break;case"D":u+=c("D",e.getDay(),n,o);break;case"o":u+=h("o",Math.round((new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()-new Date(e.getFullYear(),0,0).getTime())/864e5),3);break;case"m":u+=h("m",e.getMonth()+1,2);break;case"M":u+=c("M",e.getMonth(),a,r);break;case"y":u+=l("y")?e.getFullYear():(10>e.getFullYear()%100?"0":"")+e.getFullYear()%100;break;case"@":u+=e.getTime();break;case"!":u+=1e4*e.getTime()+this._ticksTo1970;break;case"'":l("'")?u+="'":d=!0;break;default:u+=t.charAt(s)}return u},_possibleChars:function(t){var e,i="",s=!1,n=function(i){var s=t.length>e+1&&t.charAt(e+1)===i;return s&&e++,s};for(e=0;t.length>e;e++)if(s)"'"!==t.charAt(e)||n("'")?i+=t.charAt(e):s=!1;else switch(t.charAt(e)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":n("'")?i+="'":s=!0;break;default:i+=t.charAt(e)}return i},_get:function(t,e){return void 0!==t.settings[e]?t.settings[e]:this._defaults[e]},_setDateFromField:function(t,e){if(t.input.val()!==t.lastVal){var i=this._get(t,"dateFormat"),s=t.lastVal=t.input?t.input.val():null,n=this._getDefaultDate(t),o=n,a=this._getFormatConfig(t);try{o=this.parseDate(i,s,a)||n}catch(r){s=e?"":s}t.selectedDay=o.getDate(),t.drawMonth=t.selectedMonth=o.getMonth(),t.drawYear=t.selectedYear=o.getFullYear(),t.currentDay=s?o.getDate():0,t.currentMonth=s?o.getMonth():0,t.currentYear=s?o.getFullYear():0,this._adjustInstDate(t)}},_getDefaultDate:function(t){return this._restrictMinMax(t,this._determineDate(t,this._get(t,"defaultDate"),new Date))},_determineDate:function(e,i,s){var n=function(t){var e=new Date;return e.setDate(e.getDate()+t),e},o=function(i){try{return t.datepicker.parseDate(t.datepicker._get(e,"dateFormat"),i,t.datepicker._getFormatConfig(e))}catch(s){}for(var n=(i.toLowerCase().match(/^c/)?t.datepicker._getDate(e):null)||new Date,o=n.getFullYear(),a=n.getMonth(),r=n.getDate(),l=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,h=l.exec(i);h;){switch(h[2]||"d"){case"d":case"D":r+=parseInt(h[1],10);break;case"w":case"W":r+=7*parseInt(h[1],10);break;case"m":case"M":a+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a));break;case"y":case"Y":o+=parseInt(h[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a))}h=l.exec(i)}return new Date(o,a,r)},a=null==i||""===i?s:"string"==typeof i?o(i):"number"==typeof i?isNaN(i)?s:n(i):new Date(i.getTime());return a=a&&"Invalid Date"==""+a?s:a,a&&(a.setHours(0),a.setMinutes(0),a.setSeconds(0),a.setMilliseconds(0)),this._daylightSavingAdjust(a)},_daylightSavingAdjust:function(t){return t?(t.setHours(t.getHours()>12?t.getHours()+2:0),t):null},_setDate:function(t,e,i){var s=!e,n=t.selectedMonth,o=t.selectedYear,a=this._restrictMinMax(t,this._determineDate(t,e,new Date));t.selectedDay=t.currentDay=a.getDate(),t.drawMonth=t.selectedMonth=t.currentMonth=a.getMonth(),t.drawYear=t.selectedYear=t.currentYear=a.getFullYear(),n===t.selectedMonth&&o===t.selectedYear||i||this._notifyChange(t),this._adjustInstDate(t),t.input&&t.input.val(s?"":this._formatDate(t))},_getDate:function(t){var e=!t.currentYear||t.input&&""===t.input.val()?null:this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return e},_attachHandlers:function(e){var i=this._get(e,"stepMonths"),s="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){t.datepicker._adjustDate(s,-i,"M")},next:function(){t.datepicker._adjustDate(s,+i,"M")},hide:function(){t.datepicker._hideDatepicker()},today:function(){t.datepicker._gotoToday(s)},selectDay:function(){return t.datepicker._selectDay(s,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return t.datepicker._selectMonthYear(s,this,"M"),!1},selectYear:function(){return t.datepicker._selectMonthYear(s,this,"Y"),!1}};t(this).on(this.getAttribute("data-event"),e[this.getAttribute("data-handler")])})},_generateHTML:function(t){var e,i,s,n,o,a,r,l,h,c,u,d,p,f,g,m,_,v,b,y,w,k,x,C,D,T,I,M,P,S,N,H,z,A,O,W,E,F,L,R=new Date,Y=this._daylightSavingAdjust(new Date(R.getFullYear(),R.getMonth(),R.getDate())),B=this._get(t,"isRTL"),j=this._get(t,"showButtonPanel"),q=this._get(t,"hideIfNoPrevNext"),K=this._get(t,"navigationAsDateFormat"),U=this._getNumberOfMonths(t),V=this._get(t,"showCurrentAtPos"),X=this._get(t,"stepMonths"),$=1!==U[0]||1!==U[1],G=this._daylightSavingAdjust(t.currentDay?new Date(t.currentYear,t.currentMonth,t.currentDay):new Date(9999,9,9)),J=this._getMinMaxDate(t,"min"),Q=this._getMinMaxDate(t,"max"),Z=t.drawMonth-V,te=t.drawYear;if(0>Z&&(Z+=12,te--),Q)for(e=this._daylightSavingAdjust(new Date(Q.getFullYear(),Q.getMonth()-U[0]*U[1]+1,Q.getDate())),e=J&&J>e?J:e;this._daylightSavingAdjust(new Date(te,Z,1))>e;)Z--,0>Z&&(Z=11,te--);for(t.drawMonth=Z,t.drawYear=te,i=this._get(t,"prevText"),i=K?this.formatDate(i,this._daylightSavingAdjust(new Date(te,Z-X,1)),this._getFormatConfig(t)):i,s=this._canAdjustMonth(t,-1,te,Z)?"<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(B?"e":"w")+"'>"+i+"</span></a>":q?"":"<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+i+"'><span class='ui-icon ui-icon-circle-triangle-"+(B?"e":"w")+"'>"+i+"</span></a>",n=this._get(t,"nextText"),n=K?this.formatDate(n,this._daylightSavingAdjust(new Date(te,Z+X,1)),this._getFormatConfig(t)):n,o=this._canAdjustMonth(t,1,te,Z)?"<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click' title='"+n+"'><span class='ui-icon ui-icon-circle-triangle-"+(B?"w":"e")+"'>"+n+"</span></a>":q?"":"<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+n+"'><span class='ui-icon ui-icon-circle-triangle-"+(B?"w":"e")+"'>"+n+"</span></a>",a=this._get(t,"currentText"),r=this._get(t,"gotoCurrent")&&t.currentDay?G:Y,a=K?this.formatDate(a,r,this._getFormatConfig(t)):a,l=t.inline?"":"<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>"+this._get(t,"closeText")+"</button>",h=j?"<div class='ui-datepicker-buttonpane ui-widget-content'>"+(B?l:"")+(this._isInRange(t,r)?"<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>"+a+"</button>":"")+(B?"":l)+"</div>":"",c=parseInt(this._get(t,"firstDay"),10),c=isNaN(c)?0:c,u=this._get(t,"showWeek"),d=this._get(t,"dayNames"),p=this._get(t,"dayNamesMin"),f=this._get(t,"monthNames"),g=this._get(t,"monthNamesShort"),m=this._get(t,"beforeShowDay"),_=this._get(t,"showOtherMonths"),v=this._get(t,"selectOtherMonths"),b=this._getDefaultDate(t),y="",k=0;U[0]>k;k++){for(x="",this.maxRows=4,C=0;U[1]>C;C++){if(D=this._daylightSavingAdjust(new Date(te,Z,t.selectedDay)),T=" ui-corner-all",I="",$){if(I+="<div class='ui-datepicker-group",U[1]>1)switch(C){case 0:I+=" ui-datepicker-group-first",T=" ui-corner-"+(B?"right":"left");break;case U[1]-1:I+=" ui-datepicker-group-last",T=" ui-corner-"+(B?"left":"right");break;default:I+=" ui-datepicker-group-middle",T=""}I+="'>"}for(I+="<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix"+T+"'>"+(/all|left/.test(T)&&0===k?B?o:s:"")+(/all|right/.test(T)&&0===k?B?s:o:"")+this._generateMonthYearHeader(t,Z,te,J,Q,k>0||C>0,f,g)+"</div><table class='ui-datepicker-calendar'><thead>"+"<tr>",M=u?"<th class='ui-datepicker-week-col'>"+this._get(t,"weekHeader")+"</th>":"",w=0;7>w;w++)P=(w+c)%7,M+="<th scope='col'"+((w+c+6)%7>=5?" class='ui-datepicker-week-end'":"")+">"+"<span title='"+d[P]+"'>"+p[P]+"</span></th>";for(I+=M+"</tr></thead><tbody>",S=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,S)),N=(this._getFirstDayOfMonth(te,Z)-c+7)%7,H=Math.ceil((N+S)/7),z=$?this.maxRows>H?this.maxRows:H:H,this.maxRows=z,A=this._daylightSavingAdjust(new Date(te,Z,1-N)),O=0;z>O;O++){for(I+="<tr>",W=u?"<td class='ui-datepicker-week-col'>"+this._get(t,"calculateWeek")(A)+"</td>":"",w=0;7>w;w++)E=m?m.apply(t.input?t.input[0]:null,[A]):[!0,""],F=A.getMonth()!==Z,L=F&&!v||!E[0]||J&&J>A||Q&&A>Q,W+="<td class='"+((w+c+6)%7>=5?" ui-datepicker-week-end":"")+(F?" ui-datepicker-other-month":"")+(A.getTime()===D.getTime()&&Z===t.selectedMonth&&t._keyEvent||b.getTime()===A.getTime()&&b.getTime()===D.getTime()?" "+this._dayOverClass:"")+(L?" "+this._unselectableClass+" ui-state-disabled":"")+(F&&!_?"":" "+E[1]+(A.getTime()===G.getTime()?" "+this._currentClass:"")+(A.getTime()===Y.getTime()?" ui-datepicker-today":""))+"'"+(F&&!_||!E[2]?"":" title='"+E[2].replace(/'/g,"'")+"'")+(L?"":" data-handler='selectDay' data-event='click' data-month='"+A.getMonth()+"' data-year='"+A.getFullYear()+"'")+">"+(F&&!_?" ":L?"<span class='ui-state-default'>"+A.getDate()+"</span>":"<a class='ui-state-default"+(A.getTime()===Y.getTime()?" ui-state-highlight":"")+(A.getTime()===G.getTime()?" ui-state-active":"")+(F?" ui-priority-secondary":"")+"' href='#'>"+A.getDate()+"</a>")+"</td>",A.setDate(A.getDate()+1),A=this._daylightSavingAdjust(A);I+=W+"</tr>"}Z++,Z>11&&(Z=0,te++),I+="</tbody></table>"+($?"</div>"+(U[0]>0&&C===U[1]-1?"<div class='ui-datepicker-row-break'></div>":""):""),x+=I}y+=x}return y+=h,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,o,a,r){var l,h,c,u,d,p,f,g,m=this._get(t,"changeMonth"),_=this._get(t,"changeYear"),v=this._get(t,"showMonthAfterYear"),b="<div class='ui-datepicker-title'>",y="";if(o||!m)y+="<span class='ui-datepicker-month'>"+a[e]+"</span>";else{for(l=s&&s.getFullYear()===i,h=n&&n.getFullYear()===i,y+="<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>",c=0;12>c;c++)(!l||c>=s.getMonth())&&(!h||n.getMonth()>=c)&&(y+="<option value='"+c+"'"+(c===e?" selected='selected'":"")+">"+r[c]+"</option>");y+="</select>"}if(v||(b+=y+(!o&&m&&_?"":" ")),!t.yearshtml)if(t.yearshtml="",o||!_)b+="<span class='ui-datepicker-year'>"+i+"</span>";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10);return isNaN(e)?d:e},f=p(u[0]),g=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,g=n?Math.min(g,n.getFullYear()):g,t.yearshtml+="<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";g>=f;f++)t.yearshtml+="<option value='"+f+"'"+(f===i?" selected='selected'":"")+">"+f+"</option>";t.yearshtml+="</select>",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),v&&(b+=(!o&&m&&_?"":" ")+y),b+="</div>"},_adjustInstDate:function(t,e,i){var s=t.selectedYear+("Y"===i?e:0),n=t.selectedMonth+("M"===i?e:0),o=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),a=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,o)));t.selectedDay=a.getDate(),t.drawMonth=t.selectedMonth=a.getMonth(),t.drawYear=t.selectedYear=a.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),o=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&o.setDate(this._getDaysInMonth(o.getFullYear(),o.getMonth())),this._isInRange(t,o)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),o=this._getMinMaxDate(t,"max"),a=null,r=null,l=this._get(t,"yearRange");return l&&(i=l.split(":"),s=(new Date).getFullYear(),a=parseInt(i[0],10),r=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(a+=s),i[1].match(/[+\-].*/)&&(r+=s)),(!n||e.getTime()>=n.getTime())&&(!o||e.getTime()<=o.getTime())&&(!a||e.getFullYear()>=a)&&(!r||r>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).on("mousedown",t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new i,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.12.0",t.datepicker}); \ No newline at end of file diff --git a/public/vendor/jquery/textcomplete/jquery.textcomplete.js b/public/vendor/jquery/textcomplete/jquery.textcomplete.js index ad1d508450..b4ccd18cfc 100644 --- a/public/vendor/jquery/textcomplete/jquery.textcomplete.js +++ b/public/vendor/jquery/textcomplete/jquery.textcomplete.js @@ -17,7 +17,7 @@ * Repository: https://github.com/yuku-t/jquery-textcomplete * License: MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE) * Author: Yuku Takahashi - * Version: 1.3.1 + * Version: 1.7.3 */ if (typeof jQuery === 'undefined') { @@ -137,10 +137,6 @@ if (typeof jQuery === 'undefined') { return Object.prototype.toString.call(obj) === '[object String]'; }; - var isFunction = function (obj) { - return Object.prototype.toString.call(obj) === '[object Function]'; - }; - var uniqueId = 0; function Completer(element, option) { @@ -148,33 +144,47 @@ if (typeof jQuery === 'undefined') { this.id = 'textcomplete' + uniqueId++; this.strategies = []; this.views = []; - this.option = $.extend({}, Completer._getDefaults(), option); + this.option = $.extend({}, Completer.defaults, option); if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') { throw new Error('textcomplete must be called on a Textarea or a ContentEditable.'); } - if (element === document.activeElement) { + // use ownerDocument to fix iframe / IE issues + if (element === element.ownerDocument.activeElement) { // element has already been focused. Initialize view objects immediately. this.initialize() } else { // Initialize view objects lazily. var self = this; this.$el.one('focus.' + this.id, function () { self.initialize(); }); - } - } - Completer._getDefaults = function () { - if (!Completer.DEFAULTS) { - Completer.DEFAULTS = { - appendTo: $('body'), - zIndex: '100' - }; + // Special handling for CKEditor: lazy init on instance load + if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) { + CKEDITOR.on("instanceReady", function(event) { + event.editor.once("focus", function(event2) { + // replace the element with the Iframe element and flag it as CKEditor + self.$el = $(event.editor.editable().$); + if (!self.option.adapter) { + self.option.adapter = $.fn.textcomplete['CKEditor']; + self.option.ckeditor_instance = event.editor; + } + self.initialize(); + }); + }); + } } - - return Completer.DEFAULTS; } + Completer.defaults = { + appendTo: 'body', + className: '', // deprecated option + dropdownClassName: 'dropdown-menu textcomplete-dropdown', + maxCount: 10, + zIndex: '100', + rightEdgeOffset: 30 + }; + $.extend(Completer.prototype, { // Public properties // ----------------- @@ -185,12 +195,26 @@ if (typeof jQuery === 'undefined') { adapter: null, dropdown: null, $el: null, + $iframe: null, // Public methods // -------------- initialize: function () { var element = this.$el.get(0); + + // check if we are in an iframe + // we need to alter positioning logic if using an iframe + if (this.$el.prop('ownerDocument') !== document && window.frames.length) { + for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) { + if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) { + this.$iframe = $(window.frames[iframeIndex].frameElement); + break; + } + } + } + + // Initialize view objects. this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option); var Adapter, viewName; @@ -282,7 +306,7 @@ if (typeof jQuery === 'undefined') { var strategy = this.strategies[i]; var context = strategy.context(text); if (context || context === '') { - var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match; + var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match; if (isString(context)) { text = context; } var match = text.match(matchRegexp); if (match) { return [strategy, match[strategy.index], match]; } @@ -400,7 +424,7 @@ if (typeof jQuery === 'undefined') { var $parent = option.appendTo; if (!($parent instanceof $)) { $parent = $($parent); } var $el = $('<ul></ul>') - .addClass('dropdown-menu textcomplete-dropdown') + .addClass(option.dropdownClassName) .attr('id', 'textcomplete-dropdown-' + option._oid) .css({ display: 'none', @@ -423,7 +447,7 @@ if (typeof jQuery === 'undefined') { footer: null, header: null, id: null, - maxCount: 10, + maxCount: null, placement: '', shown: false, data: [], // Shown zipped data. @@ -446,8 +470,8 @@ if (typeof jQuery === 'undefined') { render: function (zippedData) { var contentsHtml = this._buildContents(zippedData); - var unzippedData = $.map(this.data, function (d) { return d.value; }); - if (this.data.length) { + var unzippedData = $.map(zippedData, function (d) { return d.value; }); + if (zippedData.length) { var strategy = zippedData[0].strategy; if (strategy.id) { this.$el.attr('data-strategy', strategy.id); @@ -481,7 +505,7 @@ if (typeof jQuery === 'undefined') { return false; if($(this).css('position') === 'fixed') { pos.top -= $window.scrollTop(); - pos.left -= $window.scrollLeft(); + pos.left -= $window.scrollLeft(); position = 'fixed'; return false; } @@ -786,7 +810,10 @@ if (typeof jQuery === 'undefined') { var windowScrollBottom = $window.scrollTop() + $window.height(); var height = this.$el.height(); if ((this.$el.position().top + height) > windowScrollBottom) { - this.$el.offset({top: windowScrollBottom - height}); + // only do this if we are not in an iframe + if (!this.completer.$iframe) { + this.$el.offset({top: windowScrollBottom - height}); + } } }, @@ -795,9 +822,15 @@ if (typeof jQuery === 'undefined') { // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right // edge, move left. We don't know how far to move left, so just keep nudging a bit. - var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space. - while (this.$el.offset().left + this.$el.width() > $window.width() - tolerance) { - this.$el.offset({left: this.$el.offset().left - tolerance}); + var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space. + var lastOffset = this.$el.offset().left, offset; + var width = this.$el.width(); + var maxLeft = $window.width() - tolerance; + while (lastOffset + width > maxLeft) { + this.$el.offset({left: lastOffset - tolerance}); + offset = this.$el.offset().left; + if (offset >= lastOffset) { break; } + lastOffset = offset; } }, @@ -1002,6 +1035,7 @@ if (typeof jQuery === 'undefined') { case 13: // ENTER case 40: // DOWN case 38: // UP + case 27: // ESC return true; } if (clickEvent.ctrlKey) switch (clickEvent.keyCode) { @@ -1035,12 +1069,14 @@ if (typeof jQuery === 'undefined') { var pre = this.getTextFromHeadToCaret(); var post = this.el.value.substring(this.el.selectionEnd); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr); this.$el.val(pre + post); this.el.selectionStart = this.el.selectionEnd = pre.length; } @@ -1056,9 +1092,29 @@ if (typeof jQuery === 'undefined') { _getCaretRelativePosition: function () { var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart); return { - top: p.top + parseInt(this.$el.css('line-height'), 10) - this.$el.scrollTop(), - left: p.left - this.$el.scrollLeft() + top: p.top + this._calculateLineHeight() - this.$el.scrollTop(), + left: p.left - this.$el.scrollLeft(), + lineHeight: this._calculateLineHeight() }; + }, + + _calculateLineHeight: function () { + var lineHeight = parseInt(this.$el.css('line-height'), 10); + if (isNaN(lineHeight)) { + // http://stackoverflow.com/a/4515470/1297336 + var parentNode = this.el.parentNode; + var temp = document.createElement(this.el.nodeName); + var style = this.el.style; + temp.setAttribute( + 'style', + 'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize + ); + temp.innerHTML = 'test'; + parentNode.appendChild(temp); + lineHeight = temp.clientHeight; + parentNode.removeChild(temp); + } + return lineHeight; } }); @@ -1087,12 +1143,14 @@ if (typeof jQuery === 'undefined') { var pre = this.getTextFromHeadToCaret(); var post = this.el.value.substring(pre.length); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr); this.$el.val(pre + post); this.el.focus(); var range = this.el.createTextRange(); @@ -1138,30 +1196,35 @@ if (typeof jQuery === 'undefined') { // When an dropdown item is selected, it is executed. select: function (value, strategy, e) { var pre = this.getTextFromHeadToCaret(); - var sel = window.getSelection() + // use ownerDocument instead of window to support iframes + var sel = this.el.ownerDocument.getSelection(); + var range = sel.getRangeAt(0); var selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); var content = selection.toString(); var post = content.substring(range.startOffset); var newSubstr = strategy.replace(value, e); + var regExp; if (typeof newSubstr !== 'undefined') { if ($.isArray(newSubstr)) { post = newSubstr[1] + post; newSubstr = newSubstr[0]; } - pre = pre.replace(strategy.match, newSubstr); + regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match; + pre = pre.replace(regExp, newSubstr) + .replace(/ $/, " "); //   necessary at least for CKeditor to not eat spaces range.selectNodeContents(range.startContainer); range.deleteContents(); // create temporary elements - var preWrapper = document.createElement("div"); + var preWrapper = this.el.ownerDocument.createElement("div"); preWrapper.innerHTML = pre; - var postWrapper = document.createElement("div"); + var postWrapper = this.el.ownerDocument.createElement("div"); postWrapper.innerHTML = post; // create the fragment thats inserted - var fragment = document.createDocumentFragment(); + var fragment = this.el.ownerDocument.createDocumentFragment(); var childNode; var lastOfPre; while (childNode = preWrapper.firstChild) { @@ -1194,8 +1257,8 @@ if (typeof jQuery === 'undefined') { // // Dropdown's position will be decided using the result. _getCaretRelativePosition: function () { - var range = window.getSelection().getRangeAt(0).cloneRange(); - var node = document.createElement('span'); + var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange(); + var node = this.el.ownerDocument.createElement('span'); range.insertNode(node); range.selectNodeContents(node); range.deleteContents(); @@ -1204,6 +1267,17 @@ if (typeof jQuery === 'undefined') { position.left -= this.$el.offset().left; position.top += $node.height() - this.$el.offset().top; position.lineHeight = $node.height(); + + // special positioning logic for iframes + // this is typically used for contenteditables such as tinymce or ckeditor + if (this.completer.$iframe) { + var iframePosition = this.completer.$iframe.offset(); + position.top += iframePosition.top; + position.left += iframePosition.left; + //subtract scrollTop from element in iframe + position.top -= this.$el.scrollTop(); + } + $node.remove(); return position; }, @@ -1217,7 +1291,7 @@ if (typeof jQuery === 'undefined') { // this.getTextFromHeadToCaret() // // => ' wor' // not '<b>hello</b> wor' getTextFromHeadToCaret: function () { - var range = window.getSelection().getRangeAt(0); + var range = this.el.ownerDocument.getSelection().getRangeAt(0); var selection = range.cloneRange(); selection.selectNodeContents(range.startContainer); return selection.toString().substring(0, range.startOffset); @@ -1227,6 +1301,39 @@ if (typeof jQuery === 'undefined') { $.fn.textcomplete.ContentEditable = ContentEditable; }(jQuery); +// NOTE: TextComplete plugin has contenteditable support but it does not work +// fine especially on old IEs. +// Any pull requests are REALLY welcome. + ++function ($) { + 'use strict'; + + // CKEditor adapter + // ======================= + // + // Adapter for CKEditor, based on contenteditable elements. + function CKEditor (element, completer, option) { + this.initialize(element, completer, option); + } + + $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, { + _bindEvents: function () { + var $this = this; + this.option.ckeditor_instance.on('key', function(event) { + var domEvent = event.data; + $this._onKeyup(domEvent); + if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) { + return false; + } + }, null, null, 1); // 1 = Priority = Important! + // we actually also need the native event, as the CKEditor one is happening to late + this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this)); + }, +}); + + $.fn.textcomplete.CKEditor = CKEditor; +}(jQuery); + // The MIT License (MIT) // // Copyright (c) 2015 Jonathan Ong me@jongleberry.com @@ -1248,7 +1355,7 @@ if (typeof jQuery === 'undefined') { // // https://github.com/component/textarea-caret-position -(function () { +(function ($) { // The properties that we copy into a mirrored div. // Note that some browsers, such as Firefox, @@ -1369,13 +1476,9 @@ function getCaretCoordinates(element, position, options) { return coordinates; } -if (typeof module != 'undefined' && typeof module.exports != 'undefined') { - module.exports = getCaretCoordinates; -} else if(isBrowser){ - window.$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates; -} +$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates; -}()); +}(jQuery)); return jQuery; -})); \ No newline at end of file +})); diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js new file mode 100644 index 0000000000..a430a45768 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az-short.js @@ -0,0 +1,20 @@ +// Azerbaijani shortened +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "", + suffixFromNow: "", + seconds: '1 dəq', + minute: '1 dəq', + minutes: '%d dəq', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 il', + years: '%d il', + wordSeparator: '', + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.az.js b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js new file mode 100644 index 0000000000..1e04a23a83 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.az.js @@ -0,0 +1,20 @@ +// Azerbaijani +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'əvvəl', + suffixFromNow: 'sonra', + seconds: 'saniyələr', + minute: '1 dəqiqə', + minutes: '%d dəqiqə', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 il', + years: '%d il', + wordSeparator: '', + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js index 10f158de08..09427ec976 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.de-short.js @@ -4,17 +4,17 @@ jQuery.timeago.settings.strings = { prefixFromNow: null, suffixAgo: "", suffixFromNow: "", - seconds: "sec", - minute: "1min", - minutes: "%dmin", + seconds: "s", + minute: "1m", + minutes: "%dm", hour: "1h", hours: "%dh", - day: "1d", - days: "%dd", - month: "1Mon", - months: "%dMon", - year: "1Jhr", - years: "%dJhr", + day: "1T.", + days: "%dT.", + month: "1Mt.", + months: "%dMt.", + year: "1J.", + years: "%dJ.", wordSeparator: " ", numbers: [] }; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js index fd81f275d0..95a7cd2a7a 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.ja.js @@ -11,8 +11,8 @@ jQuery.timeago.settings.strings = { hours: "約 %d 時間", day: "約 1 日", days: "約 %d 日", - month: "約 1 月", - months: "約 %d 月", + month: "約 1 ヶ月", + months: "約 %d ヶ月", year: "約 1 年", years: "約 %d 年", wordSeparator: "" diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js new file mode 100644 index 0000000000..eb02391563 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.lv.js @@ -0,0 +1,20 @@ +//Latvian +jQuery.timeago.settings.strings = { + prefixAgo: "pirms", + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: "no šī brīža", + seconds: "%d sek.", + minute: "min.", + minutes: "%d min.", + hour: "st.", + hours: "%d st.", + day: "1 d.", + days: "%d d.", + month: "mēnesis.", + months: "%d mēnesis.", + year: "gads", + years: "%d gads", + wordSeparator: " ", + numbers: [] +}; diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js index 57d4f6020c..b8ab587d82 100644 --- a/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sl.js @@ -28,7 +28,7 @@ }, month: "en mesec", months: function (value) { - return numpf(value, ["%d mescov", "%d mesec", "%d mesca", "%d mesce"]); + return numpf(value, ["%d mesecev", "%d mesec", "%d meseca", "%d mesece"]); }, year: "eno leto", years: function (value) { diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js new file mode 100644 index 0000000000..c75a972e77 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.sr.js @@ -0,0 +1,49 @@ +// Serbian +(function () { + var numpf; + + numpf = function (n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "пре", + prefixFromNow: "за", + suffixAgo: null, + suffixFromNow: null, + second: "секунд", + seconds: function (value) { + return numpf(value, "%d секунд", "%d секунде", "%d секунди"); + }, + minute: "један минут", + minutes: function (value) { + return numpf(value, "%d минут", "%d минута", "%d минута"); + }, + hour: "један сат", + hours: function (value) { + return numpf(value, "%d сат", "%d сата", "%d сати"); + }, + day: "један дан", + days: function (value) { + return numpf(value, "%d дан", "%d дана", "%d дана"); + }, + month: "месец дана", + months: function (value) { + return numpf(value, "%d месец", "%d месеца", "%d месеци"); + }, + year: "годину дана", + years: function (value) { + return numpf(value, "%d годину", "%d године", "%d година"); + }, + wordSeparator: " " + }; + +}).call(this); diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js new file mode 100644 index 0000000000..ebc2277b49 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.tr-short.js @@ -0,0 +1,20 @@ +// Turkish shortened +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "", + suffixFromNow: "", + seconds: "1sn", + minute: "1d", + minutes: "%dd", + hour: "1s", + hours: "%ds", + day: "1g", + days: "%dg", + month: "1ay", + months: "%day", + year: "1y", + years: "%dy", + wordSeparator: " ", + numbers: [] +}; diff --git a/public/vendor/mousetrap/mousetrap.js b/public/vendor/mousetrap/mousetrap.js deleted file mode 100644 index 01709ffd9a..0000000000 --- a/public/vendor/mousetrap/mousetrap.js +++ /dev/null @@ -1,9 +0,0 @@ -/* mousetrap v1.4.6 craig.is/killing/mice */ -(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].length;++g)if(k= -l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation? -b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}function E(a,b){var d, -c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{type:d.action}, -c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`","!":"1", -"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c],b,d);return this}, -unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0, -b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define('mousetrap', m)})(window,document); \ No newline at end of file diff --git a/public/vendor/nprogress.min.js b/public/vendor/nprogress.min.js new file mode 100644 index 0000000000..09998a166f --- /dev/null +++ b/public/vendor/nprogress.min.js @@ -0,0 +1,3 @@ +/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT */ +!function(n,t){"object"==typeof exports?module.exports=t():n.NProgress=t()}(this,function(){function n(n,t,e){return t>n?t:n>e?e:n}function t(n){return 100*(-1+n)}function e(n,e,r){var i;return i="translate3d"===c.positionUsing?{transform:"translate3d("+t(n)+"%,0,0)"}:"translate"===c.positionUsing?{transform:"translate("+t(n)+"%,0)"}:{"margin-left":t(n)+"%"},i.transition="all "+e+"ms "+r,i}function r(n,t){var e="string"==typeof n?n:o(n);return e.indexOf(" "+t+" ")>=0}function i(n,t){var e=o(n),i=e+t;r(e,t)||(n.className=i.substring(1))}function s(n,t){var e,i=o(n);r(n,t)&&(e=i.replace(" "+t+" "," "),n.className=e.substring(1,e.length-1))}function o(n){return(" "+(n.className||"")+" ").replace(/\s+/gi," ")}function a(n){n&&n.parentNode&&n.parentNode.removeChild(n)}var u={};u.version="0.1.6";var c=u.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'};u.configure=function(n){var t,e;for(t in n)e=n[t],void 0!==e&&n.hasOwnProperty(t)&&(c[t]=e);return this},u.status=null,u.set=function(t){var r=u.isStarted();t=n(t,c.minimum,1),u.status=1===t?null:t;var i=u.render(!r),s=i.querySelector(c.barSelector),o=c.speed,a=c.easing;return i.offsetWidth,l(function(n){""===c.positionUsing&&(c.positionUsing=u.getPositioningCSS()),f(s,e(t,o,a)),1===t?(f(i,{transition:"none",opacity:1}),i.offsetWidth,setTimeout(function(){f(i,{transition:"all "+o+"ms linear",opacity:0}),setTimeout(function(){u.remove(),n()},o)},o)):setTimeout(n,o)}),this},u.isStarted=function(){return"number"==typeof u.status},u.start=function(){u.status||u.set(0);var n=function(){setTimeout(function(){u.status&&(u.trickle(),n())},c.trickleSpeed)};return c.trickle&&n(),this},u.done=function(n){return n||u.status?u.inc(.3+.5*Math.random()).set(1):this},u.inc=function(t){var e=u.status;return e?("number"!=typeof t&&(t=(1-e)*n(Math.random()*e,.1,.95)),e=n(e+t,0,.994),u.set(e)):u.start()},u.trickle=function(){return u.inc(Math.random()*c.trickleRate)},function(){var n=0,t=0;u.promise=function(e){return e&&"resolved"!=e.state()?(0==t&&u.start(),n++,t++,e.always(function(){t--,0==t?(n=0,u.done()):u.set((n-t)/n)}),this):this}}(),u.render=function(n){if(u.isRendered())return document.getElementById("nprogress");i(document.documentElement,"nprogress-busy");var e=document.createElement("div");e.id="nprogress",e.innerHTML=c.template;var r,s=e.querySelector(c.barSelector),o=n?"-100":t(u.status||0),l=document.querySelector(c.parent);return f(s,{transition:"all 0 linear",transform:"translate3d("+o+"%,0,0)"}),c.showSpinner||(r=e.querySelector(c.spinnerSelector),r&&a(r)),l!=document.body&&i(l,"nprogress-custom-parent"),l.appendChild(e),e},u.remove=function(){s(document.documentElement,"nprogress-busy"),s(document.querySelector(c.parent),"nprogress-custom-parent");var n=document.getElementById("nprogress");n&&a(n)},u.isRendered=function(){return!!document.getElementById("nprogress")},u.getPositioningCSS=function(){var n=document.body.style,t="WebkitTransform"in n?"Webkit":"MozTransform"in n?"Moz":"msTransform"in n?"ms":"OTransform"in n?"O":"";return t+"Perspective"in n?"translate3d":t+"Transform"in n?"translate":"margin"};var l=function(){function n(){var e=t.shift();e&&e(n)}var t=[];return function(e){t.push(e),1==t.length&&n()}}(),f=function(){function n(n){return n.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(n,t){return t.toUpperCase()})}function t(n){var t=document.body.style;if(n in t)return n;for(var e,r=i.length,s=n.charAt(0).toUpperCase()+n.slice(1);r--;)if(e=i[r]+s,e in t)return e;return n}function e(e){return e=n(e),s[e]||(s[e]=t(e))}function r(n,t,r){t=e(t),n.style[t]=r}var i=["Webkit","O","Moz","ms"],s={};return function(n,t){var e,i,s=arguments;if(2==s.length)for(e in t)i=t[e],void 0!==i&&t.hasOwnProperty(e)&&r(n,e,i);else r(n,s[1],s[2])}}();return u}); \ No newline at end of file diff --git a/public/vendor/requirejs/require.js b/public/vendor/requirejs/require.js index f04b8c3f7d..857eb5b700 100644 --- a/public/vendor/requirejs/require.js +++ b/public/vendor/requirejs/require.js @@ -1,36 +1,36 @@ /* - RequireJS 2.1.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. - Available via the MIT or new BSD license. - see: http://github.com/jrburke/requirejs for details + RequireJS 2.2.0 Copyright jQuery Foundation and other contributors. + Released under MIT license, http://github.com/requirejs/requirejs/LICENSE */ var requirejs,require,define; -(function(ba){function J(b){return"[object Function]"===N.call(b)}function K(b){return"[object Array]"===N.call(b)}function z(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function O(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function t(b,c){return ha.call(b,c)}function m(b,c){return t(b,c)&&b[c]}function H(b,c){for(var d in b)if(t(b,d)&&c(b[d],d))break}function S(b,c,d,m){c&&H(c,function(c,l){if(d||!t(b,l))m&&"string"!==typeof c?(b[l]||(b[l]={}),S(b[l], -c,d,m)):b[l]=c});return b}function v(b,c){return function(){return c.apply(b,arguments)}}function ca(b){throw b;}function da(b){if(!b)return b;var c=ba;z(b.split("."),function(b){c=c[b]});return c}function B(b,c,d,m){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=m;d&&(c.originalError=d);return c}function ia(b){function c(a,f,C){var e,n,b,c,d,T,k,g=f&&f.split("/");e=g;var l=j.map,h=l&&l["*"];if(a&&"."===a.charAt(0))if(f){e=m(j.pkgs,f)?g=[f]:g.slice(0,g.length- -1);f=a=e.concat(a.split("/"));for(e=0;f[e];e+=1)if(n=f[e],"."===n)f.splice(e,1),e-=1;else if(".."===n)if(1===e&&(".."===f[2]||".."===f[0]))break;else 0<e&&(f.splice(e-1,2),e-=2);e=m(j.pkgs,f=a[0]);a=a.join("/");e&&a===f+"/"+e.main&&(a=f)}else 0===a.indexOf("./")&&(a=a.substring(2));if(C&&l&&(g||h)){f=a.split("/");for(e=f.length;0<e;e-=1){b=f.slice(0,e).join("/");if(g)for(n=g.length;0<n;n-=1)if(C=m(l,g.slice(0,n).join("/")))if(C=m(C,b)){c=C;d=e;break}if(c)break;!T&&(h&&m(h,b))&&(T=m(h,b),k=e)}!c&& -T&&(c=T,d=k);c&&(f.splice(0,d,c),a=f.join("/"))}return a}function d(a){A&&z(document.getElementsByTagName("script"),function(f){if(f.getAttribute("data-requiremodule")===a&&f.getAttribute("data-requirecontext")===k.contextName)return f.parentNode.removeChild(f),!0})}function p(a){var f=m(j.paths,a);if(f&&K(f)&&1<f.length)return d(a),f.shift(),k.require.undef(a),k.require([a]),!0}function g(a){var f,b=a?a.indexOf("!"):-1;-1<b&&(f=a.substring(0,b),a=a.substring(b+1,a.length));return[f,a]}function l(a, -f,b,e){var n,D,i=null,d=f?f.name:null,l=a,h=!0,j="";a||(h=!1,a="_@r"+(N+=1));a=g(a);i=a[0];a=a[1];i&&(i=c(i,d,e),D=m(r,i));a&&(i?j=D&&D.normalize?D.normalize(a,function(a){return c(a,d,e)}):c(a,d,e):(j=c(a,d,e),a=g(j),i=a[0],j=a[1],b=!0,n=k.nameToUrl(j)));b=i&&!D&&!b?"_unnormalized"+(O+=1):"";return{prefix:i,name:j,parentMap:f,unnormalized:!!b,url:n,originalName:l,isDefine:h,id:(i?i+"!"+j:j)+b}}function s(a){var f=a.id,b=m(q,f);b||(b=q[f]=new k.Module(a));return b}function u(a,f,b){var e=a.id,n=m(q, -e);if(t(r,e)&&(!n||n.defineEmitComplete))"defined"===f&&b(r[e]);else if(n=s(a),n.error&&"error"===f)b(n.error);else n.on(f,b)}function w(a,f){var b=a.requireModules,e=!1;if(f)f(a);else if(z(b,function(f){if(f=m(q,f))f.error=a,f.events.error&&(e=!0,f.emit("error",a))}),!e)h.onError(a)}function x(){U.length&&(ja.apply(I,[I.length-1,0].concat(U)),U=[])}function y(a){delete q[a];delete W[a]}function G(a,f,b){var e=a.map.id;a.error?a.emit("error",a.error):(f[e]=!0,z(a.depMaps,function(e,c){var d=e.id, -g=m(q,d);g&&(!a.depMatched[c]&&!b[d])&&(m(f,d)?(a.defineDep(c,r[d]),a.check()):G(g,f,b))}),b[e]=!0)}function E(){var a,f,b,e,n=(b=1E3*j.waitSeconds)&&k.startTime+b<(new Date).getTime(),c=[],i=[],g=!1,l=!0;if(!X){X=!0;H(W,function(b){a=b.map;f=a.id;if(b.enabled&&(a.isDefine||i.push(b),!b.error))if(!b.inited&&n)p(f)?g=e=!0:(c.push(f),d(f));else if(!b.inited&&(b.fetched&&a.isDefine)&&(g=!0,!a.prefix))return l=!1});if(n&&c.length)return b=B("timeout","Load timeout for modules: "+c,null,c),b.contextName= -k.contextName,w(b);l&&z(i,function(a){G(a,{},{})});if((!n||e)&&g)if((A||ea)&&!Y)Y=setTimeout(function(){Y=0;E()},50);X=!1}}function F(a){t(r,a[0])||s(l(a[0],null,!0)).init(a[1],a[2])}function L(a){var a=a.currentTarget||a.srcElement,b=k.onScriptLoad;a.detachEvent&&!Z?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=k.onScriptError;(!a.detachEvent||Z)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function M(){var a;for(x();I.length;){a= -I.shift();if(null===a[0])return w(B("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));F(a)}}var X,$,k,P,Y,j={waitSeconds:7,baseUrl:"./",paths:{},pkgs:{},shim:{},config:{}},q={},W={},aa={},I=[],r={},V={},N=1,O=1;P={require:function(a){return a.require?a.require:a.require=k.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?a.exports:a.exports=r[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){var b= -m(j.pkgs,a.map.id);return(b?m(j.config,a.map.id+"/"+b.main):m(j.config,a.map.id))||{}},exports:r[a.map.id]}}};$=function(a){this.events=m(aa,a.id)||{};this.map=a;this.shim=m(j.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};$.prototype={init:function(a,b,c,e){e=e||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=v(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited= -!0;this.ignore=e.ignore;e.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;k.startTime=(new Date).getTime();var a=this.map;if(this.shim)k.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],v(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a= -this.map.url;V[a]||(V[a]=!0,k.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var e=this.exports,n=this.factory;if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(J(n)){if(this.events.error&&this.map.isDefine||h.onError!==ca)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!== -this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(r[c]=e,h.onResourceLoad))h.onResourceLoad(k,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete= -!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=l(a.prefix);this.depMaps.push(d);u(d,"defined",v(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,C=k.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=l(a.prefix+"!"+d,this.map.parentMap),u(e,"defined",v(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})), -d=m(q,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",v(this,function(a){this.emit("error",a)}));d.enable()}}else n=v(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=v(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];H(q,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),n.fromText=v(this,function(e,c){var d=a.name,g=l(d),i=Q;c&&(e=c);i&&(Q=!1);s(g);t(j.config,b)&&(j.config[d]=j.config[b]);try{h.exec(e)}catch(D){return w(B("fromtexteval", -"fromText eval for "+b+" failed: "+D,D,[b]))}i&&(Q=!0);this.depMaps.push(g);k.completeLoad(d);C([d],n)}),e.load(a.name,C,n,j)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;z(this.depMaps,v(this,function(a,b){var c,e;if("string"===typeof a){a=l(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(P,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;u(a,"defined",v(this,function(a){this.defineDep(b, -a);this.check()}));this.errback&&u(a,"error",v(this,this.errback))}c=a.id;e=q[c];!t(P,c)&&(e&&!e.enabled)&&k.enable(a,this)}));H(this.pluginMaps,v(this,function(a){var b=m(q,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){z(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:j,contextName:b,registry:q,defined:r,urlFetched:V,defQueue:I,Module:$,makeModuleMap:l, -nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.pkgs,c=j.shim,e={paths:!0,config:!0,map:!0};H(a,function(a,b){e[b]?"map"===b?(j.map||(j.map={}),S(j[b],a,!0,!0)):S(j[b],a,!0):j[b]=a});a.shim&&(H(a.shim,function(a,b){K(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),j.shim=c);a.packages&&(z(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name, -location:a.location||a.name,main:(a.main||"main").replace(ka,"").replace(fa,"")}}),j.pkgs=b);H(q,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=l(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,f){function d(e,c,g){var i,j;f.enableBuildCallback&&(c&&J(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(J(c))return w(B("requireargs", -"Invalid require call"),g);if(a&&t(P,e))return P[e](q[a.id]);if(h.get)return h.get(k,e,a,d);i=l(e,a,!1,!0);i=i.id;return!t(r,i)?w(B("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[i]}M();k.nextTick(function(){M();j=s(l(null,a));j.skipMap=f.skipMap;j.init(e,c,g,{enabled:!0});E()});return d}f=f||{};S(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1<f))d=b.substring(f,b.length),b= -b.substring(0,f);return k.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return t(r,l(b,a,!1,!0).id)},specified:function(b){b=l(b,a,!1,!0).id;return t(r,b)||t(q,b)}});a||(d.undef=function(b){x();var c=l(b,a,!0),d=m(q,b);delete r[b];delete V[c.url];delete aa[b];d&&(d.events.defined&&(aa[b]=d.events),y(b))});return d},enable:function(a){m(q,a.id)&&s(a).enable()},completeLoad:function(a){var b,c,e=m(j.shim,a)||{},d=e.exports;for(x();I.length;){c=I.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]=== -a&&(b=!0);F(c)}c=m(q,a);if(!b&&!t(r,a)&&c&&!c.inited){if(j.enforceDefine&&(!d||!da(d)))return p(a)?void 0:w(B("nodefine","No define call for "+a,null,[a]));F([a,e.deps||[],e.exportsFn])}E()},nameToUrl:function(a,b,c){var d,g,l,i,k,p;if(h.jsExtRegExp.test(a))i=a+(b||"");else{d=j.paths;g=j.pkgs;i=a.split("/");for(k=i.length;0<k;k-=1)if(p=i.slice(0,k).join("/"),l=m(g,p),p=m(d,p)){K(p)&&(p=p[0]);i.splice(0,k,p);break}else if(l){a=a===l.name?l.location+"/"+l.main:l.location;i.splice(0,k,a);break}i=i.join("/"); -i+=b||(/\?/.test(i)||c?"":".js");i=("/"===i.charAt(0)||i.match(/^[\w\+\.\-]+:/)?"":j.baseUrl)+i}return j.urlArgs?i+((-1===i.indexOf("?")?"?":"&")+j.urlArgs):i},load:function(a,b){h.load(k,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||la.test((a.currentTarget||a.srcElement).readyState))R=null,a=L(a),k.completeLoad(a.id)},onScriptError:function(a){var b=L(a);if(!p(b.id))return w(B("scripterror","Script error for: "+b.id,a,[b.id]))}};k.require=k.makeRequire(); -return k}var h,x,y,E,L,F,R,M,s,ga,ma=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,na=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,fa=/\.js$/,ka=/^\.\//;x=Object.prototype;var N=x.toString,ha=x.hasOwnProperty,ja=Array.prototype.splice,A=!!("undefined"!==typeof window&&navigator&&window.document),ea=!A&&"undefined"!==typeof importScripts,la=A&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,Z="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),G={},u={},U=[],Q= -!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(J(requirejs))return;u=requirejs;requirejs=void 0}"undefined"!==typeof require&&!J(require)&&(u=require,require=void 0);h=requirejs=function(b,c,d,p){var g,l="_";!K(b)&&"string"!==typeof b&&(g=b,K(c)?(b=c,c=d,d=p):b=[]);g&&g.context&&(l=g.context);(p=m(G,l))||(p=G[l]=h.s.newContext(l));g&&p.configure(g);return p.require(b,c,d)};h.config=function(b){return h(b)};h.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b, -4)}:function(b){b()};require||(require=h);h.version="2.1.6";h.jsExtRegExp=/^\/|:|\?|\.js$/;h.isBrowser=A;x=h.s={contexts:G,newContext:ia};h({});z(["toUrl","undef","defined","specified"],function(b){h[b]=function(){var c=G._;return c.require[b].apply(c,arguments)}});if(A&&(y=x.head=document.getElementsByTagName("head")[0],E=document.getElementsByTagName("base")[0]))y=x.head=E.parentNode;h.onError=ca;h.load=function(b,c,d){var h=b&&b.config||{},g;if(A)return g=h.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml", -"html:script"):document.createElement("script"),g.type=h.scriptType||"text/javascript",g.charset="utf-8",g.async=!0,g.setAttribute("data-requirecontext",b.contextName),g.setAttribute("data-requiremodule",c),g.attachEvent&&!(g.attachEvent.toString&&0>g.attachEvent.toString().indexOf("[native code"))&&!Z?(Q=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,E?y.insertBefore(g,E):y.appendChild(g), -M=null,g;if(ea)try{importScripts(d),b.completeLoad(c)}catch(l){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,l,[c]))}};A&&O(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return s=L,u.baseUrl||(F=s.split("/"),s=F.pop(),ga=F.length?F.join("/")+"/":"./",u.baseUrl=ga),s=s.replace(fa,""),h.jsExtRegExp.test(s)&&(s=L),u.deps=u.deps?u.deps.concat(s):[s],!0});define=function(b,c,d){var h,g;"string"!==typeof b&&(d=c,c=b,b=null); -K(c)||(d=c,c=null);!c&&J(d)&&(c=[],d.length&&(d.toString().replace(ma,"").replace(na,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(Q){if(!(h=M))R&&"interactive"===R.readyState||O(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return R=b}),h=R;h&&(b||(b=h.getAttribute("data-requiremodule")),g=G[h.getAttribute("data-requirecontext")])}(g?g.defQueue:U).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)}; -h(u)}})(this); \ No newline at end of file +(function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function X(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));--d);}}function x(b,c){return la.call(b,c)}function e(b,c){return x(b,c)&&b[c]}function D(b,c){for(var d in b)if(x(b,d)&&c(b[d],d))break}function Y(b,c,d,g){c&&D(c,function(c,e){if(d||!x(b,e))!g||"object"!== +typeof c||!c||L(c)||K(c)||c instanceof RegExp?b[e]=c:(b[e]||(b[e]={}),Y(b[e],c,d,g))});return b}function z(b,c){return function(){return c.apply(b,arguments)}}function ha(b){throw b;}function ia(b){if(!b)return b;var c=ga;y(b.split("."),function(b){c=c[b]});return c}function F(b,c,d,g){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=g;d&&(c.originalError=d);return c}function ma(b){function c(a,n,b){var h,k,f,c,d,l,g,r;n=n&&n.split("/");var q=p.map,m=q&&q["*"]; +if(a){a=a.split("/");k=a.length-1;p.nodeIdCompat&&U.test(a[k])&&(a[k]=a[k].replace(U,""));"."===a[0].charAt(0)&&n&&(k=n.slice(0,n.length-1),a=k.concat(a));k=a;for(f=0;f<k.length;f++)c=k[f],"."===c?(k.splice(f,1),--f):".."===c&&0!==f&&(1!==f||".."!==k[2])&&".."!==k[f-1]&&0<f&&(k.splice(f-1,2),f-=2);a=a.join("/")}if(b&&q&&(n||m)){k=a.split("/");f=k.length;a:for(;0<f;--f){d=k.slice(0,f).join("/");if(n)for(c=n.length;0<c;--c)if(b=e(q,n.slice(0,c).join("/")))if(b=e(b,d)){h=b;l=f;break a}!g&&m&&e(m,d)&& +(g=e(m,d),r=f)}!h&&g&&(h=g,l=r);h&&(k.splice(0,l,h),a=k.join("/"))}return(h=e(p.pkgs,a))?h:a}function d(a){E&&y(document.getElementsByTagName("script"),function(n){if(n.getAttribute("data-requiremodule")===a&&n.getAttribute("data-requirecontext")===l.contextName)return n.parentNode.removeChild(n),!0})}function m(a){var n=e(p.paths,a);if(n&&L(n)&&1<n.length)return n.shift(),l.require.undef(a),l.makeRequire(null,{skipMap:!0})([a]),!0}function r(a){var n,b=a?a.indexOf("!"):-1;-1<b&&(n=a.substring(0, +b),a=a.substring(b+1,a.length));return[n,a]}function q(a,n,b,h){var k,f,d=null,g=n?n.name:null,p=a,q=!0,m="";a||(q=!1,a="_@r"+(Q+=1));a=r(a);d=a[0];a=a[1];d&&(d=c(d,g,h),f=e(v,d));a&&(d?m=f&&f.normalize?f.normalize(a,function(a){return c(a,g,h)}):-1===a.indexOf("!")?c(a,g,h):a:(m=c(a,g,h),a=r(m),d=a[0],m=a[1],b=!0,k=l.nameToUrl(m)));b=!d||f||b?"":"_unnormalized"+(T+=1);return{prefix:d,name:m,parentMap:n,unnormalized:!!b,url:k,originalName:p,isDefine:q,id:(d?d+"!"+m:m)+b}}function u(a){var b=a.id, +c=e(t,b);c||(c=t[b]=new l.Module(a));return c}function w(a,b,c){var h=a.id,k=e(t,h);if(!x(v,h)||k&&!k.defineEmitComplete)if(k=u(a),k.error&&"error"===b)c(k.error);else k.on(b,c);else"defined"===b&&c(v[h])}function A(a,b){var c=a.requireModules,h=!1;if(b)b(a);else if(y(c,function(b){if(b=e(t,b))b.error=a,b.events.error&&(h=!0,b.emit("error",a))}),!h)g.onError(a)}function B(){V.length&&(y(V,function(a){var b=a[0];"string"===typeof b&&(l.defQueueMap[b]=!0);G.push(a)}),V=[])}function C(a){delete t[a]; +delete Z[a]}function J(a,b,c){var h=a.map.id;a.error?a.emit("error",a.error):(b[h]=!0,y(a.depMaps,function(h,f){var d=h.id,g=e(t,d);!g||a.depMatched[f]||c[d]||(e(b,d)?(a.defineDep(f,v[d]),a.check()):J(g,b,c))}),c[h]=!0)}function H(){var a,b,c=(a=1E3*p.waitSeconds)&&l.startTime+a<(new Date).getTime(),h=[],k=[],f=!1,g=!0;if(!aa){aa=!0;D(Z,function(a){var l=a.map,e=l.id;if(a.enabled&&(l.isDefine||k.push(a),!a.error))if(!a.inited&&c)m(e)?f=b=!0:(h.push(e),d(e));else if(!a.inited&&a.fetched&&l.isDefine&& +(f=!0,!l.prefix))return g=!1});if(c&&h.length)return a=F("timeout","Load timeout for modules: "+h,null,h),a.contextName=l.contextName,A(a);g&&y(k,function(a){J(a,{},{})});c&&!b||!f||!E&&!ja||ba||(ba=setTimeout(function(){ba=0;H()},50));aa=!1}}function I(a){x(v,a[0])||u(q(a[0],null,!0)).init(a[1],a[2])}function O(a){a=a.currentTarget||a.srcElement;var b=l.onScriptLoad;a.detachEvent&&!ca?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=l.onScriptError;a.detachEvent&&!ca||a.removeEventListener("error", +b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function P(){var a;for(B();G.length;){a=G.shift();if(null===a[0])return A(F("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));I(a)}l.defQueueMap={}}var aa,da,l,R,ba,p={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},t={},Z={},ea={},G=[],v={},W={},fa={},Q=1,T=1;R={require:function(a){return a.require?a.require:a.require=l.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports? +v[a.map.id]=a.exports:a.exports=v[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return e(p.config,a.map.id)||{}},exports:a.exports||(a.exports={})}}};da=function(a){this.events=e(ea,a.id)||{};this.map=a;this.shim=e(p.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};da.prototype={init:function(a,b,c,h){h=h||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&& +(c=z(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=h.ignore;h.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,--this.depCount,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;l.startTime=(new Date).getTime();var a=this.map;if(this.shim)l.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],z(this,function(){return a.prefix?this.callPlugin(): +this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;W[a]||(W[a]=!0,l.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var h=this.exports,k=this.factory;if(!this.inited)x(l.defQueueMap,c)||this.fetch();else if(this.error)this.emit("error",this.error);else if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!== +ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c); +this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})|| +""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0=== +a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a, +b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id); +b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b= +a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&& +(p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs", +"Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1<f)&&(d=b.substring(f,b.length),b=b.substring(0, +f));return l.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return x(v,q(b,a,!1,!0).id)},specified:function(b){b=q(b,a,!1,!0).id;return x(v,b)||x(t,b)}});a||(m.undef=function(b){B();var c=q(b,a,!0),f=e(t,b);f.undefed=!0;d(b);delete v[b];delete W[c.url];delete ea[b];X(G,function(a,c){a[0]===b&&G.splice(c,1)});delete l.defQueueMap[b];f&&(f.events.defined&&(ea[b]=f.events),C(b))});return m},enable:function(a){e(t,a.id)&&u(a).enable()},completeLoad:function(a){var b,c,d=e(p.shim,a)||{},g=d.exports; +for(B();G.length;){c=G.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);I(c)}l.defQueueMap={};c=e(t,a);if(!b&&!x(v,a)&&c&&!c.inited)if(!p.enforceDefine||g&&ia(g))I([a,d.deps||[],d.exportsFn]);else return m(a)?void 0:A(F("nodefine","No define call for "+a,null,[a]));H()},nameToUrl:function(a,b,c){var d,k,f,m;(d=e(p.pkgs,a))&&(a=d);if(d=e(fa,a))return l.nameToUrl(d,b,c);if(g.jsExtRegExp.test(a))d=a+(b||"");else{d=p.paths;k=a.split("/");for(f=k.length;0<f;--f)if(m=k.slice(0,f).join("/"), +m=e(d,m)){L(m)&&(m=m[0]);k.splice(0,f,m);break}d=k.join("/");d+=b||(/^data\:|^blob\:|\?/.test(d)||c?"":".js");d=("/"===d.charAt(0)||d.match(/^[\w\+\.\-]+:/)?"":p.baseUrl)+d}return p.urlArgs&&!/^blob\:/.test(d)?d+p.urlArgs(a,d):d},load:function(a,b){g.load(l,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||oa.test((a.currentTarget||a.srcElement).readyState))N=null,a=O(a),l.completeLoad(a.id)},onScriptError:function(a){var b=O(a);if(!m(b.id)){var c=[]; +D(t,function(a,d){0!==d.indexOf("_@r")&&y(a.depMaps,function(a){if(a.id===b.id)return c.push(d),!0})});return A(F("scripterror",'Script error for "'+b.id+(c.length?'", needed by: '+c.join(", "):'"'),a,[b.id]))}}};l.require=l.makeRequire();return l}function pa(){if(N&&"interactive"===N.readyState)return N;X(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b});return N}var g,B,C,H,O,I,N,P,u,T,qa=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ra=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, +U=/\.js$/,na=/^\.\//;B=Object.prototype;var Q=B.toString,la=B.hasOwnProperty,E=!("undefined"===typeof window||"undefined"===typeof navigator||!window.document),ja=!E&&"undefined"!==typeof importScripts,oa=E&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,ca="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),J={},w={},V=[],S=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(K(requirejs))return;w=requirejs;requirejs=void 0}"undefined"===typeof require|| +K(require)||(w=require,require=void 0);g=requirejs=function(b,c,d,m){var r,q="_";L(b)||"string"===typeof b||(r=b,L(c)?(b=c,c=d,d=m):b=[]);r&&r.context&&(q=r.context);(m=e(J,q))||(m=J[q]=g.s.newContext(q));r&&m.configure(r);return m.require(b,c,d)};g.config=function(b){return g(b)};g.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=g);g.version="2.2.0";g.jsExtRegExp=/^\/|:|\?|\.js$/;g.isBrowser=E;B=g.s={contexts:J,newContext:ma};g({});y(["toUrl", +"undef","defined","specified"],function(b){g[b]=function(){var c=J._;return c.require[b].apply(c,arguments)}});E&&(C=B.head=document.getElementsByTagName("head")[0],H=document.getElementsByTagName("base")[0])&&(C=B.head=H.parentNode);g.onError=ha;g.createNode=function(b,c,d){c=b.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");c.type=b.scriptType||"text/javascript";c.charset="utf-8";c.async=!0;return c};g.load=function(b,c,d){var m=b&&b.config|| +{},e;if(E){e=g.createNode(m,c,d);e.setAttribute("data-requirecontext",b.contextName);e.setAttribute("data-requiremodule",c);!e.attachEvent||e.attachEvent.toString&&0>e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){}, +0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&& +(d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this); diff --git a/public/vendor/tinycon/tinycon.js b/public/vendor/tinycon/tinycon.js index 3317cc0d03..3e3657cdf8 100644 --- a/public/vendor/tinycon/tinycon.js +++ b/public/vendor/tinycon/tinycon.js @@ -1,188 +1,200 @@ /*! * Tinycon - A small library for manipulating the Favicon * Tom Moor, http://tommoor.com - * Copyright (c) 2012 Tom Moor - * MIT Licensed - * @version 0.6.1 + * Copyright (c) 2015 Tom Moor + * @license MIT Licensed + * @version 0.6.4 */ (function(){ - var Tinycon = {}; - var currentFavicon = null; - var originalFavicon = null; - var originalTitle = document.title; - var faviconImage = null; - var canvas = null; - var options = {}; - var r = window.devicePixelRatio || 1; - var size = 16 * r; - var defaults = { - width: 7, - height: 9, - font: 9 * r + 'px arial', - colour: '#ffffff', - background: '#F03D25', - fallback: true, - crossOrigin: true, - abbreviate: true - }; - - var ua = (function () { - var agent = navigator.userAgent.toLowerCase(); - // New function has access to 'agent' via closure - return function (browser) { - return agent.indexOf(browser) !== -1; - }; - }()); - - var browser = { - ie: ua('msie'), - chrome: ua('chrome'), - webkit: ua('chrome') || ua('safari'), - safari: ua('safari') && !ua('chrome'), - mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') - }; - - // private methods - var getFaviconTag = function(){ - - var links = document.getElementsByTagName('link'); - - for(var i=0, len=links.length; i < len; i++) { - if ((links[i].getAttribute('rel') || '').match(/\bicon\b/)) { - return links[i]; - } - } - - return false; - }; - - var removeFaviconTag = function(){ - - var links = document.getElementsByTagName('link'); - var head = document.getElementsByTagName('head')[0]; - - for(var i=0, len=links.length; i < len; i++) { - var exists = (typeof(links[i]) !== 'undefined'); - if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/)) { - head.removeChild(links[i]); - } - } - }; - - var getCurrentFavicon = function(){ - - if (!originalFavicon || !currentFavicon) { - var tag = getFaviconTag(); - originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; - } - - return currentFavicon; - }; - - var getCanvas = function (){ - - if (!canvas) { - canvas = document.createElement("canvas"); - canvas.width = size; - canvas.height = size; - } - - return canvas; - }; - - var setFaviconTag = function(url){ - removeFaviconTag(); - - var link = document.createElement('link'); - link.type = 'image/x-icon'; - link.rel = 'icon'; - link.href = url; - document.getElementsByTagName('head')[0].appendChild(link); - }; - - var log = function(message){ - if (window.console) window.console.log(message); - }; - - var drawFavicon = function(label, colour) { - - // fallback to updating the browser title if unsupported - if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { - return updateTitle(label); - } - - var context = getCanvas().getContext("2d"); - var colour = colour || '#000000'; - var src = getCurrentFavicon(); - - faviconImage = document.createElement('img'); - faviconImage.onload = function() { - - // clear canvas - context.clearRect(0, 0, size, size); - - // draw the favicon - context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size); + var Tinycon = {}; + var currentFavicon = null; + var originalFavicon = null; + var faviconImage = null; + var canvas = null; + var options = {}; + var r = window.devicePixelRatio || 1; + var size = 16 * r; + var defaults = { + width: 7, + height: 9, + font: 10 * r + 'px arial', + color: '#ffffff', + background: '#F03D25', + fallback: true, + crossOrigin: true, + abbreviate: true + }; + + var ua = (function () { + var agent = navigator.userAgent.toLowerCase(); + // New function has access to 'agent' via closure + return function (browser) { + return agent.indexOf(browser) !== -1; + }; + }()); + + var browser = { + ie: ua('trident'), + chrome: ua('chrome'), + webkit: ua('chrome') || ua('safari'), + safari: ua('safari') && !ua('chrome'), + mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') + }; + + // private methods + var getFaviconTag = function(){ + + var links = document.getElementsByTagName('link'); + + for(var i=0, len=links.length; i < len; i++) { + if ((links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { + return links[i]; + } + } + + return false; + }; + + var removeFaviconTag = function(){ + + var links = document.getElementsByTagName('link'); + var head = document.getElementsByTagName('head')[0]; + + for(var i=0, len=links.length; i < len; i++) { + var exists = (typeof(links[i]) !== 'undefined'); + if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/i)) { + head.removeChild(links[i]); + } + } + }; + + var getCurrentFavicon = function(){ + + if (!originalFavicon || !currentFavicon) { + var tag = getFaviconTag(); + currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; + if (!originalFavicon) { + originalFavicon = currentFavicon; + } + } + + return currentFavicon; + }; + + var getCanvas = function (){ + + if (!canvas) { + canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + } + + return canvas; + }; + + var setFaviconTag = function(url){ + if(url){ + removeFaviconTag(); + + var link = document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'icon'; + link.href = url; + document.getElementsByTagName('head')[0].appendChild(link); + } + }; + + var log = function(message){ + if (window.console) window.console.log(message); + }; + + var drawFavicon = function(label, color) { + + // fallback to updating the browser title if unsupported + if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { + return updateTitle(label); + } + + var context = getCanvas().getContext("2d"); + var color = color || '#000000'; + var src = getCurrentFavicon(); + + faviconImage = document.createElement('img'); + faviconImage.onload = function() { + + // clear canvas + context.clearRect(0, 0, size, size); + + // draw the favicon + context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size); + + // draw bubble over the top + if ((label + '').length > 0) drawBubble(context, label, color); - // draw bubble over the top - if ((label + '').length > 0) drawBubble(context, label, colour); + // refresh tag in page + refreshFavicon(); + }; - // refresh tag in page - refreshFavicon(); - }; + // allow cross origin resource requests if the image is not a data:uri + // as detailed here: https://github.com/mrdoob/three.js/issues/1305 + if (!src.match(/^data/) && options.crossOrigin) { + faviconImage.crossOrigin = 'anonymous'; + } - // allow cross origin resource requests if the image is not a data:uri - // as detailed here: https://github.com/mrdoob/three.js/issues/1305 - if (!src.match(/^data/) && options.crossOrigin) { - faviconImage.crossOrigin = 'anonymous'; - } + faviconImage.src = src; + }; - faviconImage.src = src; - }; + var updateTitle = function(label) { - var updateTitle = function(label) { + if (options.fallback) { + // Grab the current title that we can prefix with the label + var originalTitle = document.title; - if (options.fallback) { - if ((label + '').length > 0) { - document.title = '(' + label + ') ' + originalTitle; - } else { - document.title = originalTitle; - } - } - }; + // Strip out the old label if there is one + if (originalTitle[0] === '(') { + originalTitle = originalTitle.slice(originalTitle.indexOf(' ')); + } - var drawBubble = function(context, label, colour) { + if ((label + '').length > 0) { + document.title = '(' + label + ') ' + originalTitle; + } else { + document.title = originalTitle; + } + } + }; - // automatic abbreviation for long (>2 digits) numbers - if (typeof label == 'number' && label > 99 && options.abbreviate) { - label = abbreviateNumber(label); - } + var drawBubble = function(context, label, color) { - // bubble needs to be larger for double digits - var len = (label + '').length-1; + // automatic abbreviation for long (>2 digits) numbers + if (typeof label == 'number' && label > 99 && options.abbreviate) { + label = abbreviateNumber(label); + } - var width = options.width * r + (6 * r * len), - height = options.height * r; + // bubble needs to be larger for double digits + var len = (label + '').length-1; - var top = size - height, + var width = options.width * r + (6 * r * len), + height = options.height * r; + + var top = size - height, left = size - width - r, bottom = 16 * r, right = 16 * r, radius = 2 * r; - // webkit seems to render fonts lighter than firefox - context.font = (browser.webkit ? 'bold ' : '') + options.font; - context.fillStyle = options.background; - context.strokeStyle = options.background; - context.lineWidth = r; + // webkit seems to render fonts lighter than firefox + context.font = (browser.webkit ? 'bold ' : '') + options.font; + context.fillStyle = options.background; + context.strokeStyle = options.background; + context.lineWidth = r; - // bubble - context.beginPath(); + // bubble + context.beginPath(); context.moveTo(left + radius, top); - context.quadraticCurveTo(left, top, left, top + radius); - context.lineTo(left, bottom - radius); + context.quadraticCurveTo(left, top, left, top + radius); + context.lineTo(left, bottom - radius); context.quadraticCurveTo(left, bottom, left + radius, bottom); context.lineTo(right - radius, bottom); context.quadraticCurveTo(right, bottom, right, bottom - radius); @@ -191,77 +203,85 @@ context.closePath(); context.fill(); - // bottom shadow - context.beginPath(); - context.strokeStyle = "rgba(0,0,0,0.3)"; - context.moveTo(left + radius / 2.0, bottom); - context.lineTo(right - radius / 2.0, bottom); - context.stroke(); - - // label - context.fillStyle = options.colour; - context.textAlign = "right"; - context.textBaseline = "top"; - - // unfortunately webkit/mozilla are a pixel different in text positioning - context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r); - }; - - var refreshFavicon = function(){ - // check support - if (!getCanvas().getContext) return; - - setFaviconTag(getCanvas().toDataURL()); - }; - - var abbreviateNumber = function(label) { - var metricPrefixes = [ - ['G', 1000000000], - ['M', 1000000], - ['k', 1000] - ]; - - for(var i = 0; i < metricPrefixes.length; ++i) { - if (label >= metricPrefixes[i][1]) { - label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; - break; - } - } - - return label; - }; - - var round = function (value, precision) { - var number = new Number(value); - return number.toFixed(precision); - }; - - // public methods - Tinycon.setOptions = function(custom){ - options = {}; - - for(var key in defaults){ - options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; - } - return this; - }; - - Tinycon.setImage = function(url){ - currentFavicon = url; - refreshFavicon(); - return this; - }; - - Tinycon.setBubble = function(label, colour) { - label = label || ''; - drawFavicon(label, colour); - return this; - }; - - Tinycon.reset = function(){ - setFaviconTag(originalFavicon); - }; - - Tinycon.setOptions(defaults); - window.Tinycon = Tinycon; -})(); \ No newline at end of file + // bottom shadow + context.beginPath(); + context.strokeStyle = "rgba(0,0,0,0.3)"; + context.moveTo(left + radius / 2.0, bottom); + context.lineTo(right - radius / 2.0, bottom); + context.stroke(); + + // label + context.fillStyle = options.color; + context.textAlign = "right"; + context.textBaseline = "top"; + + // unfortunately webkit/mozilla are a pixel different in text positioning + context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r); + }; + + var refreshFavicon = function(){ + // check support + if (!getCanvas().getContext) return; + + setFaviconTag(getCanvas().toDataURL()); + }; + + var abbreviateNumber = function(label) { + var metricPrefixes = [ + ['G', 1000000000], + ['M', 1000000], + ['k', 1000] + ]; + + for(var i = 0; i < metricPrefixes.length; ++i) { + if (label >= metricPrefixes[i][1]) { + label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; + break; + } + } + + return label; + }; + + var round = function (value, precision) { + var number = new Number(value); + return number.toFixed(precision); + }; + + // public methods + Tinycon.setOptions = function(custom){ + options = {}; + + // account for deprecated UK English spelling + if (custom.colour) { + custom.color = custom.colour; + } + + for(var key in defaults){ + options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; + } + return this; + }; + + Tinycon.setImage = function(url){ + currentFavicon = url; + refreshFavicon(); + return this; + }; + + Tinycon.setBubble = function(label, color) { + label = label || ''; + drawFavicon(label, color); + return this; + }; + + Tinycon.reset = function(){ + currentFavicon = originalFavicon; + setFaviconTag(originalFavicon); + }; + + Tinycon.setOptions(defaults); + + window.Tinycon = Tinycon; + +})(); diff --git a/src/analytics.js b/src/analytics.js index c1ede42eba..ab834b75b2 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -6,7 +6,7 @@ var winston = require('winston'); var db = require('./database'); -(function(Analytics) { +(function (Analytics) { var counters = {}; var pageViews = 0; @@ -15,24 +15,24 @@ var db = require('./database'); var isCategory = /^(?:\/api)?\/category\/(\d+)/; - new cronJob('*/10 * * * *', function() { + new cronJob('*/10 * * * *', function () { Analytics.writeData(); }, null, true); - Analytics.increment = function(keys) { + Analytics.increment = function (keys) { keys = Array.isArray(keys) ? keys : [keys]; - keys.forEach(function(key) { + keys.forEach(function (key) { counters[key] = counters[key] || 0; ++counters[key]; }); }; - Analytics.pageView = function(payload) { + Analytics.pageView = function (payload) { ++pageViews; if (payload.ip) { - db.sortedSetScore('ip:recent', payload.ip, function(err, score) { + db.sortedSetScore('ip:recent', payload.ip, function (err, score) { if (err) { return; } @@ -58,7 +58,7 @@ var db = require('./database'); } }; - Analytics.writeData = function() { + Analytics.writeData = function () { var today = new Date(); var month = new Date(); var dbQueue = []; @@ -92,14 +92,14 @@ var db = require('./database'); } } - async.parallel(dbQueue, function(err) { + async.parallel(dbQueue, function (err) { if (err) { winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); } }); }; - Analytics.getHourlyStatsForSet = function(set, hour, numHours, callback) { + Analytics.getHourlyStatsForSet = function (set, hour, numHours, callback) { var terms = {}, hoursArr = []; @@ -111,19 +111,19 @@ var db = require('./database'); hour.setHours(hour.getHours() - 1, 0, 0, 0); } - db.sortedSetScores(set, hoursArr, function(err, counts) { + db.sortedSetScores(set, hoursArr, function (err, counts) { if (err) { return callback(err); } - hoursArr.forEach(function(term, index) { + hoursArr.forEach(function (term, index) { terms[term] = parseInt(counts[index], 10) || 0; }); var termsArr = []; hoursArr.reverse(); - hoursArr.forEach(function(hour) { + hoursArr.forEach(function (hour) { termsArr.push(terms[hour]); }); @@ -131,36 +131,36 @@ var db = require('./database'); }); }; - Analytics.getDailyStatsForSet = function(set, day, numDays, callback) { + Analytics.getDailyStatsForSet = function (set, day, numDays, callback) { var daysArr = []; day = new Date(day); - day.setDate(day.getDate()+1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values + day.setDate(day.getDate() + 1); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values day.setHours(0, 0, 0, 0); - async.whilst(function() { + async.whilst(function () { return numDays--; - }, function(next) { - Analytics.getHourlyStatsForSet(set, day.getTime()-(1000*60*60*24*numDays), 24, function(err, day) { + }, function (next) { + Analytics.getHourlyStatsForSet(set, day.getTime() - (1000 * 60 * 60 * 24 * numDays), 24, function (err, day) { if (err) { return next(err); } - daysArr.push(day.reduce(function(cur, next) { - return cur+next; + daysArr.push(day.reduce(function (cur, next) { + return cur + next; })); next(); }); - }, function(err) { + }, function (err) { callback(err, daysArr); }); }; - Analytics.getUnwrittenPageviews = function() { + Analytics.getUnwrittenPageviews = function () { return pageViews; }; - Analytics.getMonthlyPageViews = function(callback) { + Analytics.getMonthlyPageViews = function (callback) { var thisMonth = new Date(); var lastMonth = new Date(); thisMonth.setMonth(thisMonth.getMonth(), 1); @@ -170,7 +170,7 @@ var db = require('./database'); var values = [thisMonth.getTime(), lastMonth.getTime()]; - db.sortedSetScores('analytics:pageviews:month', values, function(err, scores) { + db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { if (err) { return callback(err); } @@ -178,7 +178,7 @@ var db = require('./database'); }); }; - Analytics.getCategoryAnalytics = function(cid, callback) { + Analytics.getCategoryAnalytics = function (cid, callback) { async.parallel({ 'pageviews:hourly': async.apply(Analytics.getHourlyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 24), 'pageviews:daily': async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews:byCid:' + cid, Date.now(), 30), @@ -187,4 +187,11 @@ var db = require('./database'); }, callback); }; + Analytics.getErrorAnalytics = function (callback) { + async.parallel({ + 'not-found': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:404', Date.now(), 7), + 'toobusy': async.apply(Analytics.getDailyStatsForSet, 'analytics:errors:503', Date.now(), 7) + }, callback); + }; + }(exports)); \ No newline at end of file diff --git a/src/batch.js b/src/batch.js index 1a425e1a21..ca0944b80d 100644 --- a/src/batch.js +++ b/src/batch.js @@ -6,17 +6,17 @@ var async = require('async'), db = require('./database'), utils = require('../public/src/utils'); -(function(Batch) { +(function (Batch) { var DEFAULT_BATCH_SIZE = 100; - Batch.processSortedSet = function(setKey, process, options, callback) { + Batch.processSortedSet = function (setKey, process, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } - callback = typeof callback === 'function' ? callback : function(){}; + callback = typeof callback === 'function' ? callback : function () {}; options = options || {}; if (typeof process !== 'function') { @@ -29,7 +29,7 @@ var async = require('async'), } // custom done condition - options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function(){}; + options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function (){}; var batch = options.batch || DEFAULT_BATCH_SIZE; var start = 0; @@ -37,11 +37,11 @@ var async = require('async'), var done = false; async.whilst( - function() { + function () { return !done; }, - function(next) { - db.getSortedSetRange(setKey, start, stop, function(err, ids) { + function (next) { + db.getSortedSetRange(setKey, start, stop, function (err, ids) { if (err) { return next(err); } @@ -49,7 +49,7 @@ var async = require('async'), done = true; return next(); } - process(ids, function(err) { + process(ids, function (err) { if (err) { return next(err); } @@ -63,4 +63,52 @@ var async = require('async'), ); }; + Batch.processArray = function (array, process, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + callback = typeof callback === 'function' ? callback : function () {}; + options = options || {}; + + if (!Array.isArray(array) || !array.length) { + return callback(); + } + if (typeof process !== 'function') { + return callback(new Error('[[error:process-not-a-function]]')); + } + + var batch = options.batch || DEFAULT_BATCH_SIZE; + var start = 0; + var done = false; + + async.whilst( + function () { + return !done; + }, + function (next) { + var currentBatch = array.slice(start, start + batch); + if (!currentBatch.length) { + done = true; + return next(); + } + process(currentBatch, function (err) { + if (err) { + return next(err); + } + start = start + batch; + if (options.interval) { + setTimeout(next, options.interval); + } else { + next(); + } + }); + }, + function (err) { + callback(err); + } + ); + }; + }(exports)); diff --git a/src/categories.js b/src/categories.js index cd2b18a787..4e2c435f8e 100644 --- a/src/categories.js +++ b/src/categories.js @@ -9,7 +9,7 @@ var Groups = require('./groups'); var plugins = require('./plugins'); var privileges = require('./privileges'); -(function(Categories) { +(function (Categories) { require('./categories/data')(Categories); require('./categories/create')(Categories); @@ -20,11 +20,11 @@ var privileges = require('./privileges'); require('./categories/recentreplies')(Categories); require('./categories/update')(Categories); - Categories.exists = function(cid, callback) { + Categories.exists = function (cid, callback) { db.isSortedSetMember('categories:cid', cid, callback); }; - Categories.getCategoryById = function(data, callback) { + Categories.getCategoryById = function (data, callback) { var category; async.waterfall([ function (next) { @@ -35,15 +35,19 @@ var privileges = require('./privileges'); return next(new Error('[[error:invalid-cid]]')); } category = categories[0]; - if (parseInt(data.uid, 10)) { - Categories.markAsRead([data.cid], data.uid); - } async.parallel({ - topics: function(next) { + topics: function (next) { Categories.getCategoryTopics(data, next); }, - isIgnored: function(next) { + topicCount: function (next) { + if (Array.isArray(data.set)) { + db.sortedSetIntersectCard(data.set, next); + } else { + next(null, category.topic_count); + } + }, + isIgnored: function (next) { Categories.isIgnored([data.cid], data.uid, next); } }, next); @@ -52,6 +56,7 @@ var privileges = require('./privileges'); category.topics = results.topics.topics; category.nextStart = results.topics.nextStart; category.isIgnored = results.isIgnored[0]; + category.topic_count = results.topicCount; plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, next); }, @@ -61,24 +66,24 @@ var privileges = require('./privileges'); ], callback); }; - Categories.isIgnored = function(cids, uid, callback) { - user.getIgnoredCategories(uid, function(err, ignoredCids) { + Categories.isIgnored = function (cids, uid, callback) { + user.getIgnoredCategories(uid, function (err, ignoredCids) { if (err) { return callback(err); } - cids = cids.map(function(cid) { + cids = cids.map(function (cid) { return ignoredCids.indexOf(cid.toString()) !== -1; }); callback(null, cids); }); }; - Categories.getPageCount = function(cid, uid, callback) { + Categories.getPageCount = function (cid, uid, callback) { async.parallel({ topicCount: async.apply(Categories.getCategoryField, cid, 'topic_count'), settings: async.apply(user.getSettings, uid) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -91,8 +96,8 @@ var privileges = require('./privileges'); }); }; - Categories.getAllCategories = function(uid, callback) { - db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) { + Categories.getAllCategories = function (uid, callback) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { if (err || !Array.isArray(cids) || !cids.length) { return callback(err, []); } @@ -101,22 +106,22 @@ var privileges = require('./privileges'); }); }; - Categories.getCategoriesByPrivilege = function(set, uid, privilege, callback) { + Categories.getCategoriesByPrivilege = function (set, uid, privilege, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange(set, 0, -1, next); }, - function(cids, next) { + function (cids, next) { privileges.categories.filterCids(privilege, cids, uid, next); }, - function(cids, next) { + function (cids, next) { Categories.getCategories(cids, uid, next); } ], callback); }; - Categories.getModerators = function(cid, callback) { - Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function(err, uids) { + Categories.getModerators = function (cid, callback) { + Groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, function (err, uids) { if (err || !Array.isArray(uids) || !uids.length) { return callback(err, []); } @@ -126,7 +131,7 @@ var privileges = require('./privileges'); }; - Categories.getCategories = function(cids, uid, callback) { + Categories.getCategories = function (cids, uid, callback) { if (!Array.isArray(cids)) { return callback(new Error('[[error:invalid-cid]]')); } @@ -136,19 +141,19 @@ var privileges = require('./privileges'); } async.parallel({ - categories: function(next) { + categories: function (next) { Categories.getCategoriesData(cids, next); }, - children: function(next) { + children: function (next) { Categories.getChildren(cids, uid, next); }, - parents: function(next) { + parents: function (next) { Categories.getParents(cids, next); }, - hasRead: function(next) { + hasRead: function (next) { Categories.hasReadCategories(cids, uid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -156,7 +161,7 @@ var privileges = require('./privileges'); var categories = results.categories; var hasRead = results.hasRead; uid = parseInt(uid, 10); - for(var i=0; i<results.categories.length; ++i) { + for(var i = 0; i < results.categories.length; ++i) { if (categories[i]) { categories[i]['unread-class'] = (parseInt(categories[i].topic_count, 10) === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; categories[i].children = results.children[i]; @@ -182,7 +187,7 @@ var privileges = require('./privileges'); return; } - category.children.forEach(function(child) { + category.children.forEach(function (child) { calculateTopicPostCount(child); postCount += parseInt(child.totalPostCount, 10) || 0; topicCount += parseInt(child.totalTopicCount, 10) || 0; @@ -192,7 +197,7 @@ var privileges = require('./privileges'); category.totalTopicCount = topicCount; } - Categories.getParents = function(cids, callback) { + Categories.getParents = function (cids, callback) { var categoriesData; var parentCids; async.waterfall([ @@ -202,20 +207,20 @@ var privileges = require('./privileges'); function (_categoriesData, next) { categoriesData = _categoriesData; - parentCids = categoriesData.filter(function(category) { + parentCids = categoriesData.filter(function (category) { return category && category.hasOwnProperty('parentCid') && parseInt(category.parentCid, 10); - }).map(function(category) { + }).map(function (category) { return parseInt(category.parentCid, 10); }); if (!parentCids.length) { - return callback(null, cids.map(function() {return null;})); + return callback(null, cids.map(function () {return null;})); } Categories.getCategoriesData(parentCids, next); }, function (parentData, next) { - parentData = categoriesData.map(function(category) { + parentData = categoriesData.map(function (category) { return parentData[parentCids.indexOf(parseInt(category.parentCid, 10))]; }); next(null, parentData); @@ -223,15 +228,15 @@ var privileges = require('./privileges'); ], callback); }; - Categories.getChildren = function(cids, uid, callback) { - var categories = cids.map(function(cid) { + Categories.getChildren = function (cids, uid, callback) { + var categories = cids.map(function (cid) { return {cid: cid}; }); - async.each(categories, function(category, next) { + async.each(categories, function (category, next) { getChildrenRecursive(category, uid, next); }, function (err) { - callback(err, categories.map(function(c) { + callback(err, categories.map(function (c) { return c && c.children; })); }); @@ -246,7 +251,7 @@ var privileges = require('./privileges'); privileges.categories.filterCids('find', children, uid, next); }, function (children, next) { - children = children.filter(function(cid) { + children = children.filter(function (cid) { return parseInt(category.cid, 10) !== parseInt(cid, 10); }); if (!children.length) { @@ -258,15 +263,15 @@ var privileges = require('./privileges'); function (childrenData, next) { childrenData = childrenData.filter(Boolean); category.children = childrenData; - async.each(category.children, function(child, next) { + async.each(category.children, function (child, next) { getChildrenRecursive(child, uid, next); }, next); } ], callback); } - Categories.flattenCategories = function(allCategories, categoryData) { - categoryData.forEach(function(category) { + Categories.flattenCategories = function (allCategories, categoryData) { + categoryData.forEach(function (category) { if (!category) { return; } @@ -287,12 +292,12 @@ var privileges = require('./privileges'); * @param categories {array} flat list of categories * @param parentCid {number} start from 0 to build full tree */ - Categories.getTree = function(categories, parentCid) { + Categories.getTree = function (categories, parentCid) { var tree = [], i = 0, len = categories.length, category; for (i; i < len; ++i) { category = categories[i]; - if (!category.hasOwnProperty('parentCid')) { + if (!category.hasOwnProperty('parentCid') || category.parentCid === null) { category.parentCid = 0; } @@ -305,4 +310,56 @@ var privileges = require('./privileges'); return tree; }; + Categories.buildForSelect = function (uid, callback) { + function recursive(category, categoriesData, level) { + if (category.link) { + return; + } + + var bullet = level ? '• ' : ''; + category.value = category.cid; + category.text = level + bullet + category.name + categoriesData.push(category); + + category.children.forEach(function (child) { + recursive(child, categoriesData, ' ' + level); + }); + } + Categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function (err, categories) { + if (err) { + return callback(err); + } + + var categoriesData = []; + + categories = categories.filter(function (category) { + return category && !category.link && !parseInt(category.parentCid, 10); + }); + + categories.forEach(function (category) { + recursive(category, categoriesData, ''); + }); + callback(null, categoriesData); + }); + }; + + Categories.getIgnorers = function (cid, start, stop, callback) { + db.getSortedSetRevRange('cid:' + cid + ':ignorers', start, stop, callback); + }; + + Categories.filterIgnoringUids = function (cid, uids, callback) { + async.waterfall([ + function (next){ + db.sortedSetScores('cid:' + cid + ':ignorers', uids, next); + }, + function (scores, next) { + var readingUids = uids.filter(function (uid, index) { + return uid && !!scores[index]; + }); + next(null, readingUids); + } + ], callback); + }; + + }(exports)); diff --git a/src/categories/activeusers.js b/src/categories/activeusers.js index 4eba4a5525..15673939ae 100644 --- a/src/categories/activeusers.js +++ b/src/categories/activeusers.js @@ -4,9 +4,9 @@ var async = require('async'); var posts = require('../posts'); var db = require('../database'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.getActiveUsers = function(cid, callback) { + Categories.getActiveUsers = function (cid, callback) { async.waterfall([ function (next) { db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 24, next); @@ -15,9 +15,9 @@ module.exports = function(Categories) { posts.getPostsFields(pids, ['uid'], next); }, function (posts, next) { - var uids = posts.map(function(post) { + var uids = posts.map(function (post) { return post.uid; - }).filter(function(uid, index, array) { + }).filter(function (uid, index, array) { return parseInt(uid, 10) && array.indexOf(uid) === index; }); diff --git a/src/categories/create.js b/src/categories/create.js index 7f1f3955f7..508d8ab348 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -3,22 +3,22 @@ var async = require('async'); var db = require('../database'); -var privileges = require('../privileges'); var groups = require('../groups'); var plugins = require('../plugins'); +var privileges = require('../privileges'); var utils = require('../../public/src/utils'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.create = function(data, callback) { + Categories.create = function (data, callback) { var category; var parentCid = data.parentCid ? data.parentCid : 0; async.waterfall([ - function(next) { + function (next) { db.incrObjectField('global', 'nextCid', next); }, - function(cid, next) { + function (cid, next) { data.name = data.name || 'Category ' + cid; var slug = cid + '/' + utils.slugify(data.name); var order = data.order || cid; // If no order provided, place it at the end @@ -28,6 +28,7 @@ module.exports = function(Categories) { cid: cid, name: data.name, description: data.description ? data.description : '', + descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '', icon: data.icon ? data.icon : '', bgColor: data.bgColor || colours[0], color: data.color || colours[1], @@ -45,35 +46,40 @@ module.exports = function(Categories) { plugins.fireHook('filter:category.create', {category: category, data: data}, next); }, - function(data, next) { + function (data, next) { category = data.category; - var defaultPrivileges = ['find', 'read', 'topics:create', 'topics:reply']; + var defaultPrivileges = ['find', 'read', 'topics:read', 'topics:create', 'topics:reply', 'posts:edit', 'posts:delete', 'topics:delete', 'upload:post:image']; async.series([ async.apply(db.setObject, 'category:' + category.cid, category), - async.apply(Categories.parseDescription, category.cid, category.description), + function (next) { + if (category.descriptionParsed) { + return next(); + } + Categories.parseDescription(category.cid, category.description, next); + }, async.apply(db.sortedSetAdd, 'categories:cid', category.order, category.cid), async.apply(db.sortedSetAdd, 'cid:' + parentCid + ':children', category.order, category.cid), async.apply(privileges.categories.give, defaultPrivileges, category.cid, 'administrators'), async.apply(privileges.categories.give, defaultPrivileges, category.cid, 'registered-users'), - async.apply(privileges.categories.give, ['find', 'read'], category.cid, 'guests') + async.apply(privileges.categories.give, ['find', 'read', 'topics:read'], category.cid, 'guests') ], next); }, - function(results, next) { + function (results, next) { if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { - return Categories.copySettingsFrom(data.cloneFromCid, category.cid, next); + return Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid, next); } next(null, category); }, - function(category, next) { + function (category, next) { plugins.fireHook('action:category.create', category); next(null, category); } ], callback); }; - Categories.assignColours = function() { + Categories.assignColours = function () { var backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946']; var text = ['#fff', '#fff', '#333', '#fff', '#333', '#fff', '#fff', '#fff']; var index = Math.floor(Math.random() * backgrounds.length); @@ -81,7 +87,7 @@ module.exports = function(Categories) { return [backgrounds[index], text[index]]; }; - Categories.copySettingsFrom = function(fromCid, toCid, callback) { + Categories.copySettingsFrom = function (fromCid, toCid, copyParent, callback) { var destination; async.waterfall([ function (next) { @@ -97,14 +103,15 @@ module.exports = function(Categories) { destination = results.destination; var tasks = []; - if (parseInt(results.source.parentCid, 10)) { - tasks.push(async.apply(db.sortedSetAdd, 'cid:' + results.source.parentCid + ':children', results.source.order, toCid)); - } - if (destination && parseInt(destination.parentCid, 10)) { + if (copyParent && utils.isNumber(destination.parentCid)) { tasks.push(async.apply(db.sortedSetRemove, 'cid:' + destination.parentCid + ':children', toCid)); } + if (copyParent && utils.isNumber(results.source.parentCid)) { + tasks.push(async.apply(db.sortedSetAdd, 'cid:' + results.source.parentCid + ':children', results.source.order, toCid)); + } + destination.description = results.source.description; destination.descriptionParsed = results.source.descriptionParsed; destination.icon = results.source.icon; @@ -114,7 +121,10 @@ module.exports = function(Categories) { destination.numRecentReplies = results.source.numRecentReplies; destination.class = results.source.class; destination.imageClass = results.source.imageClass; - destination.parentCid = results.source.parentCid || 0; + + if (copyParent) { + destination.parentCid = results.source.parentCid || 0; + } tasks.push(async.apply(db.setObject, 'category:' + toCid, destination)); @@ -123,18 +133,13 @@ module.exports = function(Categories) { function (results, next) { Categories.copyPrivilegesFrom(fromCid, toCid, next); } - ], function(err) { + ], function (err) { callback(err, destination); }); }; - Categories.copyPrivilegesFrom = function(fromCid, toCid, callback) { - var privilegeList = [ - 'find', 'read', 'topics:create', 'topics:reply', 'purge', 'mods', - 'groups:find', 'groups:read', 'groups:topics:create', 'groups:topics:reply', 'groups:purge', 'groups:moderate' - ]; - - async.each(privilegeList, function(privilege, next) { + Categories.copyPrivilegesFrom = function (fromCid, toCid, callback) { + async.each(privileges.privilegeList, function (privilege, next) { copyPrivilege(privilege, fromCid, toCid, next); }, callback); }; @@ -145,7 +150,7 @@ module.exports = function(Categories) { db.getSortedSetRange('group:cid:' + toCid + ':privileges:' + privilege + ':members', 0, -1, next); }, function (currentMembers, next) { - async.eachSeries(currentMembers, function(member, next) { + async.eachSeries(currentMembers, function (member, next) { groups.leave('cid:' + toCid + ':privileges:' + privilege, member, next); }, next); }, @@ -157,7 +162,7 @@ module.exports = function(Categories) { return callback(); } - async.eachSeries(members, function(member, next) { + async.eachSeries(members, function (member, next) { groups.join('cid:' + toCid + ':privileges:' + privilege, member, next); }, next); } diff --git a/src/categories/data.js b/src/categories/data.js index fb166a47af..ccc31343d9 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -6,10 +6,10 @@ var winston = require('winston'); var db = require('../database'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.getCategoryData = function(cid, callback) { - db.getObject('category:' + cid, function(err, category) { + Categories.getCategoryData = function (cid, callback) { + db.getObject('category:' + cid, function (err, category) { if (err) { return callback(err); } @@ -19,15 +19,15 @@ module.exports = function(Categories) { }); }; - Categories.getCategoriesData = function(cids, callback) { + Categories.getCategoriesData = function (cids, callback) { if (!Array.isArray(cids) || !cids.length) { return callback(null, []); } - var keys = cids.map(function(cid) { + var keys = cids.map(function (cid) { return 'category:' + cid; }); - db.getObjects(keys, function(err, categories) { + db.getObjects(keys, function (err, categories) { if (err || !Array.isArray(categories) || !categories.length) { return callback(err, []); } @@ -42,7 +42,7 @@ module.exports = function(Categories) { return; } - category.name = validator.escape(category.name || ''); + category.name = validator.escape(String(category.name || '')); category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined; category.icon = category.icon || 'hidden'; if (category.hasOwnProperty('post_count')) { @@ -58,25 +58,25 @@ module.exports = function(Categories) { } if (category.description) { - category.description = validator.escape(category.description); + category.description = validator.escape(String(category.description)); category.descriptionParsed = category.descriptionParsed || category.description; } } - Categories.getCategoryField = function(cid, field, callback) { + Categories.getCategoryField = function (cid, field, callback) { db.getObjectField('category:' + cid, field, callback); }; - Categories.getCategoriesFields = function(cids, fields, callback) { + Categories.getCategoriesFields = function (cids, fields, callback) { if (!Array.isArray(cids) || !cids.length) { return callback(null, []); } - var keys = cids.map(function(cid) { + var keys = cids.map(function (cid) { return 'category:' + cid; }); - db.getObjectsFields(keys, fields, function(err, categories) { + db.getObjectsFields(keys, fields, function (err, categories) { if (err) { return callback(err); } @@ -86,29 +86,29 @@ module.exports = function(Categories) { }); }; - Categories.getMultipleCategoryFields = function(cids, fields, callback) { + Categories.getMultipleCategoryFields = function (cids, fields, callback) { winston.warn('[deprecated] Categories.getMultipleCategoryFields is deprecated please use Categories.getCategoriesFields'); Categories.getCategoriesFields(cids, fields, callback); }; - Categories.getAllCategoryFields = function(fields, callback) { + Categories.getAllCategoryFields = function (fields, callback) { async.waterfall([ async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), - function(cids, next) { + function (cids, next) { Categories.getCategoriesFields(cids, fields, next); } ], callback); }; - Categories.getCategoryFields = function(cid, fields, callback) { + Categories.getCategoryFields = function (cid, fields, callback) { db.getObjectFields('category:' + cid, fields, callback); }; - Categories.setCategoryField = function(cid, field, value, callback) { + Categories.setCategoryField = function (cid, field, value, callback) { db.setObjectField('category:' + cid, field, value, callback); }; - Categories.incrementCategoryFieldBy = function(cid, field, value, callback) { + Categories.incrementCategoryFieldBy = function (cid, field, value, callback) { db.incrObjectFieldBy('category:' + cid, field, value, callback); }; diff --git a/src/categories/delete.js b/src/categories/delete.js index 8d99bb60d1..5c8e5bdf9a 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -1,19 +1,21 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - batch = require('../batch'), - plugins = require('../plugins'), - topics = require('../topics'); +var async = require('async'); +var db = require('../database'); +var batch = require('../batch'); +var plugins = require('../plugins'); +var topics = require('../topics'); +var groups = require('../groups'); +var privileges = require('../privileges'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.purge = function(cid, uid, callback) { - batch.processSortedSet('cid:' + cid + ':tids', function(tids, next) { - async.eachLimit(tids, 10, function(tid, next) { + Categories.purge = function (cid, uid, callback) { + batch.processSortedSet('cid:' + cid + ':tids', function (tids, next) { + async.eachLimit(tids, 10, function (tid, next) { topics.purgePostsAndTopic(tid, uid, next); }, next); - }, {alwaysStartAt: 0}, function(err) { + }, {alwaysStartAt: 0}, function (err) { if (err) { return callback(err); } @@ -26,50 +28,56 @@ module.exports = function(Categories) { function purgeCategory(cid, callback) { async.series([ - function(next) { + function (next) { db.sortedSetRemove('categories:cid', cid, next); }, - function(next) { + function (next) { removeFromParent(cid, next); }, - function(next) { + function (next) { db.deleteAll([ 'cid:' + cid + ':tids', 'cid:' + cid + ':tids:posts', 'cid:' + cid + ':pids', 'cid:' + cid + ':read_by_uid', + 'cid:' + cid + ':ignorers', 'cid:' + cid + ':children', 'category:' + cid ], next); + }, + function (next) { + async.each(privileges.privilegeList, function (privilege, next) { + groups.destroy('cid:' + cid + ':privileges:' + privilege, next); + }, next); } ], callback); } function removeFromParent(cid, callback) { async.waterfall([ - function(next) { + function (next) { async.parallel({ - parentCid: function(next) { + parentCid: function (next) { Categories.getCategoryField(cid, 'parentCid', next); }, - children: function(next) { + children: function (next) { db.getSortedSetRange('cid:' + cid + ':children', 0, -1, next); } }, next); }, - function(results, next) { + function (results, next) { async.parallel([ - function(next) { + function (next) { results.parentCid = parseInt(results.parentCid, 10) || 0; db.sortedSetRemove('cid:' + results.parentCid + ':children', cid, next); }, - function(next) { - async.each(results.children, function(cid, next) { + function (next) { + async.each(results.children, function (cid, next) { async.parallel([ - function(next) { + function (next) { db.setObjectField('category:' + cid, 'parentCid', 0, next); }, - function(next) { + function (next) { db.sortedSetAdd('cid:0:children', cid, cid, next); } ], next); @@ -77,7 +85,7 @@ module.exports = function(Categories) { } ], next); } - ], function(err, results) { + ], function (err) { callback(err); }); } diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 7d42f053ff..831991d61a 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -1,56 +1,95 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - validator = require('validator'), - _ = require('underscore'), - - db = require('../database'), - posts = require('../posts'), - topics = require('../topics'), - privileges = require('../privileges'); - -module.exports = function(Categories) { - Categories.getRecentReplies = function(cid, uid, count, callback) { +var async = require('async'); +var winston = require('winston'); +var validator = require('validator'); +var _ = require('underscore'); + +var db = require('../database'); +var posts = require('../posts'); +var topics = require('../topics'); +var privileges = require('../privileges'); +var batch = require('../batch'); + + +module.exports = function (Categories) { + + Categories.getRecentReplies = function (cid, uid, count, callback) { if (!parseInt(count, 10)) { return callback(null, []); } async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('cid:' + cid + ':pids', 0, count - 1, next); }, - function(pids, next) { + function (pids, next) { privileges.posts.filter('read', pids, uid, next); }, - function(pids, next) { + function (pids, next) { posts.getPostSummaryByPids(pids, uid, {stripTags: true}, next); } ], callback); }; - Categories.getRecentTopicReplies = function(categoryData, uid, callback) { + Categories.updateRecentTid = function (cid, tid, callback) { + async.parallel({ + count: function (next) { + db.sortedSetCard('cid:' + cid + ':recent_tids', next); + }, + numRecentReplies: function (next) { + db.getObjectField('category:' + cid, 'numRecentReplies', next); + } + }, function (err, results) { + if (err) { + return callback(err); + } + + if (results.count < results.numRecentReplies) { + return db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, callback); + } + async.waterfall([ + function (next) { + db.getSortedSetRangeWithScores('cid:' + cid + ':recent_tids', 0, results.count - results.numRecentReplies, next); + }, + function (data, next) { + if (!data.length) { + return next(); + } + db.sortedSetsRemoveRangeByScore(['cid:' + cid + ':recent_tids'], '-inf', data[data.length - 1].score, next); + }, + function (next) { + db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, next); + } + ], callback); + }); + }; + + Categories.getRecentTopicReplies = function (categoryData, uid, callback) { if (!Array.isArray(categoryData) || !categoryData.length) { return callback(); } async.waterfall([ - function(next) { - async.map(categoryData, getRecentTopicTids, next); + function (next) { + var keys = categoryData.map(function (category) { + return 'cid:' + category.cid + ':recent_tids'; + }); + db.getSortedSetsMembers(keys, next); }, - function(results, next) { + function (results, next) { var tids = _.flatten(results); - tids = tids.filter(function(tid, index, array) { + tids = tids.filter(function (tid, index, array) { return !!tid && array.indexOf(tid) === index; }); privileges.topics.filterTids('read', tids, uid, next); }, - function(tids, next) { + function (tids, next) { getTopics(tids, next); }, - function(topics, next) { + function (topics, next) { assignTopicsToCategories(categoryData, topics); bubbleUpChildrenPosts(categoryData); @@ -60,45 +99,6 @@ module.exports = function(Categories) { ], callback); }; - function getRecentTopicTids(category, callback) { - var count = parseInt(category.numRecentReplies, 10); - if (!count) { - return callback(null, []); - } - - if (count === 1) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange('cid:' + category.cid + ':pids', 0, 0, next); - }, - function (pid, next) { - posts.getPostField(pid, 'tid', next); - }, - function (tid, next) { - next(null, [tid]); - } - ], callback); - return; - } - - async.parallel({ - pinnedTids: function(next) { - db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, -1, '+inf', Date.now(), next); - }, - tids: function(next) { - db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, Math.max(1, count), Date.now(), '-inf', next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - - results.tids = results.tids.concat(results.pinnedTids); - - callback(null, results.tids); - }); - } - function getTopics(tids, callback) { var topicData; async.waterfall([ @@ -107,48 +107,64 @@ module.exports = function(Categories) { }, function (_topicData, next) { topicData = _topicData; - topicData.forEach(function(topic) { - topic.teaserPid = topic.teaserPid || topic.mainPid; + topicData.forEach(function (topic) { + if (topic) { + topic.teaserPid = topic.teaserPid || topic.mainPid; + } + }); + var cids = _topicData.map(function (topic) { + return topic && topic.cid; + }).filter(function (cid, index, array) { + return cid && array.indexOf(cid) === index; }); - topics.getTeasers(topicData, next); + async.parallel({ + categoryData: async.apply(Categories.getCategoriesFields, cids, ['cid', 'parentCid']), + teasers: async.apply(topics.getTeasers, _topicData), + }, next); }, - function (teasers, next) { - teasers.forEach(function(teaser, index) { + function (results, next) { + var parentCids = {}; + results.categoryData.forEach(function (category) { + parentCids[category.cid] = category.parentCid; + }); + results.teasers.forEach(function (teaser, index) { if (teaser) { teaser.cid = topicData[index].cid; + teaser.parentCid = parseInt(parentCids[teaser.cid]) || 0; teaser.tid = teaser.uid = teaser.user.uid = undefined; teaser.topic = { slug: topicData[index].slug, - title: validator.escape(topicData[index].title) + title: validator.escape(String(topicData[index].title)) }; } }); - teasers = teasers.filter(Boolean); - next(null, teasers); + results.teasers = results.teasers.filter(Boolean); + next(null, results.teasers); } ], callback); } function assignTopicsToCategories(categories, topics) { - categories.forEach(function(category) { - category.posts = topics.filter(function(topic) { - return topic.cid && parseInt(topic.cid, 10) === parseInt(category.cid, 10); - }).sort(function(a, b) { + categories.forEach(function (category) { + category.posts = topics.filter(function (topic) { + return topic.cid && (parseInt(topic.cid, 10) === parseInt(category.cid, 10) || + parseInt(topic.parentCid, 10) === parseInt(category.cid, 10)); + }).sort(function (a, b) { return b.pid - a.pid; }).slice(0, parseInt(category.numRecentReplies, 10)); }); } function bubbleUpChildrenPosts(categoryData) { - categoryData.forEach(function(category) { + categoryData.forEach(function (category) { if (category.posts.length) { return; } var posts = []; getPostsRecursive(category, posts); - posts.sort(function(a, b) { + posts.sort(function (a, b) { return b.pid - a.pid; }); if (posts.length) { @@ -158,18 +174,19 @@ module.exports = function(Categories) { } function getPostsRecursive(category, posts) { - category.posts.forEach(function(p) { + category.posts.forEach(function (p) { posts.push(p); }); - category.children.forEach(function(child) { + category.children.forEach(function (child) { getPostsRecursive(child, posts); }); } - Categories.moveRecentReplies = function(tid, oldCid, cid) { + Categories.moveRecentReplies = function (tid, oldCid, cid, callback) { + callback = callback || function () {}; updatePostCount(tid, oldCid, cid); - topics.getPids(tid, function(err, pids) { + topics.getPids(tid, function (err, pids) { if (err) { return winston.error(err.message); } @@ -178,53 +195,37 @@ module.exports = function(Categories) { return; } - var start = 0, - done = false, - batch = 50; - - async.whilst(function() { - return !done; - }, function(next) { - var movePids = pids.slice(start, start + batch); - if (!movePids.length) { - done = true; - return next(); - } - - posts.getPostsFields(movePids, ['timestamp'], function(err, postData) { - if (err) { - return next(err); + batch.processArray(pids, function (pids, next) { + async.waterfall([ + function (next) { + posts.getPostsFields(pids, ['timestamp'], next); + }, + function (postData, next) { + var timestamps = postData.map(function (post) { + return post && post.timestamp; + }); + + async.parallel([ + function (next) { + db.sortedSetRemove('cid:' + oldCid + ':pids', pids, next); + }, + function (next) { + db.sortedSetAdd('cid:' + cid + ':pids', timestamps, pids, next); + } + ], next); } - - var timestamps = postData.map(function(post) { - return post && post.timestamp; - }); - - async.parallel([ - function(next) { - db.sortedSetRemove('cid:' + oldCid + ':pids', movePids, next); - }, - function(next) { - db.sortedSetAdd('cid:' + cid + ':pids', timestamps, movePids, next); - } - ], function(err) { - if (err) { - return next(err); - } - start += batch; - next(); - }); - }); - }, function(err) { + ], next); + }, function (err) { if (err) { winston.error(err.stack); } + callback(err); }); }); }; function updatePostCount(tid, oldCid, newCid) { - topics.getTopicField(tid, 'postcount', function(err, postCount) { + topics.getTopicField(tid, 'postcount', function (err, postCount) { if (err) { return winston.error(err.message); } @@ -232,13 +233,13 @@ module.exports = function(Categories) { return; } async.parallel([ - function(next) { + function (next) { db.incrObjectFieldBy('category:' + oldCid, 'post_count', -postCount, next); }, - function(next) { + function (next) { db.incrObjectFieldBy('category:' + newCid, 'post_count', postCount, next); } - ], function(err) { + ], function (err) { if (err) { winston.error(err.message); } diff --git a/src/categories/topics.js b/src/categories/topics.js index 8ee7105256..b9ab96bee2 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -6,9 +6,9 @@ var db = require('../database'); var topics = require('../topics'); var plugins = require('../plugins'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.getCategoryTopics = function(data, callback) { + Categories.getCategoryTopics = function (data, callback) { async.waterfall([ function (next) { plugins.fireHook('filter:category.topics.prepare', data, next); @@ -24,7 +24,7 @@ module.exports = function(Categories) { return next(null, {topics: [], uid: data.uid}); } - for (var i=0; i<topics.length; ++i) { + for (var i = 0; i < topics.length; ++i) { topics[i].index = data.start + i; } @@ -36,12 +36,12 @@ module.exports = function(Categories) { ], callback); }; - Categories.modifyTopicsByPrivilege = function(topics, privileges) { + Categories.modifyTopicsByPrivilege = function (topics, privileges) { if (!Array.isArray(topics) || !topics.length || privileges.isAdminOrMod) { return; } - topics.forEach(function(topic) { + topics.forEach(function (topic) { if (topic.deleted && !topic.isOwner) { topic.title = '[[topic:topic_is_deleted]]'; topic.slug = topic.tid; @@ -52,17 +52,17 @@ module.exports = function(Categories) { }); }; - Categories.getTopicIds = function(set, reverse, start, stop, callback) { - if (reverse) { - db.getSortedSetRevRange(set, start, stop, callback); + Categories.getTopicIds = function (set, reverse, start, stop, callback) { + if (Array.isArray(set)) { + db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({sets: set, start: start, stop: stop}, callback); } else { - db.getSortedSetRange(set, start, stop, callback); + db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback); } }; - Categories.getTopicIndex = function(tid, callback) { - topics.getTopicField(tid, 'cid', function(err, cid) { - if(err) { + Categories.getTopicIndex = function (tid, callback) { + topics.getTopicField(tid, 'cid', function (err, cid) { + if (err) { return callback(err); } @@ -70,26 +70,29 @@ module.exports = function(Categories) { }); }; - Categories.onNewPostMade = function(cid, pinned, postData, callback) { + Categories.onNewPostMade = function (cid, pinned, postData, callback) { if (!cid || !postData) { return callback(); } async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, postData.pid, next); }, - function(next) { + function (next) { db.incrObjectField('category:' + cid, 'post_count', next); }, - function(next) { + function (next) { if (parseInt(pinned, 10) === 1) { next(); } else { db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next); } }, - function(next) { + function (next){ + Categories.updateRecentTid(cid, postData.tid, next); + }, + function (next) { db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next); } ], callback); diff --git a/src/categories/unread.js b/src/categories/unread.js index d62aaa3c6e..3805e1e78f 100644 --- a/src/categories/unread.js +++ b/src/categories/unread.js @@ -1,26 +1,26 @@ "use strict"; -var async = require('async'), - db = require('../database'); +var async = require('async'); +var db = require('../database'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.markAsRead = function(cids, uid, callback) { - callback = callback || function() {}; + Categories.markAsRead = function (cids, uid, callback) { + callback = callback || function () {}; if (!Array.isArray(cids) || !cids.length) { return callback(); } - var keys = cids.map(function(cid) { + var keys = cids.map(function (cid) { return 'cid:' + cid + ':read_by_uid'; }); - db.isMemberOfSets(keys, uid, function(err, hasRead) { + db.isMemberOfSets(keys, uid, function (err, hasRead) { if (err) { return callback(err); } - keys = keys.filter(function(key, index) { + keys = keys.filter(function (key, index) { return !hasRead[index]; }); @@ -32,12 +32,12 @@ module.exports = function(Categories) { }); }; - Categories.markAsUnreadForAll = function(cid, callback) { - callback = callback || function() {}; + Categories.markAsUnreadForAll = function (cid, callback) { + callback = callback || function () {}; db.delete('cid:' + cid + ':read_by_uid', callback); }; - Categories.hasReadCategories = function(cids, uid, callback) { + Categories.hasReadCategories = function (cids, uid, callback) { var sets = []; for (var i = 0, ii = cids.length; i < ii; i++) { @@ -47,7 +47,7 @@ module.exports = function(Categories) { db.isMemberOfSets(sets, uid, callback); }; - Categories.hasReadCategory = function(cid, uid, callback) { + Categories.hasReadCategory = function (cid, uid, callback) { db.isSetMember('cid:' + cid + ':read_by_uid', uid, callback); }; diff --git a/src/categories/update.js b/src/categories/update.js index 78e97e4076..4f4229b5fc 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -1,39 +1,39 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - utils = require('../../public/src/utils'), - translator = require('../../public/src/modules/translator'), - plugins = require('../plugins'); +var async = require('async'); +var db = require('../database'); +var utils = require('../../public/src/utils'); +var translator = require('../../public/src/modules/translator'); +var plugins = require('../plugins'); -module.exports = function(Categories) { +module.exports = function (Categories) { - Categories.update = function(modified, callback) { + Categories.update = function (modified, callback) { var cids = Object.keys(modified); - async.each(cids, function(cid, next) { + async.each(cids, function (cid, next) { updateCategory(cid, modified[cid], next); - }, function(err) { + }, function (err) { callback(err, cids); }); }; function updateCategory(cid, modifiedFields, callback) { - Categories.exists(cid, function(err, exists) { + Categories.exists(cid, function (err, exists) { if (err || !exists) { return callback(err); } if (modifiedFields.hasOwnProperty('name')) { - translator.translate(modifiedFields.name, function(translated) { + translator.translate(modifiedFields.name, function (translated) { modifiedFields.slug = cid + '/' + utils.slugify(translated); }); } - plugins.fireHook('filter:category.update', {category: modifiedFields}, function(err, categoryData) { + plugins.fireHook('filter:category.update', {category: modifiedFields}, function (err, categoryData) { if (err) { return callback(err); } @@ -46,9 +46,9 @@ module.exports = function(Categories) { fields.splice(0, 0, fields.splice(parentCidIndex, 1)[0]); } - async.eachSeries(fields, function(key, next) { + async.eachSeries(fields, function (key, next) { updateCategoryField(cid, key, category[key], next); - }, function(err) { + }, function (err) { if (err) { return callback(err); } @@ -64,7 +64,7 @@ module.exports = function(Categories) { return updateParent(cid, value, callback); } - db.setObjectField('category:' + cid, key, value, function(err) { + db.setObjectField('category:' + cid, key, value, function (err) { if (err) { return callback(err); } @@ -83,7 +83,7 @@ module.exports = function(Categories) { if (parseInt(cid, 10) === parseInt(newParent, 10)) { return callback(new Error('[[error:cant-set-self-as-parent]]')); } - Categories.getCategoryField(cid, 'parentCid', function(err, oldParent) { + Categories.getCategoryField(cid, 'parentCid', function (err, oldParent) { if (err) { return callback(err); } @@ -100,14 +100,14 @@ module.exports = function(Categories) { function (next) { db.setObjectField('category:' + cid, 'parentCid', newParent, next); } - ], function(err) { + ], function (err) { callback(err); }); }); } function updateOrder(cid, order, callback) { - Categories.getCategoryField(cid, 'parentCid', function(err, parentCid) { + Categories.getCategoryField(cid, 'parentCid', function (err, parentCid) { if (err) { return callback(err); } @@ -124,8 +124,8 @@ module.exports = function(Categories) { }); } - Categories.parseDescription = function(cid, description, callback) { - plugins.fireHook('filter:parse.raw', description, function(err, parsedDescription) { + Categories.parseDescription = function (cid, description, callback) { + plugins.fireHook('filter:parse.raw', description, function (err, parsedDescription) { if (err) { return callback(err); } diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js index 789393703a..2c99a981c3 100644 --- a/src/controllers/accounts.js +++ b/src/controllers/accounts.js @@ -3,6 +3,7 @@ var accountsController = { profile: require('./accounts/profile'), edit: require('./accounts/edit'), + info: require('./accounts/info'), settings: require('./accounts/settings'), groups: require('./accounts/groups'), follow: require('./accounts/follow'), diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index 4af94c5521..2175708581 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -4,74 +4,115 @@ var async = require('async'); var messaging = require('../../messaging'); var meta = require('../../meta'); +var user = require('../../user'); var helpers = require('../helpers'); - var chatsController = {}; -chatsController.get = function(req, res, callback) { +chatsController.get = function (req, res, callback) { if (parseInt(meta.config.disableChat, 10) === 1) { return callback(); } + var uid; + var username; + var recentChats; - messaging.getRecentChats(req.uid, 0, 19, function(err, recentChats) { + async.waterfall([ + function (next) { + async.parallel({ + uid: async.apply(user.getUidByUserslug, req.params.userslug), + username: async.apply(user.getUsernameByUserslug, req.params.userslug) + }, next); + }, + function (results, next) { + uid = results.uid; + username = results.username; + if (!uid) { + return callback(); + } + messaging.getRecentChats(req.uid, uid, 0, 19, next); + }, + function (_recentChats, next) { + recentChats = _recentChats; + if (!recentChats) { + return callback(); + } + if (!req.params.roomid) { + return res.render('chats', { + rooms: recentChats.rooms, + uid: uid, + userslug: req.params.userslug, + nextStart: recentChats.nextStart, + allowed: true, + title: '[[pages:chats]]', + breadcrumbs: helpers.buildBreadcrumbs([{text: username, url: '/user/' + req.params.userslug}, {text: '[[pages:chats]]'}]) + }); + } + messaging.isUserInRoom(req.uid, req.params.roomid, next); + }, + function (inRoom, next) { + if (!inRoom) { + return callback(); + } + async.parallel({ + users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1), + messages: async.apply(messaging.getMessages, { + callerUid: req.uid, + uid: uid, + roomId: req.params.roomid, + isNew: false + }), + room: async.apply(messaging.getRoomData, req.params.roomid) + }, next); + } + ], function (err, data) { if (err) { return callback(err); } + var room = data.room; + room.messages = data.messages; - if (!req.params.roomid) { - return res.render('chats', { - rooms: recentChats.rooms, - nextStart: recentChats.nextStart, - allowed: true, - title: '[[pages:chats]]', - breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}]) - }); - } + room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10); + room.users = data.users.filter(function (user) { + return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; + }); - async.waterfall([ - function (next) { - messaging.isUserInRoom(req.uid, req.params.roomid, next); - }, - function (inRoom, next) { - if (!inRoom) { - return callback(); - } + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.rooms = recentChats.rooms; + room.uid = uid; + room.userslug = req.params.userslug; + room.nextStart = recentChats.nextStart; + room.title = room.roomName; + room.breadcrumbs = helpers.buildBreadcrumbs([ + {text: username, url: '/user/' + req.params.userslug}, + {text: '[[pages:chats]]', url: '/user/' + req.params.userslug + '/chats'}, + {text: room.roomName} + ]); + room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; + room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; + room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; - async.parallel({ - users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1), - messages: async.apply(messaging.getMessages, { - uid: req.uid, - roomId: req.params.roomid, - since: 'recent', - isNew: false - }), - room: async.apply(messaging.getRoomData, req.params.roomid) - }, next); - } - ], function(err, data) { - if (err) { - return callback(err); - } - var room = data.room; - room.messages = data.messages; - - room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10); - room.users = data.users.filter(function(user) { - return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; - }); + res.render('chats', room); + }); +}; - room.rooms = recentChats.rooms; - room.nextStart = recentChats.nextStart; - room.title = room.roomName; - room.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: room.roomName}]); - room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; - room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; - room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; +chatsController.redirectToChat = function (req, res, next) { + var roomid = parseInt(req.params.roomid, 10); + if (!req.uid) { + return next(); + } + user.getUserField(req.uid, 'userslug', function (err, userslug) { + if (err || !userslug) { + return next(err); + } - res.render('chats', room); - }); + if (!roomid) { + return helpers.redirect(res, '/user/' + userslug + '/chats'); + } + helpers.redirect(res, '/user/' + userslug + '/chats/' + roomid); }); }; + + module.exports = chatsController; \ No newline at end of file diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 09e824ecd9..3fe2c57092 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -10,12 +10,13 @@ var user = require('../../user'); var meta = require('../../meta'); var plugins = require('../../plugins'); var helpers = require('../helpers'); +var groups = require('../../groups'); var accountHelpers = require('./helpers'); var editController = {}; -editController.get = function(req, res, callback) { - accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function(err, userData) { +editController.get = function (req, res, callback) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function (err, userData) { if (err || !userData) { return callback(err); } @@ -25,15 +26,21 @@ editController.get = function(req, res, callback) { userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10); userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads) === 1; userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1; - + + userData.groups = userData.groups.filter(function (group) { + return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; + }); + userData.groups.forEach(function (group) { + group.selected = group.name === userData.groupTitle; + }); userData.title = '[[pages:account/edit, ' + userData.username + ']]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]); userData.editButtons = []; - plugins.fireHook('filter:user.account.edit', userData, function(err, userData) { + plugins.fireHook('filter:user.account.edit', userData, function (err, userData) { if (err) { - return next(err); + return callback(err); } res.render('account/edit', userData); @@ -41,20 +48,20 @@ editController.get = function(req, res, callback) { }); }; -editController.password = function(req, res, next) { +editController.password = function (req, res, next) { renderRoute('password', req, res, next); }; -editController.username = function(req, res, next) { +editController.username = function (req, res, next) { renderRoute('username', req, res, next); }; -editController.email = function(req, res, next) { +editController.email = function (req, res, next) { renderRoute('email', req, res, next); }; function renderRoute(name, req, res, next) { - getUserData(req, next, function(err, userData) { + getUserData(req, next, function (err, userData) { if (err) { return next(err); } @@ -80,17 +87,17 @@ function renderRoute(name, req, res, next) { function getUserData(req, next, callback) { var userData; async.waterfall([ - function(next) { + function (next) { accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(data, next) { + function (data, next) { userData = data; if (!userData) { return next(); } db.getObjectField('user:' + userData.uid, 'password', next); } - ], function(err, password) { + ], function (err, password) { if (err) { return callback(err); } @@ -106,10 +113,10 @@ editController.uploadPicture = function (req, res, next) { var updateUid; async.waterfall([ - function(next) { + function (next) { user.getUidByUserslug(req.params.userslug, next); }, - function(uid, next) { + function (uid, next) { updateUid = uid; if (parseInt(req.uid, 10) === parseInt(uid, 10)) { return next(null, true); @@ -117,16 +124,18 @@ editController.uploadPicture = function (req, res, next) { user.isAdminOrGlobalMod(req.uid, next); }, - function(isAllowed, next) { + function (isAllowed, next) { if (!isAllowed) { return helpers.notAllowed(req, res); } - + user.uploadPicture(updateUid, userPhoto, next); } - ], function(err, image) { - fs.unlink(userPhoto.path, function(err) { - winston.error('unable to delete picture ' + userPhoto.path, err); + ], function (err, image) { + fs.unlink(userPhoto.path, function (err) { + if (err) { + winston.warn('[user/picture] Unable to delete picture ' + userPhoto.path, err); + } }); if (err) { return next(err); @@ -136,13 +145,13 @@ editController.uploadPicture = function (req, res, next) { }); }; -editController.uploadCoverPicture = function(req, res, next) { +editController.uploadCoverPicture = function (req, res, next) { var params = JSON.parse(req.body.params); user.updateCoverPicture({ file: req.files.files[0], uid: params.uid - }, function(err, image) { + }, function (err, image) { if (err) { return next(err); } diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js index f9dc72c6f3..7d1e91b5e2 100644 --- a/src/controllers/accounts/follow.js +++ b/src/controllers/accounts/follow.js @@ -1,44 +1,52 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - user = require('../../user'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var user = require('../../user'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); +var pagination = require('../../pagination'); var followController = {}; -followController.getFollowing = function(req, res, next) { +followController.getFollowing = function (req, res, next) { getFollow('account/following', 'following', req, res, next); }; -followController.getFollowers = function(req, res, next) { +followController.getFollowers = function (req, res, next) { getFollow('account/followers', 'followers', req, res, next); }; function getFollow(tpl, name, req, res, callback) { var userData; + var page = parseInt(req.query.page, 10) || 1; + var resultsPerPage = 50; + var start = Math.max(0, page - 1) * resultsPerPage; + var stop = start + resultsPerPage - 1; + async.waterfall([ - function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(data, next) { + function (data, next) { userData = data; if (!userData) { return callback(); } var method = name === 'following' ? 'getFollowing' : 'getFollowers'; - user[method](userData.uid, 0, 49, next); + user[method](userData.uid, start, stop, next); } - ], function(err, users) { + ], function (err, users) { if (err) { return callback(err); } userData.users = users; - userData.nextStart = 50; userData.title = '[[pages:' + tpl + ', ' + userData.username + ']]'; + var count = name === 'following' ? userData.followingCount : userData.followerCount; + var pageCount = Math.ceil(count / resultsPerPage); + userData.pagination = pagination.create(page, pageCount); userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:' + name + ']]'}]); res.render(tpl, userData); diff --git a/src/controllers/accounts/groups.js b/src/controllers/accounts/groups.js index e19034c908..038d63de1d 100644 --- a/src/controllers/accounts/groups.js +++ b/src/controllers/accounts/groups.js @@ -1,21 +1,21 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - groups = require('../../groups'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var groups = require('../../groups'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); var groupsController = {}; -groupsController.get = function(req, res, callback) { +groupsController.get = function (req, res, callback) { var userData; var groupsData; async.waterfall([ function (next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, function (_userData, next) { userData = _userData; @@ -27,19 +27,19 @@ groupsController.get = function(req, res, callback) { }, function (_groupsData, next) { groupsData = _groupsData[0]; - var groupNames = groupsData.filter(Boolean).map(function(group) { + var groupNames = groupsData.filter(Boolean).map(function (group) { return group.name; }); groups.getMemberUsers(groupNames, 0, 3, next); }, function (members, next) { - groupsData.forEach(function(group, index) { + groupsData.forEach(function (group, index) { group.members = members[index]; }); next(); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 8acbdc08c8..1046529442 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -3,16 +3,17 @@ var async = require('async'); var validator = require('validator'); +var winston = require('winston'); var user = require('../../user'); var groups = require('../../groups'); -var plugins =require('../../plugins'); +var plugins = require('../../plugins'); var meta = require('../../meta'); var utils = require('../../../public/src/utils'); var helpers = {}; -helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { +helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { async.waterfall([ function (next) { user.getUidByUserslug(userslug, next); @@ -23,28 +24,34 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { } async.parallel({ - userData : function(next) { + userData : function (next) { user.getUserData(uid, next); }, - userSettings : function(next) { + userSettings : function (next) { user.getSettings(uid, next); }, - isAdmin : function(next) { + isAdmin : function (next) { user.isAdministrator(callerUID, next); }, - isGlobalModerator: function(next) { + isGlobalModerator: function (next) { user.isGlobalModerator(callerUID, next); }, - ips: function(next) { + isFollowing: function (next) { + user.isFollowing(callerUID, uid, next); + }, + ips: function (next) { user.getIPs(uid, 4, next); }, - profile_links: function(next) { + profile_links: function (next) { plugins.fireHook('filter:user.profileLinks', [], next); }, - groups: function(next) { + profile_menu: function (next) { + plugins.fireHook('filter:user.profileMenu', {uid: uid, callerUID: callerUID, links: []}, next); + }, + groups: function (next) { groups.getUserGroups([uid], next); }, - sso: function(next) { + sso: function (next) { plugins.fireHook('filter:auth.list', {uid: uid, associations: []}, next); } }, next); @@ -80,35 +87,44 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { userData.ips = results.ips; } + if (!isAdmin && !isGlobalModerator) { + userData.moderationNote = undefined; + } + userData.uid = userData.uid; userData.yourid = callerUID; userData.theirid = userData.uid; userData.isAdmin = isAdmin; userData.isGlobalModerator = isGlobalModerator; + userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator; userData.canBan = isAdmin || isGlobalModerator; userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1); userData.isSelf = isSelf; + userData.isFollowing = results.isFollowing; userData.showHidden = isSelf || isAdmin || isGlobalModerator; userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : []; userData.disableSignatures = meta.config.disableSignatures !== undefined && parseInt(meta.config.disableSignatures, 10) === 1; userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10); - userData.profile_links = filterLinks(results.profile_links, isSelf); + userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf); + userData.sso = results.sso.associations; userData.status = user.getStatus(userData); userData.banned = parseInt(userData.banned, 10) === 1; - userData.website = validator.escape(userData.website || ''); + userData.website = validator.escape(String(userData.website || '')); userData.websiteLink = !userData.website.startsWith('http') ? 'http://' + userData.website : userData.website; userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), ''); userData.followingCount = parseInt(userData.followingCount, 10) || 0; userData.followerCount = parseInt(userData.followerCount, 10) || 0; - userData.email = validator.escape(userData.email || ''); - userData.fullname = validator.escape(userData.fullname || ''); - userData.location = validator.escape(userData.location || ''); - userData.signature = validator.escape(userData.signature || ''); - userData.aboutme = validator.escape(userData.aboutme || ''); + userData.email = validator.escape(String(userData.email || '')); + userData.fullname = validator.escape(String(userData.fullname || '')); + userData.location = validator.escape(String(userData.location || '')); + userData.signature = validator.escape(String(userData.signature || '')); + userData.aboutme = validator.escape(String(userData.aboutme || '')); + userData.birthday = validator.escape(String(userData.birthday || '')); + userData.moderationNote = validator.escape(String(userData.moderationNote || '')); userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid); userData['cover:position'] = userData['cover:position'] || '50% 50%'; @@ -121,55 +137,13 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { }; -helpers.getBaseUser = function(userslug, callerUID, callback) { - async.waterfall([ - function (next) { - user.getUidByUserslug(userslug, next); - }, - function (uid, next) { - if (!uid) { - return callback(null, null); - } - - async.parallel({ - user: function(next) { - user.getUserFields(uid, ['uid', 'username', 'userslug', 'picture', 'cover:url', 'cover:position', 'status', 'lastonline'], next); - }, - isAdmin: function(next) { - user.isAdministrator(callerUID, next); - }, - isGlobalModerator: function(next) { - user.isGlobalModerator(callerUID, next); - }, - profile_links: function(next) { - plugins.fireHook('filter:user.profileLinks', [], next); - } - }, next); - }, - function (results, next) { - if (!results.user) { - return callback(); - } - - results.user.yourid = callerUID; - results.user.theirid = results.user.uid; - results.user.status = user.getStatus(results.user); - results.user.isSelf = parseInt(callerUID, 10) === parseInt(results.user.uid, 10); - results.user.showHidden = results.user.isSelf || results.isAdmin || results.isGlobalModerator; - results.user.profile_links = filterLinks(results.profile_links, results.user.isSelf); - - results.user['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - results.user['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; - results.user['cover:url'] = results.user['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(results.user.uid); - results.user['cover:position'] = results.user['cover:position'] || '50% 50%'; - - next(null, results.user); - } - ], callback); +helpers.getBaseUser = function (userslug, callerUID, callback) { + winston.warn('helpers.getBaseUser deprecated please use helpers.getUserDataByUserSlug'); + helpers.getUserDataByUserSlug(userslug, callerUID, callback); }; function filterLinks(links, self) { - return links.filter(function(link) { + return links.filter(function (link) { return link && (link.public || self); }); } diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js new file mode 100644 index 0000000000..77ab2f275e --- /dev/null +++ b/src/controllers/accounts/info.js @@ -0,0 +1,45 @@ +'use strict'; + +var async = require('async'); + +var user = require('../../user'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); + +var infoController = {}; + +infoController.get = function (req, res, callback) { + var userData; + async.waterfall([ + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); + }, + function (_userData, next) { + userData = _userData; + if (!userData) { + return callback(); + } + async.parallel({ + history: async.apply(user.getModerationHistory, userData.uid), + sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID), + usernames: async.apply(user.getHistory, 'user:' + userData.uid + ':usernames'), + emails: async.apply(user.getHistory, 'user:' + userData.uid + ':emails') + }, next); + } + ], function (err, data) { + if (err) { + return callback(err); + } + + userData.history = data.history; + userData.sessions = data.sessions; + userData.usernames = data.usernames; + userData.emails = data.emails; + userData.title = '[[pages:account/info]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:account_info]]'}]); + + res.render('account/info', userData); + }); +}; + +module.exports = infoController; \ No newline at end of file diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index aa60892f47..91b9eb1d07 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -6,8 +6,8 @@ var user = require('../../user'), var notificationsController = {}; -notificationsController.get = function(req, res, next) { - user.notifications.getAll(req.uid, 0, 39, function(err, notifications) { +notificationsController.get = function (req, res, next) { + user.notifications.getAll(req.uid, 0, 39, function (err, notifications) { if (err) { return next(err); } diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 7e1e67a8bd..53ff073dad 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -1,31 +1,31 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - db = require('../../database'), - user = require('../../user'), - posts = require('../../posts'), - topics = require('../../topics'), - pagination = require('../../pagination'), - helpers = require('../helpers'), - accountHelpers = require('./helpers'); +var db = require('../../database'); +var user = require('../../user'); +var posts = require('../../posts'); +var topics = require('../../topics'); +var pagination = require('../../pagination'); +var helpers = require('../helpers'); +var accountHelpers = require('./helpers'); var postsController = {}; -postsController.getFavourites = function(req, res, next) { +postsController.getBookmarks = function (req, res, next) { var data = { - template: 'account/favourites', - set: 'favourites', + template: 'account/bookmarks', + set: 'bookmarks', type: 'posts', - noItemsFoundKey: '[[topic:favourites.has_no_favourites]]', + noItemsFoundKey: '[[topic:bookmarks.has_no_bookmarks]]', method: posts.getPostSummariesFromSet, - crumb: '[[user:favourites]]' + crumb: '[[user:bookmarks]]' }; getFromUserSet(data, req, res, next); }; -postsController.getPosts = function(req, res, next) { +postsController.getPosts = function (req, res, next) { var data = { template: 'account/posts', set: 'posts', @@ -37,7 +37,7 @@ postsController.getPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getUpVotedPosts = function(req, res, next) { +postsController.getUpVotedPosts = function (req, res, next) { var data = { template: 'account/upvoted', set: 'upvote', @@ -49,7 +49,7 @@ postsController.getUpVotedPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getDownVotedPosts = function(req, res, next) { +postsController.getDownVotedPosts = function (req, res, next) { var data = { template: 'account/downvoted', set: 'downvote', @@ -61,7 +61,7 @@ postsController.getDownVotedPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getBestPosts = function(req, res, next) { +postsController.getBestPosts = function (req, res, next) { var data = { template: 'account/best', set: 'posts:votes', @@ -73,7 +73,7 @@ postsController.getBestPosts = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getWatchedTopics = function(req, res, next) { +postsController.getWatchedTopics = function (req, res, next) { var data = { template: 'account/watched', set: 'followed_tids', @@ -85,7 +85,7 @@ postsController.getWatchedTopics = function(req, res, next) { getFromUserSet(data, req, res, next); }; -postsController.getTopics = function(req, res, next) { +postsController.getTopics = function (req, res, next) { var data = { template: 'account/topics', set: 'topics', @@ -99,13 +99,13 @@ postsController.getTopics = function(req, res, next) { function getFromUserSet(data, req, res, next) { async.parallel({ - settings: function(next) { + settings: function (next) { user.getSettings(req.uid, next); }, - userData: function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + userData: function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); } - }, function(err, results) { + }, function (err, results) { if (err || !results.userData) { return next(err); } @@ -118,19 +118,19 @@ function getFromUserSet(data, req, res, next) { var itemsPerPage = (data.template === 'account/topics' || data.template === 'account/watched') ? results.settings.topicsPerPage : results.settings.postsPerPage; async.parallel({ - itemCount: function(next) { + itemCount: function (next) { if (results.settings.usePagination) { db.sortedSetCard(setName, next); } else { next(null, 0); } }, - data: function(next) { + data: function (next) { var start = (page - 1) * itemsPerPage; var stop = start + itemsPerPage - 1; data.method(setName, req.uid, start, stop, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index e52b5f4861..f0fa1e1378 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -14,7 +14,7 @@ var helpers = require('../helpers'); var profileController = {}; -profileController.get = function(req, res, callback) { +profileController.get = function (req, res, callback) { var lowercaseSlug = req.params.userslug.toLowerCase(); if (req.params.userslug !== lowercaseSlug) { @@ -43,18 +43,18 @@ profileController.get = function(req, res, callback) { } async.parallel({ - isFollowing: function(next) { + isFollowing: function (next) { user.isFollowing(req.uid, userData.theirid, next); }, - posts: function(next) { + posts: function (next) { posts.getPostSummariesFromSet('uid:' + userData.theirid + ':posts', req.uid, 0, 9, next); }, - signature: function(next) { + signature: function (next) { posts.parseSignature(userData, req.uid, next); }, - aboutme: function(next) { + aboutme: function (next) { if (userData.aboutme) { - plugins.fireHook('filter:parse.raw', userData.aboutme, next); + plugins.fireHook('filter:parse.aboutme', userData.aboutme, next); } else { next(); } @@ -118,10 +118,13 @@ profileController.get = function(req, res, callback) { } ); } + userData.selectedGroup = userData.groups.find(function (group) { + return group && group.name === userData.groupTitle; + }); plugins.fireHook('filter:user.account', {userData: userData, uid: req.uid}, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } diff --git a/src/controllers/accounts/session.js b/src/controllers/accounts/session.js index 8fdb180ded..7e31906f26 100644 --- a/src/controllers/accounts/session.js +++ b/src/controllers/accounts/session.js @@ -7,20 +7,27 @@ var user = require('../../user'); var sessionController = {}; -sessionController.revoke = function(req, res, next) { +sessionController.revoke = function (req, res, next) { if (!req.params.hasOwnProperty('uuid')) { return next(); } var _id; - + var uid; async.waterfall([ function (next) { - db.getSortedSetRange('uid:' + req.uid + ':sessions', 0, -1, next); + user.getUidByUserslug(req.params.userslug, next); + }, + function (_uid, next) { + uid = _uid; + if (!uid) { + return next(new Error('[[error:no-session-found]]')); + } + db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1, next); }, function (sids, done) { - async.eachSeries(sids, function(sid, next) { - db.sessionStore.get(sid, function(err, sessionObj) { + async.eachSeries(sids, function (sid, next) { + db.sessionStore.get(sid, function (err, sessionObj) { if (err) { return next(err); } @@ -38,9 +45,9 @@ sessionController.revoke = function(req, res, next) { return next(new Error('[[error:no-session-found]]')); } - user.auth.revokeSession(_id, req.uid, next); + user.auth.revokeSession(_id, uid, next); } - ], function(err) { + ], function (err) { if (err) { return res.status(500).send(err.message); } else { diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index b5b020c118..44499e7e68 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -3,7 +3,6 @@ var async = require('async'); var user = require('../../user'); -var groups = require('../../groups'); var languages = require('../../languages'); var meta = require('../../meta'); var plugins = require('../../plugins'); @@ -17,53 +16,60 @@ var accountHelpers = require('./helpers'); var settingsController = {}; -settingsController.get = function(req, res, callback) { +settingsController.get = function (req, res, callback) { var userData; async.waterfall([ - function(next) { - accountHelpers.getBaseUser(req.params.userslug, req.uid, next); + function (next) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next); }, - function(_userData, next) { + function (_userData, next) { userData = _userData; if (!userData) { return callback(); } async.parallel({ - settings: function(next) { + settings: function (next) { user.getSettings(userData.uid, next); }, - userGroups: function(next) { - groups.getUserGroupsFromSet('groups:createtime', [userData.uid], next); - }, - languages: function(next) { + languages: function (next) { languages.list(next); }, - homePageRoutes: function(next) { + homePageRoutes: function (next) { getHomePageRoutes(next); }, - ips: function (next) { - user.getIPs(userData.uid, 4, next); + sounds: function (next) { + meta.sounds.getFiles(next); }, - sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID) + soundsMapping: function (next) { + meta.sounds.getMapping(userData.uid, next); + } }, next); }, - function(results, next) { + function (results, next) { userData.settings = results.settings; - userData.userGroups = results.userGroups[0].filter(function(group) { - return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users'; - }); userData.languages = results.languages; userData.homePageRoutes = results.homePageRoutes; - userData.ips = results.ips; - userData.sessions = results.sessions; + + var soundSettings = { + 'notificationSound': 'notification', + 'incomingChatSound': 'chat-incoming', + 'outgoingChatSound': 'chat-outgoing' + }; + + Object.keys(soundSettings).forEach(function (setting) { + userData[setting] = Object.keys(results.sounds).map(function (name) { + return {name: name, selected: name === results.soundsMapping[soundSettings[setting]]}; + }); + }); + plugins.fireHook('filter:user.customSettings', {settings: results.settings, customSettings: [], uid: req.uid}, next); }, - function(data, next) { + function (data, next) { userData.customSettings = data.customSettings; userData.disableEmailSubscriptions = parseInt(meta.config.disableEmailSubscriptions, 10) === 1; next(); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -96,19 +102,29 @@ settingsController.get = function(req, res, callback) { { "name": "Yeti", "value": "yeti" } ]; - userData.homePageRoutes.forEach(function(route) { + var isCustom = true; + userData.homePageRoutes.forEach(function (route) { route.selected = route.route === userData.settings.homePageRoute; + if (route.selected) { + isCustom = false; + } }); - userData.bootswatchSkinOptions.forEach(function(skin) { - skin.selected = skin.value === userData.settings.bootswatchSkin; + if (isCustom && userData.settings.homePageRoute === 'none') { + isCustom = false; + } + + userData.homePageRoutes.push({ + route: 'custom', + name: 'Custom', + selected: isCustom }); - userData.userGroups.forEach(function(group) { - group.selected = group.name === userData.settings.groupTitle; + userData.bootswatchSkinOptions.forEach(function (skin) { + skin.selected = skin.value === userData.settings.bootswatchSkin; }); - userData.languages.forEach(function(language) { + userData.languages.forEach(function (language) { language.selected = language.code === userData.settings.userLang; }); @@ -116,6 +132,8 @@ settingsController.get = function(req, res, callback) { userData.allowUserHomePage = parseInt(meta.config.allowUserHomePage, 10) === 1; + userData.inTopicSearchAvailable = plugins.hasListeners('filter:topic.search'); + userData.title = '[[pages:account/settings]]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:settings]]'}]); @@ -136,46 +154,38 @@ function getHomePageRoutes(callback) { categories.getCategoriesFields(cids, ['name', 'slug'], next); }, function (categoryData, next) { - categoryData = categoryData.map(function(category) { + categoryData = categoryData.map(function (category) { return { route: 'category/' + category.slug, name: 'Category: ' + category.name }; }); - next(null, categoryData); - } - ], function(err, categoryData) { - if (err) { - return callback(err); - } - categoryData = categoryData || []; - - plugins.fireHook('filter:homepage.get', {routes: [ - { - route: 'categories', - name: 'Categories' - }, - { - route: 'recent', - name: 'Recent' - }, - { - route: 'popular', - name: 'Popular' - } - ].concat(categoryData)}, function(err, data) { - if (err) { - return callback(err); - } - data.routes.push({ - route: 'custom', - name: 'Custom' - }); + categoryData = categoryData || []; - callback(null, data.routes); - }); - }); + plugins.fireHook('filter:homepage.get', {routes: [ + { + route: 'categories', + name: 'Categories' + }, + { + route: 'unread', + name: 'Unread' + }, + { + route: 'recent', + name: 'Recent' + }, + { + route: 'popular', + name: 'Popular' + } + ].concat(categoryData)}, next); + }, + function (data, next) { + next(null, data.routes); + } + ], callback); } diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 2bba60cae6..7f622466cd 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -14,8 +14,9 @@ var adminController = { }, events: require('./admin/events'), logs: require('./admin/logs'), + errors: require('./admin/errors'), database: require('./admin/database'), - postCache: require('./admin/postCache'), + cache: require('./admin/cache'), plugins: require('./admin/plugins'), languages: require('./admin/languages'), settings: require('./admin/settings'), diff --git a/src/controllers/admin/appearance.js b/src/controllers/admin/appearance.js index 8d60efda23..8956bd175d 100644 --- a/src/controllers/admin/appearance.js +++ b/src/controllers/admin/appearance.js @@ -2,7 +2,7 @@ var appearanceController = {}; -appearanceController.get = function(req, res, next) { +appearanceController.get = function (req, res, next) { var term = req.params.term ? req.params.term : 'themes'; res.render('admin/appearance/' + term, {}); diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js index 2c0104f742..d70b1d1d79 100644 --- a/src/controllers/admin/blacklist.js +++ b/src/controllers/admin/blacklist.js @@ -4,8 +4,8 @@ var meta = require('../../meta'); var blacklistController = {}; -blacklistController.get = function(req, res, next) { - meta.blacklist.get(function(err, rules) { +blacklistController.get = function (req, res, next) { + meta.blacklist.get(function (err, rules) { if (err) { return next(err); } diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js new file mode 100644 index 0000000000..21ef6ff086 --- /dev/null +++ b/src/controllers/admin/cache.js @@ -0,0 +1,35 @@ +'use strict'; + +var cacheController = {}; + +cacheController.get = function (req, res, next) { + var postCache = require('../../posts/cache'); + var groupCache = require('../../groups').cache; + + var avgPostSize = 0; + var percentFull = 0; + if (postCache.itemCount > 0) { + avgPostSize = parseInt((postCache.length / postCache.itemCount), 10); + percentFull = ((postCache.length / postCache.max) * 100).toFixed(2); + } + + res.render('admin/advanced/cache', { + postCache: { + length: postCache.length, + max: postCache.max, + itemCount: postCache.itemCount, + percentFull: percentFull, + avgPostSize: avgPostSize + }, + groupCache: { + length: groupCache.length, + max: groupCache.max, + itemCount: groupCache.itemCount, + percentFull: ((groupCache.length / groupCache.max) * 100).toFixed(2), + dump: req.query.debug ? JSON.stringify(groupCache.dump(), null, 4) : false + } + }); +}; + + +module.exports = cacheController; \ No newline at end of file diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index 5444a087ea..8a59bb0b4e 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -6,25 +6,30 @@ var categories = require('../../categories'); var privileges = require('../../privileges'); var analytics = require('../../analytics'); var plugins = require('../../plugins'); -var translator = require('../../../public/src/modules/translator') +var translator = require('../../../public/src/modules/translator'); var categoriesController = {}; -categoriesController.get = function(req, res, next) { +categoriesController.get = function (req, res, next) { async.parallel({ category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), privileges: async.apply(privileges.categories.list, req.params.category_id) - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } + var category = data.category[0]; - plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: data.category[0], privileges: data.privileges }, function(err, data) { + if (!category) { + return next(); + } + + plugins.fireHook('filter:admin.category.get', { req: req, res: res, category: category, privileges: data.privileges }, function (err, data) { if (err) { return next(err); } - data.category.name = translator.escape(data.category.name); + data.category.name = translator.escape(String(data.category.name)); res.render('admin/manage/category', { category: data.category, privileges: data.privileges @@ -33,16 +38,20 @@ categoriesController.get = function(req, res, next) { }); }; -categoriesController.getAll = function(req, res, next) { +categoriesController.getAll = function (req, res, next) { // Categories list will be rendered on client side with recursion, etc. res.render('admin/manage/categories', {}); }; -categoriesController.getAnalytics = function(req, res, next) { +categoriesController.getAnalytics = function (req, res, next) { async.parallel({ name: async.apply(categories.getCategoryField, req.params.category_id, 'name'), analytics: async.apply(analytics.getCategoryAnalytics, req.params.category_id) - }, function(err, data) { + }, function (err, data) { + if (err) { + return next(err); + } + res.render('admin/manage/category-analytics', data); }); }; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index 24a65983f1..22105b6652 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -10,17 +10,17 @@ var plugins = require('../../plugins'); var dashboardController = {}; -dashboardController.get = function(req, res, next) { +dashboardController.get = function (req, res, next) { async.parallel({ - stats: function(next) { + stats: function (next) { getStats(next); }, - notices: function(next) { + notices: function (next) { var notices = [ { done: !meta.reloadRequired, - doneText: 'Reload not required', - notDoneText:'Reload required' + doneText: 'Restart not required', + notDoneText:'Restart required' }, { done: plugins.hasListeners('filter:search.query'), @@ -32,7 +32,7 @@ dashboardController.get = function(req, res, next) { ]; plugins.fireHook('filter:admin.notices', notices, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } @@ -46,19 +46,19 @@ dashboardController.get = function(req, res, next) { function getStats(callback) { async.parallel([ - function(next) { + function (next) { getStatsForSet('ip:recent', 'uniqueIPCount', next); }, - function(next) { + function (next) { getStatsForSet('users:joindate', 'userCount', next); }, - function(next) { + function (next) { getStatsForSet('posts:pid', 'postCount', next); }, - function(next) { + function (next) { getStatsForSet('topics:tid', 'topicCount', next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } @@ -80,23 +80,23 @@ function getStatsForSet(set, field, callback) { var now = Date.now(); async.parallel({ - day: function(next) { + day: function (next) { db.sortedSetCount(set, now - terms.day, '+inf', next); }, - week: function(next) { + week: function (next) { db.sortedSetCount(set, now - terms.week, '+inf', next); }, - month: function(next) { + month: function (next) { db.sortedSetCount(set, now - terms.month, '+inf', next); }, - alltime: function(next) { + alltime: function (next) { getGlobalField(field, next); } }, callback); } function getGlobalField(field, callback) { - db.getObjectField('global', field, function(err, count) { + db.getObjectField('global', field, function (err, count) { callback(err, parseInt(count, 10) || 0); }); } diff --git a/src/controllers/admin/database.js b/src/controllers/admin/database.js index d15db3f09c..5a28b95ec4 100644 --- a/src/controllers/admin/database.js +++ b/src/controllers/admin/database.js @@ -7,18 +7,17 @@ var databaseController = {}; -databaseController.get = function(req, res, next) { +databaseController.get = function (req, res, next) { async.parallel({ - redis: function(next) { + redis: function (next) { if (nconf.get('redis')) { var rdb = require('../../database/redis'); - var cxn = rdb.connect(); - rdb.info(cxn, next); + rdb.info(rdb.client, next); } else { next(); } }, - mongo: function(next) { + mongo: function (next) { if (nconf.get('mongo')) { var mdb = require('../../database/mongo'); mdb.info(mdb.client, next); @@ -26,7 +25,7 @@ databaseController.get = function(req, res, next) { next(); } } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js new file mode 100644 index 0000000000..4cacd425c9 --- /dev/null +++ b/src/controllers/admin/errors.js @@ -0,0 +1,38 @@ +'use strict'; + +var async = require('async'); +var json2csv = require('json-2-csv').json2csv; + +var meta = require('../../meta'); +var analytics = require('../../analytics'); + +var errorsController = {}; + +errorsController.get = function (req, res, next) { + async.parallel({ + 'not-found': async.apply(meta.errors.get, true), + analytics: async.apply(analytics.getErrorAnalytics) + }, function (err, data) { + if (err) { + return next(err); + } + + res.render('admin/advanced/errors', data); + }); +}; + +errorsController.export = function (req, res, next) { + async.waterfall([ + async.apply(meta.errors.get, false), + async.apply(json2csv) + ], function (err, csv) { + if (err) { + return next(err); + } + + res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="404.csv"').send(csv); + }); +}; + + +module.exports = errorsController; \ No newline at end of file diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js index ceee1e2a70..8a4d63bad1 100644 --- a/src/controllers/admin/events.js +++ b/src/controllers/admin/events.js @@ -1,18 +1,38 @@ 'use strict'; +var async = require('async'); + +var db = require('../../database'); var events = require('../../events'); +var pagination = require('../../pagination'); var eventsController = {}; -eventsController.get = function(req, res, next) { - events.getEvents(0, 19, function(err, events) { +eventsController.get = function (req, res, next) { + + var page = parseInt(req.query.page, 10) || 1; + var itemsPerPage = 20; + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + + async.parallel({ + eventCount: function (next) { + db.sortedSetCard('events:time', next); + }, + events: function (next) { + events.getEvents(start, stop, next); + } + }, function (err, results) { if (err) { return next(err); } + var pageCount = Math.max(1, Math.ceil(results.eventCount / itemsPerPage)); + res.render('admin/advanced/events', { - events: events, + events: results.events, + pagination: pagination.create(page, pageCount), next: 20 }); }); diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js index cb2f830365..1b31a95ff4 100644 --- a/src/controllers/admin/flags.js +++ b/src/controllers/admin/flags.js @@ -1,38 +1,103 @@ "use strict"; var async = require('async'); +var validator = require('validator'); + var posts = require('../../posts'); +var user = require('../../user'); +var categories = require('../../categories'); +var analytics = require('../../analytics'); +var pagination = require('../../pagination'); var flagsController = {}; -flagsController.get = function(req, res, next) { - var sortBy = req.query.sortBy || 'count'; +var itemsPerPage = 20; + +flagsController.get = function (req, res, next) { var byUsername = req.query.byUsername || ''; - var start = 0; - var stop = 19; + var cid = req.query.cid || 0; + var sortBy = req.query.sortBy || 'count'; + var page = parseInt(req.query.page, 10) || 1; - async.waterfall([ - function (next) { - if (byUsername) { - posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next); - } else { - var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; - posts.getFlags(set, req.uid, start, stop, next); - } - } - ], function (err, posts) { + async.parallel({ + categories: function (next) { + categories.buildForSelect(req.uid, next); + }, + flagData: function (next) { + getFlagData(req, res, next); + }, + analytics: function (next) { + analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next); + }, + assignees: async.apply(user.getAdminsandGlobalModsandModerators) + }, function (err, results) { if (err) { return next(err); } + + // Minimise data set for assignees so tjs does less work + results.assignees = results.assignees.map(function (userObj) { + return { + uid: userObj.uid, + username: userObj.username + }; + }); + + // If res.locals.cids is populated, then slim down the categories list + if (res.locals.cids) { + results.categories = results.categories.filter(function (category) { + return res.locals.cids.indexOf(String(category.cid)) !== -1; + }); + } + + var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage)); + + results.categories.forEach(function (category) { + category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); + }); + var data = { - posts: posts, - next: stop + 1, - byUsername: byUsername, + posts: results.flagData.posts, + assignees: results.assignees, + analytics: results.analytics, + categories: results.categories, + byUsername: validator.escape(String(byUsername)), + sortByCount: sortBy === 'count', + sortByTime: sortBy === 'time', + pagination: pagination.create(page, pageCount, req.query), title: '[[pages:flagged-posts]]' }; res.render('admin/manage/flags', data); }); }; +function getFlagData(req, res, callback) { + var sortBy = req.query.sortBy || 'count'; + var byUsername = req.query.byUsername || ''; + var cid = req.query.cid || res.locals.cids || 0; + var page = parseInt(req.query.page, 10) || 1; + var start = (page - 1) * itemsPerPage; + var stop = start + itemsPerPage - 1; + + var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged']; + + async.waterfall([ + function (next) { + if (byUsername) { + user.getUidByUsername(byUsername, next); + } else { + process.nextTick(next, null, 0); + } + }, + function (uid, next) { + if (uid) { + sets.push('uid:' + uid + ':flag:pids'); + } + + posts.getFlags(sets, cid, req.uid, start, stop, next); + } + ], callback); +} + module.exports = flagsController; diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 03b3514327..19aeb4f2f4 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -12,17 +12,17 @@ var async = require('async'), var groupsController = {}; -groupsController.list = function(req, res, next) { +groupsController.list = function (req, res, next) { var page = parseInt(req.query.page, 10) || 1; var groupsPerPage = 20; var pageCount = 0; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('groups:createtime', 0, -1, next); }, - function(groupNames, next) { - groupNames = groupNames.filter(function(name) { + function (groupNames, next) { + groupNames = groupNames.filter(function (name) { return name.indexOf(':privileges:') === -1 && name !== 'registered-users'; }); pageCount = Math.ceil(groupNames.length / groupsPerPage); @@ -33,10 +33,10 @@ groupsController.list = function(req, res, next) { groupNames = groupNames.slice(start, stop + 1); groups.getGroupsData(groupNames, next); }, - function(groupData, next) { + function (groupData, next) { next(null, {groups: groupData, pagination: pagination.create(page, pageCount)}); } - ], function(err, data) { + ], function (err, data) { if (err) { return next(err); } @@ -49,19 +49,19 @@ groupsController.list = function(req, res, next) { }); }; -groupsController.get = function(req, res, callback) { +groupsController.get = function (req, res, callback) { var groupName = req.params.name; async.waterfall([ - function(next){ + function (next){ groups.exists(groupName, next); }, - function(exists, next) { + function (exists, next) { if (!exists) { return callback(); } groups.get(groupName, {uid: req.uid, truncateUserList: true, userListCount: 20}, next); } - ], function(err, group) { + ], function (err, group) { if (err) { return callback(err); } diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js index f503dd8865..9fdf3a2371 100644 --- a/src/controllers/admin/homepage.js +++ b/src/controllers/admin/homepage.js @@ -10,19 +10,19 @@ var plugins = require('../../plugins'); var homePageController = {}; -homePageController.get = function(req, res, next) { +homePageController.get = function (req, res, next) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange('categories:cid', 0, -1, next); }, - function(cids, next) { + function (cids, next) { privileges.categories.filterCids('find', cids, 0, next); }, - function(cids, next) { + function (cids, next) { categories.getCategoriesFields(cids, ['name', 'slug'], next); }, - function(categoryData, next) { - categoryData = categoryData.map(function(category) { + function (categoryData, next) { + categoryData = categoryData.map(function (category) { return { route: 'category/' + category.slug, name: 'Category: ' + category.name @@ -30,7 +30,7 @@ homePageController.get = function(req, res, next) { }); next(null, categoryData); } - ], function(err, categoryData) { + ], function (err, categoryData) { if (err || !categoryData) { categoryData = []; } @@ -48,7 +48,11 @@ homePageController.get = function(req, res, next) { route: 'popular', name: 'Popular' } - ].concat(categoryData)}, function(err, data) { + ].concat(categoryData)}, function (err, data) { + if (err) { + return next(err); + } + data.routes.push({ route: '', name: 'Custom' diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index 2459ad7140..6c60d6a048 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -13,23 +13,23 @@ var infoController = {}; var info = {}; -infoController.get = function(req, res, next) { +infoController.get = function (req, res, next) { info = {}; pubsub.publish('sync:node:info:start'); - setTimeout(function() { + setTimeout(function () { var data = []; - Object.keys(info).forEach(function(key) { + Object.keys(info).forEach(function (key) { data.push(info[key]); }); - data.sort(function(a, b) { + data.sort(function (a, b) { return (a.os.hostname < b.os.hostname) ? -1 : (a.os.hostname > b.os.hostname) ? 1 : 0; }); res.render('admin/development/info', {info: data, infoJSON: JSON.stringify(data, null, 4), host: os.hostname(), port: nconf.get('port')}); - }, 300); + }, 500); }; -pubsub.on('sync:node:info:start', function() { - getNodeInfo(function(err, data) { +pubsub.on('sync:node:info:start', function () { + getNodeInfo(function (err, data) { if (err) { return winston.error(err); } @@ -37,7 +37,7 @@ pubsub.on('sync:node:info:start', function() { }); }); -pubsub.on('sync:node:info:end', function(data) { +pubsub.on('sync:node:info:end', function (data) { info[data.id] = data.data; }); @@ -57,39 +57,38 @@ function getNodeInfo(callback) { platform: os.platform(), arch: os.arch(), release: os.release(), - load: os.loadavg().map(function(load){ return load.toFixed(2); }).join(', ') + load: os.loadavg().map(function (load){ return load.toFixed(2); }).join(', ') } }; async.parallel({ - pubsub: function(next) { - pubsub.publish('sync:stats:start'); - next(); + stats: function (next) { + rooms.getLocalStats(next); }, - gitInfo: function(next) { + gitInfo: function (next) { getGitInfo(next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } data.git = results.gitInfo; - data.stats = rooms.stats[data.os.hostname + ':' + data.process.port]; + data.stats = results.stats; callback(null, data); }); } function getGitInfo(callback) { function get(cmd, callback) { - exec(cmd, function(err, stdout) { + exec(cmd, function (err, stdout) { callback(err, stdout ? stdout.replace(/\n$/, '') : ''); }); } async.parallel({ - hash: function(next) { + hash: function (next) { get('git rev-parse HEAD', next); }, - branch: function(next) { + branch: function (next) { get('git rev-parse --abbrev-ref HEAD', next); } }, callback); diff --git a/src/controllers/admin/languages.js b/src/controllers/admin/languages.js index 85c6d60484..292cd2a3b4 100644 --- a/src/controllers/admin/languages.js +++ b/src/controllers/admin/languages.js @@ -6,13 +6,13 @@ var meta = require('../../meta'); var languagesController = {}; -languagesController.get = function(req, res, next) { - languages.list(function(err, languages) { +languagesController.get = function (req, res, next) { + languages.list(function (err, languages) { if (err) { return next(err); } - languages.forEach(function(language) { + languages.forEach(function (language) { language.selected = language.code === (meta.config.defaultLang || 'en_GB'); }); diff --git a/src/controllers/admin/logger.js b/src/controllers/admin/logger.js index 45c9f246c9..7ae327a858 100644 --- a/src/controllers/admin/logger.js +++ b/src/controllers/admin/logger.js @@ -2,7 +2,7 @@ var loggerController = {}; -loggerController.get = function(req, res) { +loggerController.get = function (req, res) { res.render('admin/development/logger', {}); }; diff --git a/src/controllers/admin/logs.js b/src/controllers/admin/logs.js index f3ae601dd4..6723d3795f 100644 --- a/src/controllers/admin/logs.js +++ b/src/controllers/admin/logs.js @@ -6,8 +6,8 @@ var meta = require('../../meta'); var logsController = {}; -logsController.get = function(req, res, next) { - meta.logs.get(function(err, logs) { +logsController.get = function (req, res, next) { + meta.logs.get(function (err, logs) { if (err) { return next(err); } diff --git a/src/controllers/admin/navigation.js b/src/controllers/admin/navigation.js index 463c525eff..423f21721c 100644 --- a/src/controllers/admin/navigation.js +++ b/src/controllers/admin/navigation.js @@ -2,14 +2,14 @@ var navigationController = {}; -navigationController.get = function(req, res, next) { - require('../../navigation/admin').getAdmin(function(err, data) { +navigationController.get = function (req, res, next) { + require('../../navigation/admin').getAdmin(function (err, data) { if (err) { return next(err); } - data.enabled.forEach(function(enabled, index) { + data.enabled.forEach(function (enabled, index) { enabled.index = index; enabled.selected = index === 0; }); diff --git a/src/controllers/admin/plugins.js b/src/controllers/admin/plugins.js index a4733e4c51..f1a72720ac 100644 --- a/src/controllers/admin/plugins.js +++ b/src/controllers/admin/plugins.js @@ -5,10 +5,10 @@ var plugins = require('../../plugins'); var pluginsController = {}; -pluginsController.get = function(req, res, next) { +pluginsController.get = function (req, res, next) { async.parallel({ - compatible: function(next) { - plugins.list(function(err, plugins) { + compatible: function (next) { + plugins.list(function (err, plugins) { if (err || !Array.isArray(plugins)) { plugins = []; } @@ -16,8 +16,8 @@ pluginsController.get = function(req, res, next) { next(null, plugins); }); }, - all: function(next) { - plugins.list(false, function(err, plugins) { + all: function (next) { + plugins.list(false, function (err, plugins) { if (err || !Array.isArray(plugins)) { plugins = []; } @@ -25,22 +25,28 @@ pluginsController.get = function(req, res, next) { next(null, plugins); }); } - }, function(err, payload) { + }, function (err, payload) { if (err) { return next(err); } - var compatiblePkgNames = payload.compatible.map(function(pkgData) { + var compatiblePkgNames = payload.compatible.map(function (pkgData) { return pkgData.name; }); res.render('admin/extend/plugins' , { - installed: payload.compatible.filter(function(plugin) { + installed: payload.compatible.filter(function (plugin) { return plugin.installed; }), - download: payload.compatible.filter(function(plugin) { + upgradeCount: payload.compatible.reduce(function (count, current) { + if (current.installed && current.outdated) { + ++count; + } + return count; + }, 0), + download: payload.compatible.filter(function (plugin) { return !plugin.installed; }), - incompatible: payload.all.filter(function(plugin) { + incompatible: payload.all.filter(function (plugin) { return compatiblePkgNames.indexOf(plugin.name) === -1; }) }); diff --git a/src/controllers/admin/postCache.js b/src/controllers/admin/postCache.js deleted file mode 100644 index bbfd222586..0000000000 --- a/src/controllers/admin/postCache.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -var postCacheController = {}; - -postCacheController.get = function(req, res, next) { - var cache = require('../../posts/cache'); - var avgPostSize = 0; - var percentFull = 0; - if (cache.itemCount > 0) { - avgPostSize = parseInt((cache.length / cache.itemCount), 10); - percentFull = ((cache.length / cache.max) * 100).toFixed(2); - } - - res.render('admin/advanced/post-cache', { - cache: { - length: cache.length, - max: cache.max, - itemCount: cache.itemCount, - percentFull: percentFull, - avgPostSize: avgPostSize - } - }); -}; - - -module.exports = postCacheController; \ No newline at end of file diff --git a/src/controllers/admin/rewards.js b/src/controllers/admin/rewards.js index 063abb6807..8ff05c75b3 100644 --- a/src/controllers/admin/rewards.js +++ b/src/controllers/admin/rewards.js @@ -2,8 +2,8 @@ var rewardsController = {}; -rewardsController.get = function(req, res, next) { - require('../../rewards/admin').get(function(err, data) { +rewardsController.get = function (req, res, next) { + require('../../rewards/admin').get(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index bf0975058a..a6afb80cfc 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -4,7 +4,7 @@ var settingsController = {}; var async = require('async'), meta = require('../../meta'); -settingsController.get = function(req, res, next) { +settingsController.get = function (req, res, next) { var term = req.params.term ? req.params.term : 'general'; switch (req.params.term) { @@ -19,29 +19,41 @@ settingsController.get = function(req, res, next) { function renderEmail(req, res, next) { - var fs = require('fs'), - path = require('path'), - utils = require('../../../public/src/utils'); + var fs = require('fs'); + var path = require('path'); + var utils = require('../../../public/src/utils'); var emailsPath = path.join(__dirname, '../../../public/templates/emails'); - utils.walk(emailsPath, function(err, emails) { - async.map(emails, function(email, next) { + utils.walk(emailsPath, function (err, emails) { + if (err) { + return next(err); + } + + async.map(emails, function (email, next) { var path = email.replace(emailsPath, '').substr(1).replace('.tpl', ''); - fs.readFile(email, function(err, original) { + fs.readFile(email, function (err, original) { + if (err) { + return next(err); + } + var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original.toString(); - next(err, { + next(null, { path: path, fullpath: email, text: text, original: original.toString() }); }); - }, function(err, emails) { + }, function (err, emails) { + if (err) { + return next(err); + } + res.render('admin/settings/email', { emails: emails, - sendable: emails.filter(function(email) { + sendable: emails.filter(function (email) { return email.path.indexOf('_plaintext') === -1 && email.path.indexOf('partials') === -1; }) }); diff --git a/src/controllers/admin/social.js b/src/controllers/admin/social.js index d8f87af060..11c7982701 100644 --- a/src/controllers/admin/social.js +++ b/src/controllers/admin/social.js @@ -5,8 +5,8 @@ var social = require('../../social'); var socialController = {}; -socialController.get = function(req, res, next) { - social.getPostSharing(function(err, posts) { +socialController.get = function (req, res, next) { + social.getPostSharing(function (err, posts) { if (err) { return next(err); } diff --git a/src/controllers/admin/sounds.js b/src/controllers/admin/sounds.js index 6e7ebf3f19..801a2067ac 100644 --- a/src/controllers/admin/sounds.js +++ b/src/controllers/admin/sounds.js @@ -4,9 +4,13 @@ var meta = require('../../meta'); var soundsController = {}; -soundsController.get = function(req, res, next) { - meta.sounds.getFiles(function(err, sounds) { - sounds = Object.keys(sounds).map(function(name) { +soundsController.get = function (req, res, next) { + meta.sounds.getFiles(function (err, sounds) { + if (err) { + return next(err); + } + + sounds = Object.keys(sounds).map(function (name) { return { name: name }; diff --git a/src/controllers/admin/tags.js b/src/controllers/admin/tags.js index 22e3b32d67..a645e2ef11 100644 --- a/src/controllers/admin/tags.js +++ b/src/controllers/admin/tags.js @@ -4,8 +4,8 @@ var topics = require('../../topics'); var tagsController = {}; -tagsController.get = function(req, res, next) { - topics.getTags(0, 199, function(err, tags) { +tagsController.get = function (req, res, next) { + topics.getTags(0, 199, function (err, tags) { if (err) { return next(err); } diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js index e5ef8a9343..4f6f3e1f3b 100644 --- a/src/controllers/admin/themes.js +++ b/src/controllers/admin/themes.js @@ -5,9 +5,9 @@ var file = require('../../file'); var themesController = {}; -themesController.get = function(req, res, next) { +themesController.get = function (req, res, next) { var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme); - file.exists(themeDir, function(exists) { + file.exists(themeDir, function (exists) { if (!exists) { return next(); } diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index aea79e3934..d1664cb8f1 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -1,26 +1,26 @@ "use strict"; -var fs = require('fs'), - path = require('path'), - async = require('async'), - nconf = require('nconf'), - winston = require('winston'), - file = require('../../file'), - image = require('../../image'), - plugins = require('../../plugins'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var nconf = require('nconf'); +var winston = require('winston'); +var file = require('../../file'); +var image = require('../../image'); +var plugins = require('../../plugins'); +var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; var uploadsController = {}; -uploadsController.uploadCategoryPicture = function(req, res, next) { +uploadsController.uploadCategoryPicture = function (req, res, next) { var uploadedFile = req.files.files[0]; - var allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/svg+xml'], - params = null; + var params = null; try { params = JSON.parse(req.body.params); } catch (e) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -28,19 +28,19 @@ uploadsController.uploadCategoryPicture = function(req, res, next) { return next(e); } - if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { + if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = 'category-' + params.cid + path.extname(uploadedFile.name); uploadImage(filename, 'category', uploadedFile, req, res, next); } }; -uploadsController.uploadFavicon = function(req, res, next) { +uploadsController.uploadFavicon = function (req, res, next) { var uploadedFile = req.files.files[0]; var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { - file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function(err, image) { - fs.unlink(uploadedFile.path, function(err) { + file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function (err, image) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -54,15 +54,19 @@ uploadsController.uploadFavicon = function(req, res, next) { } }; -uploadsController.uploadTouchIcon = function(req, res, next) { +uploadsController.uploadTouchIcon = function (req, res, next) { var uploadedFile = req.files.files[0], allowedTypes = ['image/png'], sizes = [36, 48, 72, 96, 144, 192]; if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { - file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function(err, imageObj) { + file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path, function (err, imageObj) { + if (err) { + return next(err); + } + // Resize the image into squares for use as touch icons at various DPIs - async.each(sizes, function(size, next) { + async.each(sizes, function (size, next) { async.series([ async.apply(file.saveFileToLocal, 'touchicon-' + size + '.png', 'system', uploadedFile.path), async.apply(image.resizeImage, { @@ -72,8 +76,8 @@ uploadsController.uploadTouchIcon = function(req, res, next) { height: size }) ], next); - }, function(err) { - fs.unlink(uploadedFile.path, function(err) { + }, function (err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -89,14 +93,14 @@ uploadsController.uploadTouchIcon = function(req, res, next) { } }; -uploadsController.uploadLogo = function(req, res, next) { +uploadsController.uploadLogo = function (req, res, next) { upload('site-logo', req, res, next); }; -uploadsController.uploadSound = function(req, res, next) { +uploadsController.uploadSound = function (req, res, next) { var uploadedFile = req.files.files[0]; - file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function(err) { + file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) { if (err) { return next(err); } @@ -110,7 +114,7 @@ uploadsController.uploadSound = function(req, res, next) { fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file'); } - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { return next(err); } @@ -120,14 +124,18 @@ uploadsController.uploadSound = function(req, res, next) { }); }; -uploadsController.uploadDefaultAvatar = function(req, res, next) { +uploadsController.uploadDefaultAvatar = function (req, res, next) { upload('avatar-default', req, res, next); }; +uploadsController.uploadOgImage = function (req, res, next) { + upload('og:image', req, res, next); +}; + function upload(name, req, res, next) { var uploadedFile = req.files.files[0]; - var allowedTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif']; - if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { + + if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { var filename = name + path.extname(uploadedFile.name); uploadImage(filename, 'system', uploadedFile, req, res, next); } @@ -135,7 +143,7 @@ function upload(name, req, res, next) { function validateUpload(req, res, next, uploadedFile, allowedTypes) { if (allowedTypes.indexOf(uploadedFile.type) === -1) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } @@ -150,7 +158,7 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { function uploadImage(filename, folder, uploadedFile, req, res, next) { function done(err, image) { - fs.unlink(uploadedFile.path, function(err) { + fs.unlink(uploadedFile.path, function (err) { if (err) { winston.error(err); } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 7e5bd530d6..426d8b1c19 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -1,116 +1,92 @@ "use strict"; var async = require('async'); +var validator = require('validator'); + var user = require('../../user'); var meta = require('../../meta'); var db = require('../../database'); var pagination = require('../../pagination'); - +var events = require('../../events'); +var plugins = require('../../plugins'); var usersController = {}; -usersController.search = function(req, res, next) { +var userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', + 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed']; + +usersController.search = function (req, res, next) { res.render('admin/manage/users', { search_display: '', users: [] }); }; -usersController.sortByJoinDate = function(req, res, next) { - getUsers('users:joindate', 'latest', req, res, next); +usersController.sortByJoinDate = function (req, res, next) { + getUsers('users:joindate', 'latest', undefined, undefined, req, res, next); +}; + +usersController.notValidated = function (req, res, next) { + getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res, next); }; -usersController.notValidated = function(req, res, next) { - getUsers('users:notvalidated', 'notvalidated', req, res, next); +usersController.noPosts = function (req, res, next) { + getUsers('users:postcount', 'noposts', '-inf', 0, req, res, next); }; -usersController.noPosts = function(req, res, next) { - getUsersByScore('users:postcount', 'noposts', 0, 0, req, res, next); +usersController.flagged = function (req, res, next) { + getUsers('users:flags', 'mostflags', 1, '+inf', req, res, next); }; -usersController.inactive = function(req, res, next) { +usersController.inactive = function (req, res, next) { var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3); var cutoff = Date.now() - timeRange; - getUsersByScore('users:online', 'inactive', '-inf', cutoff, req, res, next); + getUsers('users:online', 'inactive', '-inf', cutoff, req, res, next); }; -function getUsersByScore(set, section, min, max, req, res, callback) { - var page = parseInt(req.query.page, 10) || 1; - var resultsPerPage = 25; - var start = Math.max(0, page - 1) * resultsPerPage; - var count = 0; - - async.waterfall([ - function (next) { - async.parallel({ - count: function (next) { - db.sortedSetCount(set, min, max, next); - }, - uids: function (next) { - db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next); - } - }, next); - }, - function (results, next) { - count = results.count; - user.getUsers(results.uids, req.uid, next); - } - ], function(err, users) { - if (err) { - return callback(err); - } - users = users.filter(function(user) { - return user && parseInt(user.uid, 10); - }); - var data = { - users: users, - page: page, - pageCount: Math.ceil(count / resultsPerPage) - }; - data[section] = true; - render(req, res, data); - }); -} - -usersController.banned = function(req, res, next) { - getUsers('users:banned', 'banned', req, res, next); +usersController.banned = function (req, res, next) { + getUsers('users:banned', 'banned', undefined, undefined, req, res, next); }; -usersController.registrationQueue = function(req, res, next) { +usersController.registrationQueue = function (req, res, next) { var page = parseInt(req.query.page, 10) || 1; var itemsPerPage = 20; var start = (page - 1) * 20; var stop = start + itemsPerPage - 1; var invitations; + async.parallel({ - registrationQueueCount: function(next) { + registrationQueueCount: function (next) { db.sortedSetCard('registration:queue', next); }, - users: function(next) { + users: function (next) { user.getRegistrationQueue(start, stop, next); }, - invites: function(next) { + customHeaders: function (next) { + plugins.fireHook('filter:admin.registrationQueue.customHeaders', {headers: []}, next); + }, + invites: function (next) { async.waterfall([ - function(next) { + function (next) { user.getAllInvites(next); }, - function(_invitations, next) { + function (_invitations, next) { invitations = _invitations; - async.map(invitations, function(invites, next) { + async.map(invitations, function (invites, next) { user.getUserField(invites.uid, 'username', next); }, next); }, - function(usernames, next) { - invitations.forEach(function(invites, index) { + function (usernames, next) { + invitations.forEach(function (invites, index) { invites.username = usernames[index]; }); - async.map(invitations, function(invites, next) { + async.map(invitations, function (invites, next) { async.map(invites.invitations, user.getUsernameByEmail, next); }, next); }, - function(usernames, next) { - invitations.forEach(function(invites, index) { - invites.invitations = invites.invitations.map(function(email, i) { + function (usernames, next) { + invitations.forEach(function (invites, index) { + invites.invitations = invites.invitations.map(function (email, i) { return { email: email, username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i] @@ -121,35 +97,53 @@ usersController.registrationQueue = function(req, res, next) { } ], next); } - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } var pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage)); data.pagination = pagination.create(page, pageCount); + data.customHeaders = data.customHeaders.headers; res.render('admin/manage/registration', data); }); }; -function getUsers(set, section, req, res, next) { +function getUsers(set, section, min, max, req, res, next) { var page = parseInt(req.query.page, 10) || 1; - var resultsPerPage = 25; + var resultsPerPage = 50; var start = Math.max(0, page - 1) * resultsPerPage; var stop = start + resultsPerPage - 1; + var byScore = min !== undefined && max !== undefined; async.parallel({ - count: function(next) { - db.sortedSetCard(set, next); + count: function (next) { + if (byScore) { + db.sortedSetCount(set, min, max, next); + } else { + db.sortedSetCard(set, next); + } }, - users: function(next) { - user.getUsersFromSet(set, req.uid, start, stop, next); + users: function (next) { + async.waterfall([ + function (next) { + if (byScore) { + db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min, next); + } else { + user.getUidsFromSet(set, start, stop, next); + } + }, + function (uids, next) { + user.getUsersWithFields(uids, userFields, req.uid, next); + } + ], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } - results.users = results.users.filter(function(user) { + results.users = results.users.filter(function (user) { + user.email = validator.escape(String(user.email || '')); return user && parseInt(user.uid, 10); }); var data = { @@ -166,11 +160,23 @@ function render(req, res, data) { data.search_display = 'hidden'; data.pagination = pagination.create(data.page, data.pageCount, req.query); data.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1; + + var registrationType = meta.config.registrationType; + + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + res.render('admin/manage/users', data); } -usersController.getCSV = function(req, res, next) { - user.getUsersCSV(function(err, data) { +usersController.getCSV = function (req, res, next) { + events.log({ + type: 'getUsersCSV', + uid: req.user.uid, + ip: req.ip + }); + + user.getUsersCSV(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/admin/widgets.js b/src/controllers/admin/widgets.js index 8dd93fbead..c2d0d1e667 100644 --- a/src/controllers/admin/widgets.js +++ b/src/controllers/admin/widgets.js @@ -2,8 +2,8 @@ var widgetsController = {}; -widgetsController.get = function(req, res, next) { - require('../../widgets/admin').get(function(err, data) { +widgetsController.get = function (req, res, next) { + require('../../widgets/admin').get(function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/api.js b/src/controllers/api.js index 77ae7131ad..36261a23c1 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -12,16 +12,17 @@ var categories = require('../categories'); var privileges = require('../privileges'); var plugins = require('../plugins'); var widgets = require('../widgets'); +var accountHelpers = require('../controllers/accounts/helpers'); var apiController = {}; -apiController.getConfig = function(req, res, next) { +apiController.getConfig = function (req, res, next) { var config = {}; config.environment = process.env.NODE_ENV; config.relative_path = nconf.get('relative_path'); config.version = nconf.get('version'); - config.siteTitle = validator.escape(meta.config.title || meta.config.browserTitle || 'NodeBB'); - config.browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB'); + config.siteTitle = validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')); + config.browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); config.titleLayout = (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'); config.showSiteTitle = parseInt(meta.config.showSiteTitle, 10) === 1; config.minimumTitleLength = meta.config.minimumTitleLength; @@ -51,7 +52,7 @@ apiController.getConfig = function(req, res, next) { config['theme:id'] = meta.config['theme:id']; config['theme:src'] = meta.config['theme:src']; config.defaultLang = meta.config.defaultLang || 'en_GB'; - config.userLang = req.query.lang || config.defaultLang; + config.userLang = req.query.lang ? validator.escape(String(req.query.lang)) : config.defaultLang; config.loggedIn = !!req.user; config['cache-buster'] = meta.config['cache-buster'] || ''; config.requireEmailConfirmation = parseInt(meta.config.requireEmailConfirmation, 10) === 1; @@ -66,27 +67,22 @@ apiController.getConfig = function(req, res, next) { if (!req.user) { return next(null, config); } - user.getSettings(req.uid, function(err, settings) { - if (err) { - return next(err); - } - config.usePagination = settings.usePagination; - config.topicsPerPage = settings.topicsPerPage; - config.postsPerPage = settings.postsPerPage; - config.notificationSounds = settings.notificationSounds; - config.userLang = req.query.lang || settings.userLang || config.defaultLang; - config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; - config.topicPostSort = settings.topicPostSort || config.topicPostSort; - config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; - config.topicSearchEnabled = settings.topicSearchEnabled || false; - config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; - next(null, config); - }); + user.getSettings(req.uid, next); }, - function (config, next) { + function (settings, next) { + config.usePagination = settings.usePagination; + config.topicsPerPage = settings.topicsPerPage; + config.postsPerPage = settings.postsPerPage; + config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang; + config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; + config.topicPostSort = settings.topicPostSort || config.topicPostSort; + config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; + config.topicSearchEnabled = settings.topicSearchEnabled || false; + config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true; + config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; plugins.fireHook('filter:config.get', config, next); } - ], function(err, config) { + ], function (err, config) { if (err) { return next(err); } @@ -100,7 +96,7 @@ apiController.getConfig = function(req, res, next) { }; -apiController.renderWidgets = function(req, res, next) { +apiController.renderWidgets = function (req, res, next) { var areas = { template: req.query.template, locations: req.query.locations, @@ -116,10 +112,11 @@ apiController.renderWidgets = function(req, res, next) { template: areas.template, url: areas.url, locations: areas.locations, + isMobile: req.query.isMobile === 'true' }, req, res, - function(err, widgets) { + function (err, widgets) { if (err) { return next(err); } @@ -127,105 +124,158 @@ apiController.renderWidgets = function(req, res, next) { }); }; -apiController.getObject = function(req, res, next) { - apiController.getObjectByType(req.uid, req.params.type, req.params.id, function(err, results) { - if (err) { - return next(err); +apiController.getPostData = function (pid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.posts.get([pid], uid, next); + }, + post: function (next) { + posts.getPostData(pid, next); + } + }, function (err, results) { + if (err || !results.post) { + return callback(err); + } + + var post = results.post; + var privileges = results.privileges[0]; + + if (!privileges.read || !privileges['topics:read']) { + return callback(); } - res.json(results); + post.ip = privileges.isAdminOrMod ? post.ip : undefined; + var selfPost = uid && uid === parseInt(post.uid, 10); + if (post.deleted && !(privileges.isAdminOrMod || selfPost)) { + post.content = '[[topic:post_is_deleted]]'; + } + callback(null, post); }); }; -apiController.getObjectByType = function(uid, type, id, callback) { - var methods = { - post: { - canRead: privileges.posts.can, - data: posts.getPostData - }, - topic: { - canRead: privileges.topics.can, - data: topics.getTopicData +apiController.getTopicData = function (tid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.topics.get(tid, uid, next); }, - category: { - canRead: privileges.categories.can, - data: categories.getCategoryData + topic: function (next) { + topics.getTopicData(tid, next); + } + }, function (err, results) { + if (err || !results.topic) { + return callback(err); } - }; - if (!methods[type]) { - return callback(); - } + if (!results.privileges.read || !results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + return callback(); + } + callback(null, results.topic); + }); +}; - async.waterfall([ - function (next) { - methods[type].canRead('read', id, uid, next); +apiController.getCategoryData = function (cid, uid, callback) { + async.parallel({ + privileges: function (next) { + privileges.categories.get(cid, uid, next); }, - function (canRead, next) { - if (!canRead) { - return next(new Error('[[error:no-privileges]]')); - } - methods[type].data(id, next); + category: function (next) { + categories.getCategoryData(cid, next); } - ], callback); + }, function (err, results) { + if (err || !results.category) { + return callback(err); + } + + if (!results.privileges.read) { + return callback(); + } + callback(null, results.category); + }); }; -apiController.getUserByUID = function(req, res, next) { - var uid = req.params.uid ? req.params.uid : 0; - apiController.getUserDataByUID(req.uid, uid, function(err, data) { - if (err) { +apiController.getObject = function (req, res, next) { + var methods = { + post: apiController.getPostData, + topic: apiController.getTopicData, + category: apiController.getCategoryData + }; + var method = methods[req.params.type]; + if (!method) { + return next(); + } + method(req.params.id, req.uid, function (err, result) { + if (err || !result) { return next(err); } - res.json(data); + + res.json(result); }); }; -apiController.getUserByUsername = function(req, res, next) { - var username = req.params.username ? req.params.username : 0; - - apiController.getUserDataByUsername(req.uid, username, function(err, data) { +apiController.getCurrentUser = function (req, res, next) { + if (!req.uid) { + return res.status(401).json('not-authorized'); + } + async.waterfall([ + function (next) { + user.getUserField(req.uid, 'userslug', next); + }, + function (userslug, next) { + accountHelpers.getUserDataByUserSlug(userslug, req.uid, next); + } + ], function (err, userData) { if (err) { return next(err); } - res.json(data); + res.json(userData); }); }; -apiController.getUserByEmail = function(req, res, next) { - var email = req.params.email ? req.params.email : 0; +apiController.getUserByUID = function (req, res, next) { + byType('uid', req, res, next); +}; - apiController.getUserDataByEmail(req.uid, email, function(err, data) { - if (err) { +apiController.getUserByUsername = function (req, res, next) { + byType('username', req, res, next); +}; + +apiController.getUserByEmail = function (req, res, next) { + byType('email', req, res, next); +}; + +function byType(type, req, res, next) { + apiController.getUserDataByField(req.uid, type, req.params[type], function (err, data) { + if (err || !data) { return next(err); } res.json(data); }); -}; - -apiController.getUserDataByUsername = function(callerUid, username, callback) { - async.waterfall([ - function(next) { - user.getUidByUsername(username, next); - }, - function(uid, next) { - apiController.getUserDataByUID(callerUid, uid, next); - } - ], callback); -}; +} -apiController.getUserDataByEmail = function(callerUid, email, callback) { +apiController.getUserDataByField = function (callerUid, field, fieldValue, callback) { async.waterfall([ - function(next) { - user.getUidByEmail(email, next); + function (next) { + if (field === 'uid') { + next(null, fieldValue); + } else if (field === 'username') { + user.getUidByUsername(fieldValue, next); + } else if (field === 'email') { + user.getUidByEmail(fieldValue, next); + } else { + next(); + } }, - function(uid, next) { + function (uid, next) { + if (!uid) { + return next(); + } apiController.getUserDataByUID(callerUid, uid, next); } ], callback); }; -apiController.getUserDataByUID = function(callerUid, uid, callback) { +apiController.getUserDataByUID = function (callerUid, uid, callback) { if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) { return callback(new Error('[[error:no-privileges]]')); } @@ -237,7 +287,7 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) { async.parallel({ userData: async.apply(user.getUserData, uid), settings: async.apply(user.getSettings, uid) - }, function(err, results) { + }, function (err, results) { if (err || !results.userData) { return callback(err || new Error('[[error:no-user]]')); } @@ -249,8 +299,8 @@ apiController.getUserDataByUID = function(callerUid, uid, callback) { }); }; -apiController.getModerators = function(req, res, next) { - categories.getModerators(req.params.cid, function(err, moderators) { +apiController.getModerators = function (req, res, next) { + categories.getModerators(req.params.cid, function (err, moderators) { if (err) { return next(err); } @@ -259,7 +309,7 @@ apiController.getModerators = function(req, res, next) { }; -apiController.getRecentPosts = function(req, res, next) { +apiController.getRecentPosts = function (req, res, next) { posts.getRecentPosts(req.uid, 0, 19, req.params.term, function (err, data) { if (err) { return next(err); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 8db2c9e580..0111f62573 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -6,6 +6,7 @@ var passport = require('passport'); var nconf = require('nconf'); var validator = require('validator'); var _ = require('underscore'); +var url = require('url'); var db = require('../database'); var meta = require('../meta'); @@ -16,7 +17,7 @@ var Password = require('../password'); var authenticationController = {}; -authenticationController.register = function(req, res, next) { +authenticationController.register = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; if (registrationType === 'disabled') { @@ -32,14 +33,14 @@ authenticationController.register = function(req, res, next) { } async.waterfall([ - function(next) { - if (registrationType === 'invite-only') { + function (next) { + if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { user.verifyInvitation(userData, next); } else { next(); } }, - function(next) { + function (next) { if (!userData.email) { return next(new Error('[[error:invalid-email]]')); } @@ -54,18 +55,28 @@ authenticationController.register = function(req, res, next) { user.isPasswordValid(userData.password, next); }, - function(next) { + function (next) { res.locals.processLogin = true; // set it to false in plugin if you wish to just register only plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next); }, - function(data, next) { - if (registrationType === 'normal' || registrationType === 'invite-only') { + function (data, next) { + if (registrationType === 'normal' || registrationType === 'invite-only' || registrationType === 'admin-invite-only') { registerAndLoginUser(req, res, userData, next); } else if (registrationType === 'admin-approval') { addToApprovalQueue(req, userData, next); + } else if (registrationType === 'admin-approval-ip') { + db.sortedSetCard('ip:' + req.ip + ':uid', function (err, count) { + if (err) { + next(err); + } else if (count) { + addToApprovalQueue(req, userData, next); + } else { + registerAndLoginUser(req, res, userData, next); + } + }); } } - ], function(err, data) { + ], function (err, data) { if (err) { return res.status(400).send(err.message); } @@ -81,10 +92,31 @@ authenticationController.register = function(req, res, next) { function registerAndLoginUser(req, res, userData, callback) { var uid; async.waterfall([ - function(next) { + function (next) { + plugins.fireHook('filter:register.interstitial', { + userData: userData, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + // If interstitials are found, save registration attempt into session and abort + var deferRegistration = data.interstitials.length; + + if (!deferRegistration) { + return next(); + } else { + userData.register = true; + req.session.registration = userData; + return res.json({ referrer: nconf.get('relative_path') + '/register/complete' }); + } + }); + }, + function (next) { user.create(userData, next); }, - function(_uid, next) { + function (_uid, next) { uid = _uid; if (res.locals.processLogin) { authenticationController.doLogin(req, uid, next); @@ -92,7 +124,7 @@ function registerAndLoginUser(req, res, userData, callback) { next(); } }, - function(next) { + function (next) { user.deleteInvitationKey(userData.email); plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next); } @@ -101,20 +133,76 @@ function registerAndLoginUser(req, res, userData, callback) { function addToApprovalQueue(req, userData, callback) { async.waterfall([ - function(next) { + function (next) { userData.ip = req.ip; user.addToApprovalQueue(userData, next); }, - function(next) { + function (next) { next(null, {message: '[[register:registration-added-to-queue]]'}); } ], callback); } -authenticationController.login = function(req, res, next) { +authenticationController.registerComplete = function (req, res, next) { + // For the interstitials that respond, execute the callback with the form body + plugins.fireHook('filter:register.interstitial', { + userData: req.session.registration, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + var callbacks = data.interstitials.reduce(function (memo, cur) { + if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') { + memo.push(async.apply(cur.callback, req.session.registration, req.body)); + } + + return memo; + }, []); + + var done = function () { + delete req.session.registration; + + if (req.session.returnTo) { + res.redirect(req.session.returnTo); + } else { + res.redirect(nconf.get('relative_path') + '/'); + } + }; + + async.parallel(callbacks, function (err) { + if (err) { + req.flash('error', err.message); + return res.redirect(nconf.get('relative_path') + '/register/complete'); + } + + if (req.session.registration.register === true) { + res.locals.processLogin = true; + registerAndLoginUser(req, res, req.session.registration, done); + } else { + // Clear registration data in session + done(); + } + }); + }); +}; + +authenticationController.registerAbort = function (req, res) { + // End the session and redirect to home + req.session.destroy(function () { + res.redirect(nconf.get('relative_path') + '/'); + }); +}; + +authenticationController.login = function (req, res, next) { // Handle returnTo data if (req.body.hasOwnProperty('returnTo') && !req.session.returnTo) { - req.session.returnTo = req.body.returnTo; + // As req.body is data obtained via userland, it is untrusted, restrict to internal links only + var parsed = url.parse(req.body.returnTo); + var isInternal = utils.isInternalURI(url.parse(req.body.returnTo), nconf.get('url_parsed'), nconf.get('relative_path')); + + req.session.returnTo = isInternal ? req.body.returnTo : nconf.get('url'); } if (plugins.hasListeners('action:auth.overrideLogin')) { @@ -124,7 +212,7 @@ authenticationController.login = function(req, res, next) { 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) { + user.getUsernameByEmail(req.body.username, function (err, username) { if (err) { return next(err); } @@ -139,7 +227,7 @@ authenticationController.login = function(req, res, next) { }; function continueLogin(req, res, next) { - passport.authenticate('local', function(err, userData, info) { + passport.authenticate('local', function (err, userData, info) { if (err) { return res.status(403).send(err.message); } @@ -167,11 +255,15 @@ function continueLogin(req, res, next) { if (passwordExpiry && passwordExpiry < Date.now()) { winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy'); req.session.passwordExpired = true; - user.reset.generate(userData.uid, function(err, code) { + user.reset.generate(userData.uid, function (err, code) { + if (err) { + return res.status(403).send(err.message); + } + res.status(200).send(nconf.get('relative_path') + '/reset/' + code); }); } else { - authenticationController.doLogin(req, userData.uid, function(err) { + authenticationController.doLogin(req, userData.uid, function (err) { if (err) { return res.status(403).send(err.message); } @@ -189,12 +281,12 @@ function continueLogin(req, res, next) { })(req, res, next); } -authenticationController.doLogin = function(req, uid, callback) { +authenticationController.doLogin = function (req, uid, callback) { if (!uid) { return callback(); } - req.login({uid: uid}, function(err) { + req.login({uid: uid}, function (err) { if (err) { return callback(err); } @@ -203,11 +295,13 @@ authenticationController.doLogin = function(req, uid, callback) { }); }; -authenticationController.onSuccessfulLogin = function(req, uid, callback) { - callback = callback || function() {}; +authenticationController.onSuccessfulLogin = function (req, uid, callback) { + callback = callback || function () {}; var uuid = utils.generateUUID(); req.session.meta = {}; + delete req.session.forceLogin; + // Associate IP used during login with user account user.logIP(uid, req.ip); req.session.meta.ip = req.ip; @@ -228,8 +322,11 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) { }, function (next) { db.setObjectField('uid:' + uid + 'sessionUUID:sessionId', uuid, req.sessionID, next); + }, + function (next) { + user.updateLastOnlineTime(uid, next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -238,7 +335,7 @@ authenticationController.onSuccessfulLogin = function(req, uid, callback) { }); }; -authenticationController.localLogin = function(req, username, password, next) { +authenticationController.localLogin = function (req, username, password, next) { if (!username) { return next(new Error('[[error:invalid-username]]')); } @@ -262,11 +359,14 @@ authenticationController.localLogin = function(req, username, password, next) { }, function (next) { async.parallel({ - userData: function(next) { - db.getObjectFields('user:' + uid, ['password', 'banned', 'passwordExpiry'], next); + userData: function (next) { + db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next); }, - isAdmin: function(next) { + isAdmin: function (next) { user.isAdministrator(uid, next); + }, + banned: function (next) { + user.isBanned(uid, next); } }, next); }, @@ -278,13 +378,22 @@ authenticationController.localLogin = function(req, username, password, next) { if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) { return next(new Error('[[error:local-login-disabled]]')); } - if (!userData || !userData.password) { return next(new Error('[[error:invalid-user-data]]')); } - if (userData.banned && parseInt(userData.banned, 10) === 1) { - return next(new Error('[[error:user-banned]]')); + if (result.banned) { + // Retrieve ban reason and show error + return user.getLatestBanInfo(uid, function (err, banInfo) { + if (err) { + next(err); + } else if (banInfo.reason) { + next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]')); + } else { + next(new Error('[[error:user-banned]]')); + } + }); } + Password.compare(password, userData.password, next); }, function (passwordMatch, next) { @@ -297,18 +406,19 @@ authenticationController.localLogin = function(req, username, password, next) { ], next); }; -authenticationController.logout = function(req, res, next) { +authenticationController.logout = function (req, res, next) { if (req.user && parseInt(req.user.uid, 10) > 0 && req.sessionID) { var uid = parseInt(req.user.uid, 10); - user.auth.revokeSession(req.sessionID, uid, function(err) { + user.auth.revokeSession(req.sessionID, uid, function (err) { if (err) { return next(err); } req.logout(); + req.session.destroy(); + + user.setUserField(uid, 'lastonline', Date.now() - 300000); - // action:user.loggedOut deprecated in > v0.9.3 - plugins.fireHook('action:user.loggedOut', {req: req, res: res, uid: uid}); - plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function() { + plugins.fireHook('static:user.loggedOut', {req: req, res: res, uid: uid}, function () { res.status(200).send(''); }); }); diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 7ed087c704..9a18e7f1dd 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -1,25 +1,22 @@ "use strict"; - var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); var categories = require('../categories'); var meta = require('../meta'); -var plugins = require('../plugins'); - var helpers = require('./helpers'); var categoriesController = {}; -categoriesController.list = function(req, res, next) { +categoriesController.list = function (req, res, next) { res.locals.metaTags = [{ name: "title", - content: validator.escape(meta.config.title || 'NodeBB') + content: validator.escape(String(meta.config.title || 'NodeBB')) }, { name: "description", - content: validator.escape(meta.config.description || '') + content: validator.escape(String(meta.config.description || '')) }, { property: 'og:title', content: '[[pages:categories]]' @@ -28,14 +25,14 @@ categoriesController.list = function(req, res, next) { content: 'website' }]; - if (meta.config['brand:logo']) { - var brandLogo = meta.config['brand:logo']; - if (!brandLogo.startsWith('http')) { - brandLogo = nconf.get('url') + brandLogo; + var ogImage = meta.config['og:image'] || meta.config['brand:logo'] || ''; + if (ogImage) { + if (!ogImage.startsWith('http')) { + ogImage = nconf.get('url') + ogImage; } res.locals.metaTags.push({ property: 'og:image', - content: brandLogo + content: ogImage }); } @@ -51,33 +48,32 @@ categoriesController.list = function(req, res, next) { categories.flattenCategories(allCategories, categoryData); categories.getRecentTopicReplies(allCategories, req.uid, next); - }, - function (next) { - var data = { - title: '[[pages:categories]]', - categories: categoryData - }; - - if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) { - data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]); - } - - data.categories.forEach(function(category) { - if (category && Array.isArray(category.posts) && category.posts.length) { - category.teaser = { - url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index, - timestampISO: category.posts[0].timestampISO - }; - } - }); - - plugins.fireHook('filter:categories.build', {req: req, res: res, templateData: data}, next); } - ], function(err, data) { + ], function (err) { if (err) { return next(err); } - res.render('categories', data.templateData); + + var data = { + title: '[[pages:categories]]', + categories: categoryData + }; + + if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]); + } + + data.categories.forEach(function (category) { + if (category && Array.isArray(category.posts) && category.posts.length) { + category.teaser = { + url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index, + timestampISO: category.posts[0].timestampISO, + pid: category.posts[0].pid + }; + } + }); + + res.render('categories', data); }); }; diff --git a/src/controllers/category.js b/src/controllers/category.js index f80ad122a7..2a660ee63c 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -9,18 +9,18 @@ var privileges = require('../privileges'); var user = require('../user'); var categories = require('../categories'); var meta = require('../meta'); -var plugins = require('../plugins'); var pagination = require('../pagination'); var helpers = require('./helpers'); var utils = require('../../public/src/utils'); var categoryController = {}; -categoryController.get = function(req, res, callback) { +categoryController.get = function (req, res, callback) { var cid = req.params.category_id; var currentPage = parseInt(req.query.page, 10) || 1; var pageCount = 1; var userPrivileges; + var settings; if ((req.params.topic_index && !utils.isNumber(req.params.topic_index)) || !utils.isNumber(cid)) { return callback(); @@ -29,13 +29,13 @@ categoryController.get = function(req, res, callback) { async.waterfall([ function (next) { async.parallel({ - categoryData: function(next) { + categoryData: function (next) { categories.getCategoryFields(cid, ['slug', 'disabled', 'topic_count'], next); }, - privileges: function(next) { + privileges: function (next) { privileges.categories.get(cid, req.uid, next); }, - userSettings: function(next) { + userSettings: function (next) { user.getSettings(req.uid, next); } }, next); @@ -55,7 +55,7 @@ categoryController.get = function(req, res, callback) { return helpers.redirect(res, '/category/' + results.categoryData.slug); } - var settings = results.userSettings; + settings = results.userSettings; var topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0; var topicCount = parseInt(results.categoryData.topic_count, 10); pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); @@ -76,20 +76,21 @@ categoryController.get = function(req, res, callback) { topicIndex = 0; } - var set = 'cid:' + cid + ':tids', - reverse = false; - - if (settings.categoryTopicSort === 'newest_to_oldest') { + var set = 'cid:' + cid + ':tids'; + var reverse = false; + // `sort` qs has priority over user setting + var sort = req.query.sort || settings.categoryTopicSort; + if (sort === 'newest_to_oldest') { reverse = true; - } else if (settings.categoryTopicSort === 'most_posts') { + } else if (sort === 'most_posts') { reverse = true; set = 'cid:' + cid + ':tids:posts'; } - var start = (currentPage - 1) * settings.topicsPerPage + topicIndex, - stop = start + settings.topicsPerPage - 1; + var start = (currentPage - 1) * settings.topicsPerPage + topicIndex; + var stop = start + settings.topicsPerPage - 1; - next(null, { + var payload = { cid: cid, set: set, reverse: reverse, @@ -97,19 +98,24 @@ categoryController.get = function(req, res, callback) { stop: stop, uid: req.uid, settings: settings - }); - }, - function (payload, next) { - user.getUidByUserslug(req.query.author, function(err, uid) { - payload.targetUid = uid; - if (uid) { - payload.set = 'cid:' + cid + ':uid:' + uid + ':tids'; + }; + + async.waterfall([ + function (next) { + user.getUidByUserslug(req.query.author, next); + }, + function (uid, next) { + payload.targetUid = uid; + if (uid) { + payload.set = 'cid:' + cid + ':uid:' + uid + ':tids'; + } + + if (req.query.tag) { + payload.set = [payload.set, 'tag:' + req.query.tag + ':topics']; + } + categories.getCategoryById(payload, next); } - next(err, payload); - }); - }, - function (payload, next) { - categories.getCategoryById(payload, next); + ], next); }, function (categoryData, next) { @@ -126,7 +132,7 @@ categoryController.get = function(req, res, callback) { url: nconf.get('relative_path') + '/category/' + categoryData.slug } ]; - helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function(err, crumbs) { + helpers.buildCategoryBreadcrumbs(categoryData.parentCid, function (err, crumbs) { if (err) { return next(err); } @@ -140,68 +146,71 @@ categoryController.get = function(req, res, callback) { } var allCategories = []; categories.flattenCategories(allCategories, categoryData.children); - categories.getRecentTopicReplies(allCategories, req.uid, function(err) { + categories.getRecentTopicReplies(allCategories, req.uid, function (err) { next(err, categoryData); }); - }, - function (categoryData, next) { - categoryData.privileges = userPrivileges; - categoryData.showSelect = categoryData.privileges.editable; - - res.locals.metaTags = [ - { - name: 'title', - content: categoryData.name - }, - { - property: 'og:title', - content: categoryData.name - }, - { - name: 'description', - content: categoryData.description - }, - { - property: "og:type", - content: 'website' - } - ]; + } + ], function (err, categoryData) { + if (err) { + return callback(err); + } - if (categoryData.backgroundImage) { - res.locals.metaTags.push({ - name: 'og:image', - content: categoryData.backgroundImage - }); + categoryData.privileges = userPrivileges; + categoryData.showSelect = categoryData.privileges.editable; + + res.locals.metaTags = [ + { + name: 'title', + content: categoryData.name + }, + { + property: 'og:title', + content: categoryData.name + }, + { + name: 'description', + content: categoryData.description + }, + { + property: "og:type", + content: 'website' } + ]; - res.locals.linkTags = [ - { - rel: 'alternate', - type: 'application/rss+xml', - href: nconf.get('url') + '/category/' + cid + '.rss' - }, - { - rel: 'up', - href: nconf.get('url') - } - ]; - - categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss'; - categoryData.title = categoryData.name; - categoryData.pagination = pagination.create(currentPage, pageCount); - categoryData.pagination.rel.forEach(function(rel) { - rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href; - res.locals.linkTags.push(rel); + if (categoryData.backgroundImage) { + res.locals.metaTags.push({ + name: 'og:image', + content: categoryData.backgroundImage }); - - plugins.fireHook('filter:category.build', {req: req, res: res, templateData: categoryData}, next); } - ], function (err, data) { - if (err) { - return callback(err); + + res.locals.linkTags = [ + { + rel: 'alternate', + type: 'application/rss+xml', + href: nconf.get('url') + '/category/' + cid + '.rss' + }, + { + rel: 'up', + href: nconf.get('url') + } + ]; + + if (parseInt(req.uid, 10)) { + categories.markAsRead([cid], req.uid); } - res.render('category', data.templateData); + + categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + categoryData.rssFeedUrl = nconf.get('relative_path') + '/category/' + categoryData.cid + '.rss'; + categoryData.title = categoryData.name; + pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage)); + categoryData.pagination = pagination.create(currentPage, pageCount, req.query); + categoryData.pagination.rel.forEach(function (rel) { + rel.href = nconf.get('url') + '/category/' + categoryData.slug + rel.href; + res.locals.linkTags.push(rel); + }); + + res.render('category', categoryData); }); }; diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js index 3275c7929e..7e4fd1ffec 100644 --- a/src/controllers/globalmods.js +++ b/src/controllers/globalmods.js @@ -1,23 +1,12 @@ "use strict"; var user = require('../user'); -var adminFlagsController = require('./admin/flags'); var adminBlacklistController = require('./admin/blacklist'); var globalModsController = {}; -globalModsController.flagged = function(req, res, next) { - user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) { - if (err || !isAdminOrGlobalMod) { - return next(err); - } - - adminFlagsController.get(req, res, next); - }); -}; - -globalModsController.ipBlacklist = function(req, res, next) { - user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) { +globalModsController.ipBlacklist = function (req, res, next) { + user.isAdminOrGlobalMod(req.uid, function (err, isAdminOrGlobalMod) { if (err || !isAdminOrGlobalMod) { return next(err); } diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 837ba3b1f6..53b7064f59 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -1,19 +1,20 @@ "use strict"; -var async = require('async'), - nconf = require('nconf'), - validator = require('validator'), - meta = require('../meta'), - groups = require('../groups'), - user = require('../user'), - helpers = require('./helpers'), - plugins = require('../plugins'), - groupsController = {}; - -groupsController.list = function(req, res, next) { +var async = require('async'); +var nconf = require('nconf'); +var validator = require('validator'); + +var meta = require('../meta'); +var groups = require('../groups'); +var user = require('../user'); +var helpers = require('./helpers'); + +var groupsController = {}; + +groupsController.list = function (req, res, next) { var sort = req.query.sort || 'alpha'; - groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function(err, data) { + groupsController.getGroupsFromSet(req.uid, sort, 0, 14, function (err, data) { if (err) { return next(err); } @@ -23,7 +24,7 @@ groupsController.list = function(req, res, next) { }); }; -groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { +groupsController.getGroupsFromSet = function (uid, sort, start, stop, callback) { var set = 'groups:visible:name'; if (sort === 'count') { set = 'groups:visible:memberCount'; @@ -31,7 +32,7 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { set = 'groups:visible:createtime'; } - groups.getGroupsFromSet(set, uid, start, stop, function(err, groups) { + groups.getGroupsFromSet(set, uid, start, stop, function (err, groups) { if (err) { return callback(err); } @@ -44,25 +45,33 @@ groupsController.getGroupsFromSet = function(uid, sort, start, stop, callback) { }); }; -groupsController.details = function(req, res, callback) { +groupsController.details = function (req, res, callback) { + var groupName; async.waterfall([ - async.apply(groups.exists, res.locals.groupName), - function (exists, next) { - if (!exists) { + function (next) { + groups.getGroupNameByGroupSlug(req.params.slug, next); + }, + function (_groupName, next) { + groupName = _groupName; + if (!groupName) { return callback(); } - - groups.isHidden(res.locals.groupName, next); + async.parallel({ + exists: async.apply(groups.exists, groupName), + hidden: async.apply(groups.isHidden, groupName) + }, next); }, - function (hidden, next) { - if (!hidden) { + function (results, next) { + if (!results.exists) { + return callback(); + } + if (!results.hidden) { return next(); } - async.parallel({ - isMember: async.apply(groups.isMember, req.uid, res.locals.groupName), - isInvited: async.apply(groups.isInvited, req.uid, res.locals.groupName) - }, function(err, checks) { + isMember: async.apply(groups.isMember, req.uid, groupName), + isInvited: async.apply(groups.isInvited, req.uid, groupName) + }, function (err, checks) { if (err || checks.isMember || checks.isInvited) { return next(err); } @@ -71,55 +80,59 @@ groupsController.details = function(req, res, callback) { }, function (next) { async.parallel({ - group: function(next) { - groups.get(res.locals.groupName, { + group: function (next) { + groups.get(groupName, { uid: req.uid, truncateUserList: true, userListCount: 20 }, next); }, - posts: function(next) { - groups.getLatestMemberPosts(res.locals.groupName, 10, req.uid, next); + posts: function (next) { + groups.getLatestMemberPosts(groupName, 10, req.uid, next); }, - isAdmin: async.apply(user.isAdministrator, req.uid) + isAdmin:function (next) { + user.isAdministrator(req.uid, next); + }, + isGlobalMod: function (next) { + user.isGlobalModerator(req.uid, next); + } }, next); - }, - function (results, next) { - if (!results.group) { - return callback(); - } - results.title = '[[pages:group, ' + results.group.displayName + ']]'; - results.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:groups]]', url: '/groups' }, {text: results.group.displayName}]); - results.allowPrivateGroups = parseInt(meta.config.allowPrivateGroups, 10) === 1; - plugins.fireHook('filter:group.build', {req: req, res: res, templateData: results}, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } - res.render('groups/details', results.templateData); + if (!results.group) { + return callback(); + } + results.group.isOwner = results.group.isOwner || results.isAdmin || (results.isGlobalMod && !results.group.system); + results.title = '[[pages:group, ' + results.group.displayName + ']]'; + results.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:groups]]', url: '/groups' }, {text: results.group.displayName}]); + results.allowPrivateGroups = parseInt(meta.config.allowPrivateGroups, 10) === 1; + + res.render('groups/details', results); }); }; -groupsController.members = function(req, res, next) { +groupsController.members = function (req, res, next) { var groupName; async.waterfall([ - function(next) { + function (next) { groups.getGroupNameByGroupSlug(req.params.slug, next); }, - function(_groupName, next) { + function (_groupName, next) { groupName = _groupName; user.getUsersFromSet('group:' + groupName + ':members', req.uid, 0, 49, next); }, - ], function(err, users) { + ], function (err, users) { if (err || !groupName) { return next(err); } var breadcrumbs = helpers.buildBreadcrumbs([ {text: '[[pages:groups]]', url: '/groups' }, - {text: validator.escape(groupName), url: '/groups/' + req.params.slug}, + {text: validator.escape(String(groupName)), url: '/groups/' + req.params.slug}, {text: '[[groups:details.members]]'} ]); @@ -132,13 +145,13 @@ groupsController.members = function(req, res, next) { }); }; -groupsController.uploadCover = function(req, res, next) { +groupsController.uploadCover = function (req, res, next) { var params = JSON.parse(req.body.params); groups.updateCover(req.uid, { file: req.files.files[0].path, groupName: params.groupName - }, function(err, image) { + }, function (err, image) { if (err) { return next(err); } diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 058a1849b1..13a91aaa4c 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -1,43 +1,52 @@ 'use strict'; -var nconf = require('nconf'), - async = require('async'), - validator = require('validator'), +var nconf = require('nconf'); +var async = require('async'); +var validator = require('validator'); +var winston = require('winston'); - translator = require('../../public/src/modules/translator'), - categories = require('../categories'), - plugins = require('../plugins'), - meta = require('../meta'); +var categories = require('../categories'); +var plugins = require('../plugins'); +var meta = require('../meta'); var helpers = {}; -helpers.notAllowed = function(req, res, error) { - if (req.uid) { - if (res.locals.isAPI) { - res.status(403).json({ - path: req.path.replace(/^\/api/, ''), - loggedIn: !!req.uid, error: error, - title: '[[global:403.title]]' - }); - } else { - res.status(403).render('403', { - path: req.path, - loggedIn: !!req.uid, error: error, - title: '[[global:403.title]]' - }); +helpers.notAllowed = function (req, res, error) { + plugins.fireHook('filter:helpers.notAllowed', { + req: req, + res: res, + error: error + }, function (err, data) { + if (err) { + return winston.error(err); } - } else { - if (res.locals.isAPI) { - req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, ''); - res.status(401).json('not-authorized'); + if (req.uid) { + if (res.locals.isAPI) { + res.status(403).json({ + path: req.path.replace(/^\/api/, ''), + loggedIn: !!req.uid, error: error, + title: '[[global:403.title]]' + }); + } else { + res.status(403).render('403', { + path: req.path, + loggedIn: !!req.uid, error: error, + title: '[[global:403.title]]' + }); + } } else { - req.session.returnTo = nconf.get('relative_path') + req.url; - res.redirect(nconf.get('relative_path') + '/login'); + if (res.locals.isAPI) { + req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, ''); + res.status(401).json('not-authorized'); + } else { + req.session.returnTo = nconf.get('relative_path') + req.url; + res.redirect(nconf.get('relative_path') + '/login'); + } } - } + }); }; -helpers.redirect = function(res, url) { +helpers.redirect = function (res, url) { if (res.locals.isAPI) { res.status(308).json(url); } else { @@ -45,20 +54,20 @@ helpers.redirect = function(res, url) { } }; -helpers.buildCategoryBreadcrumbs = function(cid, callback) { +helpers.buildCategoryBreadcrumbs = function (cid, callback) { var breadcrumbs = []; - async.whilst(function() { + async.whilst(function () { return parseInt(cid, 10); - }, function(next) { - categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function(err, data) { + }, function (next) { + categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled'], function (err, data) { if (err) { return next(err); } if (!parseInt(data.disabled, 10)) { breadcrumbs.unshift({ - text: validator.escape(data.name), + text: validator.escape(String(data.name)), url: nconf.get('relative_path') + '/category/' + data.slug }); } @@ -66,11 +75,18 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) { cid = data.parentCid; next(); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } + if (!meta.config.homePageRoute && meta.config.homePageCustom) { + breadcrumbs.unshift({ + text: '[[global:header.categories]]', + url: nconf.get('relative_path') + '/categories' + }); + } + breadcrumbs.unshift({ text: '[[global:home]]', url: nconf.get('relative_path') + '/' @@ -80,7 +96,7 @@ helpers.buildCategoryBreadcrumbs = function(cid, callback) { }); }; -helpers.buildBreadcrumbs = function(crumbs) { +helpers.buildBreadcrumbs = function (crumbs) { var breadcrumbs = [ { text: '[[global:home]]', @@ -88,7 +104,7 @@ helpers.buildBreadcrumbs = function(crumbs) { } ]; - crumbs.forEach(function(crumb) { + crumbs.forEach(function (crumb) { if (crumb) { if (crumb.url) { crumb.url = nconf.get('relative_path') + crumb.url; @@ -100,14 +116,14 @@ helpers.buildBreadcrumbs = function(crumbs) { return breadcrumbs; }; -helpers.buildTitle = function(pageTitle) { +helpers.buildTitle = function (pageTitle) { var titleLayout = meta.config.titleLayout || '{pageTitle} | {browserTitle}'; - var browserTitle = validator.escape(meta.config.browserTitle || meta.config.title || 'NodeBB'); + var browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); pageTitle = pageTitle || ''; - var title = titleLayout.replace('{pageTitle}', function() { + var title = titleLayout.replace('{pageTitle}', function () { return pageTitle; - }).replace('{browserTitle}', function() { + }).replace('{browserTitle}', function () { return browserTitle; }); return title; diff --git a/src/controllers/index.js b/src/controllers/index.js index fa35523c2b..6f73886986 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -3,15 +3,16 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); +var winston = require('winston'); var meta = require('../meta'); var user = require('../user'); var plugins = require('../plugins'); -var sitemap = require('../sitemap'); var helpers = require('./helpers'); var Controllers = { topics: require('./topics'), + posts: require('./posts'), categories: require('./categories'), category: require('./category'), unread: require('./unread'), @@ -25,14 +26,16 @@ var Controllers = { authentication: require('./authentication'), api: require('./api'), admin: require('./admin'), - globalMods: require('./globalmods') + globalMods: require('./globalmods'), + mods: require('./mods'), + sitemap: require('./sitemap') }; -Controllers.home = function(req, res, next) { - var route = meta.config.homePageRoute || meta.config.homePageCustom || 'categories'; +Controllers.home = function (req, res, next) { + var route = meta.config.homePageRoute || (meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; - user.getSettings(req.uid, function(err, settings) { + user.getSettings(req.uid, function (err, settings) { if (err) { return next(err); } @@ -48,6 +51,8 @@ Controllers.home = function(req, res, next) { if (route === 'categories' || route === '/') { Controllers.categories.list(req, res, next); + } else if (route === 'unread') { + Controllers.unread.get(req, res, next); } else if (route === 'recent') { Controllers.recent.get(req, res, next); } else if (route === 'popular') { @@ -67,9 +72,9 @@ Controllers.home = function(req, res, next) { }); }; -Controllers.reset = function(req, res, next) { +Controllers.reset = function (req, res, next) { if (req.params.code) { - user.reset.validate(req.params.code, function(err, valid) { + user.reset.validate(req.params.code, function (err, valid) { if (err) { return next(err); } @@ -94,76 +99,144 @@ Controllers.reset = function(req, res, next) { }; -Controllers.login = function(req, res, next) { - var data = {}, - loginStrategies = require('../routes/authentication').getLoginStrategies(), - registrationType = meta.config.registrationType || 'normal'; +Controllers.login = function (req, res, next) { + var data = {}; + var loginStrategies = require('../routes/authentication').getLoginStrategies(); + var registrationType = meta.config.registrationType || 'normal'; + + var allowLoginWith = (meta.config.allowLoginWith || 'username-email'); + + var errorText; + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } else if (req.query.error) { + errorText = validator.escape(String(req.query.error)); + } data.alternate_logins = loginStrategies.length > 0; data.authentication = loginStrategies; data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1 || parseInt(req.query.local, 10) === 1; - data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval'; - data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]'; + data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval' || registrationType === 'admin-approval-ip'; + data.allowLoginWith = '[[login:' + allowLoginWith + ']]'; data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]); - data.error = req.flash('error')[0]; + data.error = req.flash('error')[0] || errorText; data.title = '[[pages:login]]'; - res.render('login', data); + if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { + if (res.locals.isAPI) { + return helpers.redirect(res, { + external: data.authentication[0].url + }); + } else { + return res.redirect(data.authentication[0].url); + } + } + if (req.uid) { + user.getUserFields(req.uid, ['username', 'email'], function (err, user) { + if (err) { + return next(err); + } + data.username = allowLoginWith === 'email' ? user.email : user.username; + data.alternate_logins = []; + res.render('login', data); + }); + } else { + res.render('login', data); + } + }; -Controllers.register = function(req, res, next) { +Controllers.register = function (req, res, next) { var registrationType = meta.config.registrationType || 'normal'; if (registrationType === 'disabled') { return next(); } + var errorText; + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } + async.waterfall([ - function(next) { + function (next) { if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { user.verifyInvitation(req.query, next); } else { next(); } }, - function(next) { + function (next) { plugins.fireHook('filter:parse.post', {postData: {content: meta.config.termsOfUse || ''}}, next); - }, - function(tos, next) { - var loginStrategies = require('../routes/authentication').getLoginStrategies(); - var data = { - 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', - 'alternate_logins': !!loginStrategies.length - }; - - data.authentication = loginStrategies; - - data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10); - data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10); - data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10); - data.termsOfUse = tos.postData.content; - data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); - data.regFormEntry = []; - data.error = req.flash('error')[0]; - data.title = '[[pages:register]]'; - - plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, next); } - ], function(err, data) { + ], function (err, termsOfUse) { if (err) { return next(err); } - res.render('register', data.templateData); + var loginStrategies = require('../routes/authentication').getLoginStrategies(); + var data = { + 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', + 'alternate_logins': !!loginStrategies.length + }; + + data.authentication = loginStrategies; + + data.minimumUsernameLength = parseInt(meta.config.minimumUsernameLength, 10); + data.maximumUsernameLength = parseInt(meta.config.maximumUsernameLength, 10); + data.minimumPasswordLength = parseInt(meta.config.minimumPasswordLength, 10); + data.termsOfUse = termsOfUse.postData.content; + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); + data.regFormEntry = []; + data.error = req.flash('error')[0] || errorText; + data.title = '[[pages:register]]'; + + res.render('register', data); }); }; -Controllers.compose = function(req, res, next) { +Controllers.registerInterstitial = function (req, res, next) { + if (!req.session.hasOwnProperty('registration')) { + return res.redirect(nconf.get('relative_path') + '/register'); + } + + plugins.fireHook('filter:register.interstitial', { + userData: req.session.registration, + interstitials: [] + }, function (err, data) { + if (err) { + return next(err); + } + + if (!data.interstitials.length) { + return next(); + } + + var renders = data.interstitials.map(function (interstitial) { + return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {}); + }); + var errors = req.flash('error'); + + async.parallel(renders, function (err, sections) { + if (err) { + return next(err); + } + + res.render('registerComplete', { + title: '[[pages:registration-complete]]', + errors: errors, + sections: sections + }); + }); + }); +}; + +Controllers.compose = function (req, res, next) { plugins.fireHook('filter:composer.build', { req: req, res: res, next: next, templateData: {} - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } @@ -179,7 +252,7 @@ Controllers.compose = function(req, res, next) { }); }; -Controllers.confirmEmail = function(req, res, next) { +Controllers.confirmEmail = function (req, res) { user.email.confirm(req.params.code, function (err) { res.render('confirm', { error: err ? err.message : '', @@ -188,61 +261,6 @@ Controllers.confirmEmail = function(req, res, next) { }); }; -Controllers.sitemap = {}; -Controllers.sitemap.render = function(req, res, next) { - sitemap.render(function(err, tplData) { - Controllers.render('sitemap', tplData, function(err, xml) { - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); - }); -}; - -Controllers.sitemap.getPages = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getPages(function(err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - -Controllers.sitemap.getCategories = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getCategories(function(err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - -Controllers.sitemap.getTopicPage = function(req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getTopicPage(parseInt(req.params[0], 10), function(err, xml) { - if (err) { - return next(err); - } else if (!xml) { - return next(); - } - - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; - Controllers.robots = function (req, res) { res.set('Content-Type', 'text/plain'); @@ -255,7 +273,7 @@ Controllers.robots = function (req, res) { } }; -Controllers.manifest = function(req, res) { +Controllers.manifest = function (req, res) { var manifest = { name: meta.config.title || 'NodeBB', start_url: nconf.get('relative_path') + '/', @@ -301,13 +319,13 @@ Controllers.manifest = function(req, res) { res.status(200).json(manifest); }; -Controllers.outgoing = function(req, res, next) { - var url = req.query.url, - data = { - url: validator.escape(url), - title: meta.config.title, - breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}]) - }; +Controllers.outgoing = function (req, res) { + var url = req.query.url || ''; + var data = { + outgoing: validator.escape(String(url)), + title: meta.config.title, + breadcrumbs: helpers.buildBreadcrumbs([{text: '[[notifications:outgoing_link]]'}]) + }; if (url) { res.render('outgoing', data); @@ -316,11 +334,115 @@ Controllers.outgoing = function(req, res, next) { } }; -Controllers.termsOfUse = function(req, res, next) { +Controllers.termsOfUse = function (req, res, next) { if (!meta.config.termsOfUse) { return next(); } res.render('tos', {termsOfUse: meta.config.termsOfUse}); }; +Controllers.ping = function (req, res) { + res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); +}; + +Controllers.handle404 = function (req, res) { + var relativePath = nconf.get('relative_path'); + var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json'); + var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); + + if (plugins.hasListeners('action:meta.override404')) { + return plugins.fireHook('action:meta.override404', { + req: req, + res: res, + error: {} + }); + } + + if (isClientScript.test(req.url)) { + res.type('text/javascript').status(200).send(''); + } else if (isLanguage.test(req.url)) { + res.status(200).json({}); + } else if (req.path.startsWith(relativePath + '/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') { + meta.errors.log404(req.path || ''); + res.sendStatus(404); + } else if (req.accepts('html')) { + if (process.env.NODE_ENV === 'development') { + winston.warn('Route requested but not found: ' + req.url); + } + + meta.errors.log404(req.path.replace(/^\/api/, '') || ''); + res.status(404); + + var path = String(req.path || ''); + + if (res.locals.isAPI) { + return res.json({path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]'}); + } + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('404', {path: validator.escape(path), title: '[[global:404.title]]'}); + }); + } else { + res.status(404).type('txt').send('Not found'); + } +}; + +Controllers.handleURIErrors = function (err, req, res, next) { + // Handle cases where malformed URIs are passed in + if (err instanceof URIError) { + var tidMatch = req.path.match(/^\/topic\/(\d+)\//); + var cidMatch = req.path.match(/^\/category\/(\d+)\//); + + if (tidMatch) { + res.redirect(nconf.get('relative_path') + tidMatch[0]); + } else if (cidMatch) { + res.redirect(nconf.get('relative_path') + cidMatch[0]); + } else { + winston.warn('[controller] Bad request: ' + req.path); + if (res.locals.isAPI) { + res.status(400).json({ + error: '[[global:400.title]]' + }); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('400', { error: validator.escape(String(err.message)) }); + }); + } + } + + return; + } else { + next(err); + } +}; + +Controllers.handleErrors = function (err, req, res, next) { + switch (err.code) { + case 'EBADCSRFTOKEN': + winston.error(req.path + '\n', err.message); + return res.sendStatus(403); + case 'blacklisted-ip': + return res.status(403).type('text/plain').send(err.message); + } + + if (parseInt(err.status, 10) === 302 && err.path) { + return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path); + } + + winston.error(req.path + '\n', err.stack); + + res.status(err.status || 500); + + var path = String(req.path || ''); + if (res.locals.isAPI) { + res.json({path: validator.escape(path), error: err.message}); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); + }); + } +}; + module.exports = Controllers; diff --git a/src/controllers/mods.js b/src/controllers/mods.js new file mode 100644 index 0000000000..0079412f87 --- /dev/null +++ b/src/controllers/mods.js @@ -0,0 +1,27 @@ +"use strict"; + +var async = require('async'); + +var user = require('../user'); +var adminFlagsController = require('./admin/flags'); + +var modsController = {}; + +modsController.flagged = function (req, res, next) { + async.parallel({ + isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid), + moderatedCids: async.apply(user.getModeratedCids, req.uid) + }, function (err, results) { + if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) { + return next(err); + } + + if (!results.isAdminOrGlobalMod && results.moderatedCids.length) { + res.locals.cids = results.moderatedCids; + } + + adminFlagsController.get(req, res, next); + }); +}; + +module.exports = modsController; diff --git a/src/controllers/popular.js b/src/controllers/popular.js index 813f3527b8..f38edd594c 100644 --- a/src/controllers/popular.js +++ b/src/controllers/popular.js @@ -1,15 +1,15 @@ 'use strict'; -var nconf = require('nconf'), - topics = require('../topics'), - plugins = require('../plugins'), - meta = require('../meta'), - helpers = require('./helpers'); +var nconf = require('nconf'); +var topics = require('../topics'); +var meta = require('../meta'); +var helpers = require('./helpers'); var popularController = {}; -var anonCache = {}, lastUpdateTime = 0; +var anonCache = {}; +var lastUpdateTime = 0; var terms = { daily: 'day', @@ -17,7 +17,7 @@ var terms = { monthly: 'month' }; -popularController.get = function(req, res, next) { +popularController.get = function (req, res, next) { var term = terms[req.params.term]; @@ -39,7 +39,7 @@ popularController.get = function(req, res, next) { } } - topics.getPopular(term, req.uid, meta.config.topicsPerList, function(err, topics) { + topics.getPopular(term, req.uid, meta.config.topicsPerList, function (err, topics) { if (err) { return next(err); } @@ -48,7 +48,8 @@ popularController.get = function(req, res, next) { topics: topics, 'feeds:disableRSS': parseInt(meta.config['feeds:disableRSS'], 10) === 1, rssFeedUrl: nconf.get('relative_path') + '/popular/' + (req.params.term || 'daily') + '.rss', - title: '[[pages:popular-' + term + ']]' + title: '[[pages:popular-' + term + ']]', + term: term }; if (req.path.startsWith('/api/popular') || req.path.startsWith('/popular')) { @@ -66,12 +67,7 @@ popularController.get = function(req, res, next) { lastUpdateTime = Date.now(); } - plugins.fireHook('filter:popular.build', {req: req, res: res, term: term, templateData: data}, function(err, data) { - if (err) { - return next(err); - } - res.render('popular', data.templateData); - }); + res.render('popular', data); }); }; diff --git a/src/controllers/posts.js b/src/controllers/posts.js new file mode 100644 index 0000000000..dae990e171 --- /dev/null +++ b/src/controllers/posts.js @@ -0,0 +1,24 @@ +"use strict"; + +var posts = require('../posts'); +var helpers = require('./helpers'); + +var postsController = {}; + +postsController.redirectToPost = function (req, res, callback) { + var pid = parseInt(req.params.pid, 10); + if (!pid) { + return callback(); + } + + posts.generatePostPath(pid, req.uid, function (err, path) { + if (err || !path) { + return callback(err); + } + + helpers.redirect(res, path); + }); +}; + + +module.exports = postsController; diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 242d26ac12..72c0f45721 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -1,38 +1,69 @@ 'use strict'; -var nconf = require('nconf'); var async = require('async'); +var nconf = require('nconf'); + +var db = require('../database'); +var privileges = require('../privileges'); +var user = require('../user'); var topics = require('../topics'); var meta = require('../meta'); var helpers = require('./helpers'); -var plugins = require('../plugins'); +var pagination = require('../pagination'); var recentController = {}; -recentController.get = function(req, res, next) { - - var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; +recentController.get = function (req, res, next) { + var page = parseInt(req.query.page, 10) || 1; + var pageCount = 1; + var stop = 0; + var topicCount = 0; + var settings; async.waterfall([ function (next) { - topics.getTopicsFromSet('topics:recent', req.uid, 0, stop, next); + async.parallel({ + settings: function (next) { + user.getSettings(req.uid, next); + }, + tids: function (next) { + db.getSortedSetRevRange('topics:recent', 0, 199, next); + } + }, next); + }, + function (results, next) { + settings = results.settings; + privileges.topics.filterTids('read', results.tids, req.uid, next); }, - function (data, next) { - data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; - data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss'; - data.title = '[[pages:recent]]'; - if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) { - data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]); - } - - plugins.fireHook('filter:recent.build', {req: req, res: res, templateData: data}, next); + function (tids, next) { + var start = Math.max(0, (page - 1) * settings.topicsPerPage); + stop = start + settings.topicsPerPage - 1; + + topicCount = tids.length; + pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); + tids = tids.slice(start, stop + 1); + + topics.getTopicsByTids(tids, req.uid, next); } - ], function(err, data) { + ], function (err, topics) { if (err) { return next(err); } - res.render('recent', data.templateData); + + var data = {}; + data.topics = topics; + data.nextStart = stop + 1; + data.set = 'topics:recent'; + data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss'; + data.title = '[[pages:recent]]'; + data.pagination = pagination.create(page, pageCount); + if (req.path.startsWith('/api/recent') || req.path.startsWith('/recent')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]); + } + + res.render('recent', data); }); }; diff --git a/src/controllers/search.js b/src/controllers/search.js index dee14e7c1f..5967cfc88e 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -1,19 +1,19 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - meta = require('../meta'), - plugins = require('../plugins'), - search = require('../search'), - categories = require('../categories'), - pagination = require('../pagination'), - helpers = require('./helpers'); +var meta = require('../meta'); +var plugins = require('../plugins'); +var search = require('../search'); +var categories = require('../categories'); +var pagination = require('../pagination'); +var helpers = require('./helpers'); var searchController = {}; -searchController.search = function(req, res, next) { +searchController.search = function (req, res, next) { if (!plugins.hasListeners('filter:search.query')) { return next(); } @@ -28,7 +28,7 @@ searchController.search = function(req, res, next) { } var data = { - query: req.params.term, + query: req.query.term, searchIn: req.query.in || 'posts', postedBy: req.query.by, categories: req.query.categories, @@ -45,69 +45,30 @@ searchController.search = function(req, res, next) { }; async.parallel({ - categories: async.apply(buildCategories, req.uid), + categories: async.apply(categories.buildForSelect, req.uid), search: async.apply(search.search, data) - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } + + var categoriesData = [ + {value: 'all', text: '[[unread:all_categories]]'}, + {value: 'watched', text: '[[category:watched-categories]]'} + ].concat(results.categories); + var searchData = results.search; - searchData.categories = results.categories; + searchData.categories = categoriesData; searchData.categoriesCount = results.categories.length; searchData.pagination = pagination.create(page, searchData.pageCount, req.query); searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts'; searchData.showAsTopics = req.query.showAs === 'topics'; searchData.title = '[[global:header.search]]'; searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]); - searchData.expandSearch = !req.params.term; + searchData.expandSearch = !req.query.term; - plugins.fireHook('filter:search.build', {data: data, results: searchData}, function(err, data) { - if (err) { - return next(err); - } - res.render('search', data.results); - }); + res.render('search', searchData); }); }; -function buildCategories(uid, callback) { - categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) { - if (err) { - return callback(err); - } - - var categoriesData = [ - {value: 'all', text: '[[unread:all_categories]]'}, - {value: 'watched', text: '[[category:watched-categories]]'} - ]; - - categories = categories.filter(function(category) { - return category && !category.link && !parseInt(category.parentCid, 10); - }); - - categories.forEach(function(category) { - recursive(category, categoriesData, ''); - }); - callback(null, categoriesData); - }); -} - - -function recursive(category, categoriesData, level) { - if (category.link) { - return; - } - - var bullet = level ? '• ' : ''; - - categoriesData.push({ - value: category.cid, - text: level + bullet + category.name - }); - - category.children.forEach(function(child) { - recursive(child, categoriesData, ' ' + level); - }); -} - module.exports = searchController; diff --git a/src/controllers/sitemap.js b/src/controllers/sitemap.js new file mode 100644 index 0000000000..42b0ae1076 --- /dev/null +++ b/src/controllers/sitemap.js @@ -0,0 +1,68 @@ +'use strict'; + +var sitemap = require('../sitemap'); +var meta = require('../meta'); + +var sitemapController = {}; +sitemapController.render = function (req, res, next) { + sitemap.render(function (err, tplData) { + if (err) { + return next(err); + } + + req.app.render('sitemap', tplData, function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); + }); +}; + +sitemapController.getPages = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getPages(function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +sitemapController.getCategories = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getCategories(function (err, xml) { + if (err) { + return next(err); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +sitemapController.getTopicPage = function (req, res, next) { + if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { + return next(); + } + + sitemap.getTopicPage(parseInt(req.params[0], 10), function (err, xml) { + if (err) { + return next(err); + } else if (!xml) { + return next(); + } + + res.header('Content-Type', 'application/xml'); + res.send(xml); + }); +}; + +module.exports = sitemapController; \ No newline at end of file diff --git a/src/controllers/tags.js b/src/controllers/tags.js index af8f6058c0..a433694220 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -5,15 +5,16 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); -var meta = require('../meta'); +var user = require('../user'); var topics = require('../topics'); +var pagination = require('../pagination'); var helpers = require('./helpers'); var tagsController = {}; -tagsController.getTag = function(req, res, next) { - var tag = validator.escape(req.params.tag); - var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; +tagsController.getTag = function (req, res, next) { + var tag = validator.escape(String(req.params.tag)); + var page = parseInt(req.query.page, 10) || 1; var templateData = { topics: [], @@ -21,20 +22,34 @@ tagsController.getTag = function(req, res, next) { breadcrumbs: helpers.buildBreadcrumbs([{text: '[[tags:tags]]', url: '/tags'}, {text: tag}]), title: '[[pages:tag, ' + tag + ']]' }; - + var settings; + var topicCount = 0; async.waterfall([ function (next) { - topics.getTagTids(req.params.tag, 0, stop, next); + user.getSettings(req.uid, next); + }, + function (_settings, next) { + settings = _settings; + var start = Math.max(0, (page - 1) * settings.topicsPerPage); + var stop = start + settings.topicsPerPage - 1; + templateData.nextStart = stop + 1; + async.parallel({ + topicCount: function (next) { + topics.getTagTopicCount(tag, next); + }, + tids: function (next) { + topics.getTagTids(req.params.tag, start, stop, next); + } + }, next); }, - function (tids, next) { - if (Array.isArray(tids) && !tids.length) { - topics.deleteTag(req.params.tag); + function (results, next) { + if (Array.isArray(results.tids) && !results.tids.length) { return res.render('tag', templateData); } - - topics.getTopics(tids, req.uid, next); + topicCount = results.topicCount; + topics.getTopics(results.tids, req.uid, next); } - ], function(err, topics) { + ], function (err, topics) { if (err) { return next(err); } @@ -54,20 +69,20 @@ tagsController.getTag = function(req, res, next) { } ]; templateData.topics = topics; - templateData.nextStart = stop + 1; + + var pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); + templateData.pagination = pagination.create(page, pageCount); res.render('tag', templateData); }); }; -tagsController.getTags = function(req, res, next) { - topics.getTags(0, 99, function(err, tags) { +tagsController.getTags = function (req, res, next) { + topics.getTags(0, 99, function (err, tags) { if (err) { return next(err); } - tags = tags.filter(function(tag) { - return tag && tag.score > 0; - }); + tags = tags.filter(Boolean); var data = { tags: tags, nextStart: 100, diff --git a/src/controllers/topics.js b/src/controllers/topics.js index c3c2847778..c739052937 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -17,9 +17,8 @@ var utils = require('../../public/src/utils'); var topicsController = {}; -topicsController.get = function(req, res, callback) { +topicsController.get = function (req, res, callback) { var tid = req.params.topic_id; - var sort = req.query.sort; var currentPage = parseInt(req.query.page, 10) || 1; var pageCount = 1; var userPrivileges; @@ -32,13 +31,13 @@ topicsController.get = function(req, res, callback) { async.waterfall([ function (next) { async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.topics.get(tid, req.uid, next); }, - settings: function(next) { + settings: function (next) { user.getSettings(req.uid, next); }, - topic: function(next) { + topic: function (next) { topics.getTopicData(tid, next); } }, next); @@ -50,14 +49,17 @@ topicsController.get = function(req, res, callback) { userPrivileges = results.privileges; - if (!userPrivileges.read || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { + if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { return helpers.notAllowed(req, res); } if (!res.locals.isAPI && (!req.params.slug || results.topic.slug !== tid + '/' + req.params.slug) && (results.topic.slug && results.topic.slug !== tid + '/')) { var url = '/topic/' + results.topic.slug; if (req.params.post_index){ - url += '/'+req.params.post_index; + url += '/' + req.params.post_index; + } + if (currentPage > 1) { + url += '?page=' + currentPage; } return helpers.redirect(res, url); } @@ -76,18 +78,13 @@ topicsController.get = function(req, res, callback) { var set = 'tid:' + tid + ':posts'; var reverse = false; - // `sort` qs has priority over user setting + var sort = req.query.sort || settings.topicPostSort; if (sort === 'newest_to_oldest') { reverse = true; } else if (sort === 'most_votes') { reverse = true; set = 'tid:' + tid + ':posts:votes'; - } else if (settings.topicPostSort === 'newest_to_oldest') { - reverse = true; - } else if (settings.topicPostSort === 'most_votes') { - reverse = true; - set = 'tid:' + tid + ':posts:votes'; } var postIndex = 0; @@ -97,7 +94,9 @@ topicsController.get = function(req, res, callback) { req.params.post_index = 0; } if (!settings.usePagination) { - currentPage = 1; + if (req.params.post_index !== 0) { + currentPage = 1; + } if (reverse) { postIndex = Math.max(0, postCount - (req.params.post_index || postCount) - Math.ceil(settings.postsPerPage / 2)); } else { @@ -140,7 +139,7 @@ topicsController.get = function(req, res, callback) { } ]; - helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function(err, crumbs) { + helpers.buildCategoryBreadcrumbs(data.topicData.category.parentCid, function (err, crumbs) { if (err) { return next(err); } @@ -150,7 +149,7 @@ topicsController.get = function(req, res, callback) { }, function (topicData, next) { function findPost(index) { - for(var i=0; i<topicData.posts.length; ++i) { + for(var i = 0; i < topicData.posts.length; ++i) { if (parseInt(topicData.posts[i].index, 10) === parseInt(index, 10)) { return topicData.posts[i]; } @@ -170,8 +169,10 @@ topicsController.get = function(req, res, callback) { var ogImageUrl = ''; if (topicData.thumb) { ogImageUrl = topicData.thumb; - } else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture){ + } else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) { ogImageUrl = postAtIndex.user.picture; + } else if (meta.config['og:image']) { + ogImageUrl = meta.config['og:image']; } else if (meta.config['brand:logo']) { ogImageUrl = meta.config['brand:logo']; } else { @@ -261,10 +262,13 @@ topicsController.get = function(req, res, callback) { data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + data.bookmarkThreshold = parseInt(meta.config.bookmarkThreshold, 10) || 5; + data.postEditDuration = parseInt(meta.config.postEditDuration, 10) || 0; + data.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; data.scrollToMyPost = settings.scrollToMyPost; data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss'; - data.pagination = pagination.create(currentPage, pageCount); - data.pagination.rel.forEach(function(rel) { + data.pagination = pagination.create(currentPage, pageCount, req.query); + data.pagination.rel.forEach(function (rel) { rel.href = nconf.get('url') + '/topic/' + data.slug + rel.href; res.locals.linkTags.push(rel); }); @@ -276,27 +280,22 @@ topicsController.get = function(req, res, callback) { } if (req.uid) { - topics.markAsRead([tid], req.uid, function(err, markedRead) { + topics.markAsRead([tid], req.uid, function (err, markedRead) { if (err) { return callback(err); } if (markedRead) { topics.pushUnreadCount(req.uid); - topics.markTopicNotificationsRead(tid, req.uid); + topics.markTopicNotificationsRead([tid], req.uid); } }); } - plugins.fireHook('filter:topic.build', {req: req, res: res, templateData: data}, function(err, data) { - if (err) { - return callback(err); - } - res.render('topic', data.templateData); - }); + res.render('topic', data); }); }; -topicsController.teaser = function(req, res, next) { +topicsController.teaser = function (req, res, next) { var tid = req.params.topic_id; if (!utils.isNumber(tid)) { @@ -304,22 +303,22 @@ topicsController.teaser = function(req, res, next) { } async.waterfall([ - function(next) { + function (next) { privileges.topics.can('read', tid, req.uid, next); }, - function(canRead, next) { + function (canRead, next) { if (!canRead) { return res.status(403).json('[[error:no-privileges]]'); } topics.getLatestUndeletedPid(tid, next); }, - function(pid, next) { + function (pid, next) { if (!pid) { return res.status(404).json('not-found'); } posts.getPostSummaryByPids([pid], req.uid, {stripTags: false}, next); } - ], function(err, posts) { + ], function (err, posts) { if (err) { return next(err); } @@ -331,5 +330,37 @@ topicsController.teaser = function(req, res, next) { }); }; +topicsController.pagination = function (req, res, callback) { + var tid = req.params.topic_id; + var currentPage = parseInt(req.query.page, 10) || 1; + + if (!utils.isNumber(tid)) { + return callback(); + } + + async.parallel({ + privileges: async.apply(privileges.topics.get, tid, req.uid), + settings: async.apply(user.getSettings, req.uid), + topic: async.apply(topics.getTopicData, tid) + }, function (err, results) { + if (err || !results.topic) { + return callback(err); + } + + if (!results.privileges.read || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + return helpers.notAllowed(req, res); + } + + var postCount = parseInt(results.topic.postcount, 10); + var pageCount = Math.max(1, Math.ceil((postCount - 1) / results.settings.postsPerPage)); + + var paginationData = pagination.create(currentPage, pageCount); + paginationData.rel.forEach(function (rel) { + rel.href = nconf.get('url') + '/topic/' + results.topic.slug + rel.href; + }); + + res.json(paginationData); + }); +}; module.exports = topicsController; diff --git a/src/controllers/unread.js b/src/controllers/unread.js index d81774661f..1c5bafcbae 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -2,69 +2,149 @@ 'use strict'; var async = require('async'); -var meta = require('../meta'); +var querystring = require('querystring'); +var validator = require('validator'); + var categories = require('../categories'); var privileges = require('../privileges'); -var user = require('../user') +var pagination = require('../pagination'); +var user = require('../user'); var topics = require('../topics'); var helpers = require('./helpers'); -var plugins = require('../plugins'); var unreadController = {}; -unreadController.get = function(req, res, next) { - var stop = (parseInt(meta.config.topicsPerList, 10) || 20) - 1; +var validFilter = {'': true, 'new': true, 'watched': true}; + +unreadController.get = function (req, res, next) { + var page = parseInt(req.query.page, 10) || 1; var results; var cid = req.query.cid; + var filter = req.params.filter || ''; + if (!validFilter[filter]) { + return next(); + } + var settings; async.waterfall([ - function(next) { + function (next) { async.parallel({ - watchedCategories: function(next) { - user.getWatchedCategories(req.uid, next); + watchedCategories: function (next) { + getWatchedCategories(req.uid, cid, next); }, - unreadTopics: function(next) { - topics.getUnreadTopics(cid, req.uid, 0, stop, next); + settings: function (next) { + user.getSettings(req.uid, next); } }, next); }, - function(_results, next) { + function (_results, next) { results = _results; + settings = results.settings; + var start = Math.max(0, (page - 1) * settings.topicsPerPage); + var stop = start + settings.topicsPerPage - 1; + topics.getUnreadTopics(cid, req.uid, start, stop, filter, next); + } + ], function (err, data) { + if (err) { + return next(err); + } + + data.pageCount = Math.max(1, Math.ceil(data.topicCount / settings.topicsPerPage)); + data.pagination = pagination.create(page, data.pageCount, req.query); + + if (settings.usePagination && (page < 1 || page > data.pageCount)) { + req.query.page = Math.max(1, Math.min(data.pageCount, page)); + return helpers.redirect(res, '/unread?' + querystring.stringify(req.query)); + } + + data.categories = results.watchedCategories.categories; + data.selectedCategory = results.watchedCategories.selectedCategory; + + if (req.path.startsWith('/api/unread') || req.path.startsWith('/unread')) { + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[unread:title]]'}]); + } + + data.title = '[[pages:unread]]'; + data.filters = [{ + name: '[[unread:all-topics]]', + url: 'unread', + selected: filter === '', + filter: '' + }, { + name: '[[unread:new-topics]]', + url: 'unread/new', + selected: filter === 'new', + filter: 'new' + }, { + name: '[[unread:watched-topics]]', + url: 'unread/watched', + selected: filter === 'watched', + filter: 'watched' + }]; + + data.selectedFilter = data.filters.filter(function (filter) { + return filter && filter.selected; + })[0]; + + data.querystring = cid ? ('?cid=' + validator.escape(String(cid))) : ''; - privileges.categories.filterCids('read', results.watchedCategories, req.uid, next); + res.render('unread', data); + }); +}; + +function getWatchedCategories(uid, selectedCid, callback) { + async.waterfall([ + function (next) { + user.getWatchedCategories(uid, next); }, - function(cids, next) { - categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor'], next); + function (cids, next) { + privileges.categories.filterCids('read', cids, uid, next); }, - function(categories, next) { - categories = categories.filter(function(category) { + function (cids, next) { + categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor', 'parentCid'], next); + }, + function (categoryData, next) { + categoryData = categoryData.filter(function (category) { return category && !category.link; }); - categories.forEach(function(category) { - category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); + + var selectedCategory; + categoryData.forEach(function (category) { + category.selected = parseInt(category.cid, 10) === parseInt(selectedCid, 10); if (category.selected) { - results.unreadTopics.selectedCategory = category; + selectedCategory = category; } }); - results.unreadTopics.categories = categories; - results.unreadTopics.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[unread:title]]'}]); - results.unreadTopics.title = '[[pages:unread]]'; + var categoriesData = []; + var tree = categories.getTree(categoryData, 0); - plugins.fireHook('filter:unread.build', {req: req, res: res, templateData: results.unreadTopics}, next); - } - ], function(err, data) { - if (err) { - return next(err); + tree.forEach(function (category) { + recursive(category, categoriesData, ''); + }); + + next(null, {categories: categoriesData, selectedCategory: selectedCategory}); } + ], callback); +} - res.render('unread', data.templateData); +function recursive(category, categoriesData, level) { + category.level = level; + categoriesData.push(category); + + category.children.forEach(function (child) { + recursive(child, categoriesData, ' ' + level); }); -}; +} + +unreadController.unreadTotal = function (req, res, next) { + var filter = req.params.filter || ''; + if (!validFilter[filter]) { + return next(); + } -unreadController.unreadTotal = function(req, res, next) { - topics.getTotalUnread(req.uid, function (err, data) { + topics.getTotalUnread(req.uid, filter, function (err, data) { if (err) { return next(err); } diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 59253c5666..f2896ec8f6 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -6,22 +6,19 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); var winston = require('winston'); +var mime = require('mime'); var meta = require('../meta'); var file = require('../file'); var plugins = require('../plugins'); var image = require('../image'); +var privileges = require('../privileges'); var uploadsController = {}; -uploadsController.upload = function(req, res, filesIterator, next) { +uploadsController.upload = function (req, res, filesIterator) { var files = req.files.files; - if (!req.user && meta.config.allowGuestUploads !== '1') { - deleteTempFiles(files); - return res.status(403).json('[[error:guest-upload-disabled]]'); - } - if (!Array.isArray(files)) { return res.status(500).json('invalid files'); } @@ -30,7 +27,7 @@ uploadsController.upload = function(req, res, filesIterator, next) { files = files[0]; } - async.map(files, filesIterator, function(err, images) { + async.map(files, filesIterator, function (err, images) { deleteTempFiles(files); if (err) { @@ -43,39 +40,104 @@ uploadsController.upload = function(req, res, filesIterator, next) { }); }; -uploadsController.uploadPost = function(req, res, next) { - uploadsController.upload(req, res, function(uploadedFile, next) { +uploadsController.uploadPost = function (req, res, next) { + uploadsController.upload(req, res, function (uploadedFile, next) { var isImage = uploadedFile.type.match(/image./); - if (isImage && plugins.hasListeners('filter:uploadImage')) { - return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, next); + if (isImage) { + uploadAsImage(req, uploadedFile, next); + } else { + uploadAsFile(req, uploadedFile, next); } - - async.waterfall([ - function(next) { - if (isImage) { - file.isFileTypeAllowed(uploadedFile.path, next); - } else { - next(); - } - }, - function (next) { - if (parseInt(meta.config.allowFileUploads, 10) !== 1) { - return next(new Error('[[error:uploads-are-disabled]]')); - } - uploadFile(req.uid, uploadedFile, next); - } - ], next); }, next); }; -uploadsController.uploadThumb = function(req, res, next) { +function uploadAsImage(req, uploadedFile, callback) { + async.waterfall([ + function (next) { + privileges.categories.can('upload:post:image', req.body.cid, req.uid, next); + }, + function (canUpload, next) { + if (!canUpload) { + return next(new Error('[[error:no-privileges]]')); + } + if (plugins.hasListeners('filter:uploadImage')) { + return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: req.uid}, callback); + } + file.isFileTypeAllowed(uploadedFile.path, next); + }, + function (next) { + uploadFile(req.uid, uploadedFile, next); + }, + function (fileObj, next) { + if (parseInt(meta.config.maximumImageWidth, 10) === 0) { + return next(null, fileObj); + } + + resizeImage(fileObj, next); + } + ], callback); +} + +function uploadAsFile(req, uploadedFile, callback) { + async.waterfall([ + function (next) { + privileges.categories.can('upload:post:file', req.body.cid, req.uid, next); + }, + function (canUpload, next) { + if (!canUpload) { + return next(new Error('[[error:no-privileges]]')); + } + if (parseInt(meta.config.allowFileUploads, 10) !== 1) { + return next(new Error('[[error:uploads-are-disabled]]')); + } + uploadFile(req.uid, uploadedFile, next); + } + ], callback); +} + +function resizeImage(fileObj, callback) { + async.waterfall([ + function (next) { + image.size(fileObj.path, next); + }, + function (imageData, next) { + if (imageData.width < (parseInt(meta.config.maximumImageWidth, 10) || 760)) { + return callback(null, fileObj); + } + + var dirname = path.dirname(fileObj.path); + var extname = path.extname(fileObj.path); + var basename = path.basename(fileObj.path, extname); + + image.resizeImage({ + path: fileObj.path, + target: path.join(dirname, basename + '-resized' + extname), + extension: extname, + width: parseInt(meta.config.maximumImageWidth, 10) || 760 + }, next); + }, + function (next) { + + // Return the resized version to the composer/postData + var dirname = path.dirname(fileObj.url); + var extname = path.extname(fileObj.url); + var basename = path.basename(fileObj.url, extname); + + fileObj.url = path.join(dirname, basename + '-resized' + extname); + + next(null, fileObj); + } + ], callback); +} + +uploadsController.uploadThumb = function (req, res, next) { if (parseInt(meta.config.allowTopicsThumbnail, 10) !== 1) { deleteTempFiles(req.files.files); return next(new Error('[[error:topic-thumbnails-are-disabled]]')); } - uploadsController.upload(req, res, function(uploadedFile, next) { - file.isFileTypeAllowed(uploadedFile.path, function(err) { + uploadsController.upload(req, res, function (uploadedFile, next) { + file.isFileTypeAllowed(uploadedFile.path, function (err) { if (err) { return next(err); } @@ -90,7 +152,7 @@ uploadsController.uploadThumb = function(req, res, next) { extension: path.extname(uploadedFile.name), width: size, height: size - }, function(err) { + }, function (err) { if (err) { return next(err); } @@ -105,7 +167,7 @@ uploadsController.uploadThumb = function(req, res, next) { }, next); }; -uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) { +uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) { if (plugins.hasListeners('filter:uploadImage')) { return plugins.fireHook('filter:uploadImage', {image: uploadedFile, uid: uid}, callback); } @@ -114,7 +176,7 @@ uploadsController.uploadGroupCover = function(uid, uploadedFile, callback) { return plugins.fireHook('filter:uploadFile', {file: uploadedFile, uid: uid}, callback); } - file.isFileTypeAllowed(uploadedFile.path, function(err) { + file.isFileTypeAllowed(uploadedFile.path, function (err) { if (err) { return callback(err); } @@ -138,6 +200,9 @@ function uploadFile(uid, uploadedFile, callback) { if (meta.config.hasOwnProperty('allowedFileExtensions')) { var allowed = file.allowedExtensions(); var extension = path.extname(uploadedFile.name); + if (!extension) { + extension = '.' + mime.extension(uploadedFile.type); + } if (allowed.length > 0 && allowed.indexOf(extension) === -1) { return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]')); } @@ -147,24 +212,31 @@ function uploadFile(uid, uploadedFile, callback) { } function saveFileToLocal(uploadedFile, callback) { + var extension = path.extname(uploadedFile.name); + if (!extension && uploadedFile.type) { + extension = '.' + mime.extension(uploadedFile.type); + } + var filename = uploadedFile.name || 'upload'; - filename = Date.now() + '-' + validator.escape(filename).substr(0, 255); - file.saveFileToLocal(filename, 'files', uploadedFile.path, function(err, upload) { + filename = Date.now() + '-' + validator.escape(filename.replace(extension, '')).substr(0, 255) + extension; + + file.saveFileToLocal(filename, 'files', uploadedFile.path, function (err, upload) { if (err) { return callback(err); } callback(null, { url: nconf.get('relative_path') + upload.url, + path: upload.path, name: uploadedFile.name }); }); } function deleteTempFiles(files) { - async.each(files, function(file, next) { - fs.unlink(file.path, function(err) { + async.each(files, function (file, next) { + fs.unlink(file.path, function (err) { if (err) { winston.error(err); } diff --git a/src/controllers/users.js b/src/controllers/users.js index 31ac0bd1cc..baf18a5b64 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -5,29 +5,79 @@ var user = require('../user'); var meta = require('../meta'); var pagination = require('../pagination'); -var plugins = require('../plugins'); var db = require('../database'); var helpers = require('./helpers'); var usersController = {}; -usersController.getOnlineUsers = function(req, res, next) { + +usersController.index = function (req, res, next) { + var section = req.query.section || 'joindate'; + var sectionToController = { + joindate: usersController.getUsersSortedByJoinDate, + online: usersController.getOnlineUsers, + 'sort-posts': usersController.getUsersSortedByPosts, + 'sort-reputation': usersController.getUsersSortedByReputation, + banned: usersController.getBannedUsers, + flagged: usersController.getFlaggedUsers + }; + + if (req.query.term) { + usersController.search(req, res, next); + } else if (sectionToController[section]) { + sectionToController[section](req, res, next); + } else { + usersController.getUsersSortedByJoinDate(req, res, next); + } +}; + +usersController.search = function (req, res, next) { + async.parallel({ + search: function (next) { + user.search({ + query: req.query.term, + searchBy: req.query.searchBy || 'username', + page: req.query.page || 1, + sortBy: req.query.sortBy, + onlineOnly: req.query.onlineOnly === 'true', + bannedOnly: req.query.bannedOnly === 'true', + flaggedOnly: req.query.flaggedOnly === 'true' + }, next); + }, + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(req.uid, next); + } + }, function (err, results) { + if (err) { + return next(err); + } + + var section = req.query.section || 'joindate'; + + results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod; + results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query); + results.search['section_' + section] = true; + render(req, res, results.search, next); + }); +}; + +usersController.getOnlineUsers = function (req, res, next) { async.parallel({ - users: function(next) { - usersController.getUsers('users:online', req.uid, req.query.page, next); + users: function (next) { + usersController.getUsers('users:online', req.uid, req.query, next); }, - guests: function(next) { + guests: function (next) { require('../socket.io/admin/rooms').getTotalGuestCount(next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } var userData = results.users; var hiddenCount = 0; if (!userData.isAdminOrGlobalMod) { - userData.users = userData.users.filter(function(user) { + userData.users = userData.users.filter(function (user) { if (user && user.status === 'offline') { hiddenCount ++; } @@ -41,23 +91,23 @@ usersController.getOnlineUsers = function(req, res, next) { }); }; -usersController.getUsersSortedByPosts = function(req, res, next) { +usersController.getUsersSortedByPosts = function (req, res, next) { usersController.renderUsersPage('users:postcount', req, res, next); }; -usersController.getUsersSortedByReputation = function(req, res, next) { +usersController.getUsersSortedByReputation = function (req, res, next) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return next(); } usersController.renderUsersPage('users:reputation', req, res, next); }; -usersController.getUsersSortedByJoinDate = function(req, res, next) { +usersController.getUsersSortedByJoinDate = function (req, res, next) { usersController.renderUsersPage('users:joindate', req, res, next); }; -usersController.getBannedUsers = function(req, res, next) { - usersController.getUsers('users:banned', req.uid, req.query.page, function(err, userData) { +usersController.getBannedUsers = function (req, res, next) { + usersController.getUsers('users:banned', req.uid, req.query, function (err, userData) { if (err) { return next(err); } @@ -70,93 +120,103 @@ usersController.getBannedUsers = function(req, res, next) { }); }; -usersController.renderUsersPage = function(set, req, res, next) { - usersController.getUsers(set, req.uid, req.query.page, function(err, userData) { +usersController.getFlaggedUsers = function (req, res, next) { + usersController.getUsers('users:flags', req.uid, req.query, function (err, userData) { if (err) { return next(err); } + + if (!userData.isAdminOrGlobalMod) { + return next(); + } + render(req, res, userData, next); }); }; -usersController.getUsers = function(set, uid, page, callback) { - var setToTitles = { - 'users:postcount': '[[pages:users/sort-posts]]', - 'users:reputation': '[[pages:users/sort-reputation]]', - 'users:joindate': '[[pages:users/latest]]', - 'users:online': '[[pages:users/online]]', - 'users:banned': '[[pages:users/banned]]' - }; +usersController.renderUsersPage = function (set, req, res, next) { + usersController.getUsers(set, req.uid, req.query, function (err, userData) { + if (err) { + return next(err); + } + + render(req, res, userData, next); + }); +}; - var setToCrumbs = { - 'users:postcount': '[[users:top_posters]]', - 'users:reputation': '[[users:most_reputation]]', - 'users:joindate': '[[global:users]]', - 'users:online': '[[global:online]]', - 'users:banned': '[[user:banned]]' +usersController.getUsers = function (set, uid, query, callback) { + var setToData = { + 'users:postcount': {title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]'}, + 'users:reputation': {title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]'}, + 'users:joindate': {title: '[[pages:users/latest]]', crumb: '[[global:users]]'}, + 'users:online': {title: '[[pages:users/online]]', crumb: '[[global:online]]'}, + 'users:banned': {title: '[[pages:users/banned]]', crumb: '[[user:banned]]'}, + 'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'}, }; - var breadcrumbs = [{text: setToCrumbs[set]}]; + if (!setToData[set]) { + setToData[set] = {title: '', crumb: ''}; + } + + var breadcrumbs = [{text: setToData[set].crumb}]; if (set !== 'users:joindate') { breadcrumbs.unshift({text: '[[global:users]]', url: '/users'}); } - page = parseInt(page, 10) || 1; + var page = parseInt(query.page, 10) || 1; var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 50; var start = Math.max(0, page - 1) * resultsPerPage; var stop = start + resultsPerPage - 1; async.parallel({ - isAdministrator: function(next) { - user.isAdministrator(uid, next); - }, - isGlobalMod: function(next) { - user.isGlobalModerator(uid, next); + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(uid, next); }, - usersData: function(next) { + usersData: function (next) { usersController.getUsersAndCount(set, uid, start, stop, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var pageCount = Math.ceil(results.usersData.count / resultsPerPage); var userData = { - loadmore_display: results.usersData.count > (stop - start + 1) ? 'block' : 'hide', users: results.usersData.users, - pagination: pagination.create(page, pageCount), - title: setToTitles[set] || '[[pages:users/latest]]', + pagination: pagination.create(page, pageCount, query), + userCount: results.usersData.count, + title: setToData[set].title || '[[pages:users/latest]]', breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs), - setName: set, - isAdminOrGlobalMod: results.isAdministrator || results.isGlobalMod + isAdminOrGlobalMod: results.isAdminOrGlobalMod }; - userData['route_' + set] = true; + userData['section_' + (query.section || 'joindate')] = true; callback(null, userData); }); }; -usersController.getUsersAndCount = function(set, uid, start, stop, callback) { +usersController.getUsersAndCount = function (set, uid, start, stop, callback) { async.parallel({ - users: function(next) { + users: function (next) { user.getUsersFromSet(set, uid, start, stop, next); }, - count: function(next) { + count: function (next) { if (set === 'users:online') { var now = Date.now(); db.sortedSetCount('users:online', now - 300000, '+inf', next); } else if (set === 'users:banned') { db.sortedSetCard('users:banned', next); + } else if (set === 'users:flags') { + db.sortedSetCard('users:flags', next); } else { db.getObjectField('global', 'userCount', next); } } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.users = results.users.filter(function(user) { + results.users = results.users.filter(function (user) { return user && parseInt(user.uid, 10); }); @@ -165,26 +225,22 @@ usersController.getUsersAndCount = function(set, uid, start, stop, callback) { }; function render(req, res, data, next) { - plugins.fireHook('filter:users.build', {req: req, res: res, templateData: data }, function(err, data) { + var registrationType = meta.config.registrationType; + + data.maximumInvites = meta.config.maximumInvites; + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; + + user.getInvitesNumber(req.uid, function (err, numInvites) { if (err) { return next(err); } - var registrationType = meta.config.registrationType; + res.append('X-Total-Count', data.userCount); + data.invites = numInvites; - data.templateData.maximumInvites = meta.config.maximumInvites; - data.templateData.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; - data.templateData.adminInviteOnly = registrationType === 'admin-invite-only'; - data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - - user.getInvitesNumber(req.uid, function(err, num) { - if (err) { - return next(err); - } - - data.templateData.invites = num; - res.render('users', data.templateData); - }); + res.render('users', data); }); } diff --git a/src/coverPhoto.js b/src/coverPhoto.js index d699ace785..699e4ee374 100644 --- a/src/coverPhoto.js +++ b/src/coverPhoto.js @@ -5,11 +5,11 @@ var meta = require('./meta'); var nconf = require('nconf'); -coverPhoto.getDefaultGroupCover = function(groupName) { +coverPhoto.getDefaultGroupCover = function (groupName) { return getCover('groups', groupName); }; -coverPhoto.getDefaultProfileCover = function(uid) { +coverPhoto.getDefaultProfileCover = function (uid) { return getCover('profile', parseInt(uid, 10)); }; diff --git a/src/database/mongo.js b/src/database/mongo.js index 0e7e8e2d2a..92bfd264dc 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -1,7 +1,7 @@ 'use strict'; -(function(module) { +(function (module) { var winston = require('winston'), async = require('async'), @@ -33,20 +33,21 @@ name: 'mongo:password', description: 'Password of your MongoDB database', hidden: true, - before: function(value) { value = value || nconf.get('mongo:password') || ''; return value; } + default: nconf.get('mongo:password') || '', + before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; } }, { name: "mongo:database", - description: "Which database to use", - 'default': nconf.get('mongo:database') || 0 + description: "MongoDB database name", + 'default': nconf.get('mongo:database') || 'nodebb' } ]; module.helpers = module.helpers || {}; module.helpers.mongo = require('./mongo/helpers'); - module.init = function(callback) { - callback = callback || function() {}; + module.init = function (callback) { + callback = callback || function () {}; try { var sessionStore; mongoClient = require('mongodb').MongoClient; @@ -74,7 +75,7 @@ nconf.set('mongo:port', 27017); } if (!nconf.get('mongo:database')) { - nconf.set('mongo:database', '0'); + nconf.set('mongo:database', 'nodebb'); } var hosts = nconf.get('mongo:host').split(','); @@ -95,7 +96,7 @@ connOptions = _.deepExtend((nconf.get('mongo:options') || {}), connOptions); - mongoClient.connect(connString, connOptions, function(err, _db) { + mongoClient.connect(connString, connOptions, function (err, _db) { if (err) { winston.error("NodeBB could not connect to your Mongo database. Mongo returned the following error: " + err.message); return callback(err); @@ -110,8 +111,13 @@ db: db }); } else { + // Initial Redis database + var rdb = require('./redis'); + // Create a new redis connection and store it in module (skeleton) + rdb.client = rdb.connect(); + module.sessionStore = new sessionStore({ - client: require('./redis').connect(), + client: rdb.client, ttl: 60 * 60 * 24 * 14 }); } @@ -141,7 +147,7 @@ async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}), async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}), async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true}) - ], function(err) { + ], function (err) { if (err) { winston.error('Error creating index ' + err.message); } @@ -155,7 +161,7 @@ }); }; - module.checkCompatibility = function(callback) { + module.checkCompatibility = function (callback) { var mongoPkg = require.main.require('./node_modules/mongodb/package.json'), err = semver.lt(mongoPkg.version, '2.0.0') ? new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.') : null; @@ -165,32 +171,32 @@ callback(err); }; - module.info = function(db, callback) { + module.info = function (db, callback) { async.parallel({ - serverStatus: function(next) { + serverStatus: function (next) { db.command({'serverStatus': 1}, next); }, - stats: function(next) { + stats: function (next) { db.command({'dbStats': 1}, next); }, - listCollections: function(next) { - db.listCollections().toArray(function(err, items) { + listCollections: function (next) { + db.listCollections().toArray(function (err, items) { if (err) { return next(err); } - async.map(items, function(collection, next) { + async.map(items, function (collection, next) { db.collection(collection.name).stats(next); }, next); }); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var stats = results.stats; var scale = 1024 * 1024; - results.listCollections = results.listCollections.map(function(collectionInfo) { + results.listCollections = results.listCollections.map(function (collectionInfo) { return { name: collectionInfo.ns, count: collectionInfo.count, @@ -222,7 +228,7 @@ }); }; - module.close = function() { + module.close = function () { db.close(); }; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index dbf294119e..7baf70b401 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -1,20 +1,20 @@ "use strict"; -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.setObject = function(key, data, callback) { + module.setObject = function (key, data, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function(err) { + db.collection('objects').update({_key: key}, {$set: data}, {upsert: true, w: 1}, function (err) { callback(err); }); }; - module.setObjectField = function(key, field, value, callback) { + module.setObjectField = function (key, field, value, callback) { callback = callback || helpers.noop; if (!field) { return callback(); @@ -25,18 +25,18 @@ module.exports = function(db, module) { module.setObject(key, data, callback); }; - module.getObject = function(key, callback) { + module.getObject = function (key, callback) { if (!key) { return callback(); } db.collection('objects').findOne({_key: key}, {_id: 0, _key: 0}, callback); }; - module.getObjects = function(keys, callback) { + module.getObjects = function (keys, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } - db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function(err, data) { + db.collection('objects').find({_key: {$in: keys}}, {_id: 0}).toArray(function (err, data) { if (err) { return callback(err); } @@ -44,7 +44,7 @@ module.exports = function(db, module) { var map = helpers.toMap(data); var returnData = []; - for (var i=0; i<keys.length; ++i) { + for (var i = 0; i < keys.length; ++i) { returnData.push(map[keys[i]]); } @@ -52,7 +52,7 @@ module.exports = function(db, module) { }); }; - module.getObjectField = function(key, field, callback) { + module.getObjectField = function (key, field, callback) { if (!key) { return callback(); } @@ -61,7 +61,7 @@ module.exports = function(db, module) { _id: 0 }; _fields[field] = 1; - db.collection('objects').findOne({_key: key}, {fields: _fields}, function(err, item) { + db.collection('objects').findOne({_key: key}, {fields: _fields}, function (err, item) { if (err || !item) { return callback(err, null); } @@ -70,7 +70,7 @@ module.exports = function(db, module) { }); }; - module.getObjectFields = function(key, fields, callback) { + module.getObjectFields = function (key, fields, callback) { if (!key) { return callback(); } @@ -78,24 +78,24 @@ module.exports = function(db, module) { _id: 0 }; - for(var i=0; i<fields.length; ++i) { + for(var i = 0; i < fields.length; ++i) { fields[i] = helpers.fieldToString(fields[i]); _fields[fields[i]] = 1; } - db.collection('objects').findOne({_key: key}, {fields: _fields}, function(err, item) { + db.collection('objects').findOne({_key: key}, {fields: _fields}, function (err, item) { if (err) { return callback(err); } item = item || {}; var result = {}; - for(i=0; i<fields.length; ++i) { + for(i = 0; i < fields.length; ++i) { result[fields[i]] = item[fields[i]] !== undefined ? item[fields[i]] : null; } callback(null, result); }); }; - module.getObjectsFields = function(keys, fields, callback) { + module.getObjectsFields = function (keys, fields, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } @@ -104,12 +104,12 @@ module.exports = function(db, module) { _key: 1 }; - for(var i=0; i<fields.length; ++i) { + for(var i = 0; i < fields.length; ++i) { fields[i] = helpers.fieldToString(fields[i]); _fields[fields[i]] = 1; } - db.collection('objects').find({_key: {$in: keys}}, {fields: _fields}).toArray(function(err, items) { + db.collection('objects').find({_key: {$in: keys}}, {fields: _fields}).toArray(function (err, items) { if (err) { return callback(err); } @@ -122,10 +122,10 @@ module.exports = function(db, module) { var returnData = []; var item; - for (var i=0; i<keys.length; ++i) { + for (var i = 0; i < keys.length; ++i) { item = map[keys[i]] || {}; - for (var k=0; k<fields.length; ++k) { + for (var k = 0; k < fields.length; ++k) { if (item[fields[k]] === undefined) { item[fields[k]] = null; } @@ -137,14 +137,14 @@ module.exports = function(db, module) { }); }; - module.getObjectKeys = function(key, callback) { - module.getObject(key, function(err, data) { + module.getObjectKeys = function (key, callback) { + module.getObject(key, function (err, data) { callback(err, data ? Object.keys(data) : []); }); }; - module.getObjectValues = function(key, callback) { - module.getObject(key, function(err, data) { + module.getObjectValues = function (key, callback) { + module.getObject(key, function (err, data) { if(err) { return callback(err); } @@ -159,36 +159,36 @@ module.exports = function(db, module) { }); }; - module.isObjectField = function(key, field, callback) { + module.isObjectField = function (key, field, callback) { if (!key) { return callback(); } var data = {}; field = helpers.fieldToString(field); data[field] = ''; - db.collection('objects').findOne({_key: key}, {fields: data}, function(err, item) { + db.collection('objects').findOne({_key: key}, {fields: data}, function (err, item) { callback(err, !!item && item[field] !== undefined && item[field] !== null); }); }; - module.isObjectFields = function(key, fields, callback) { + module.isObjectFields = function (key, fields, callback) { if (!key) { return callback(); } var data = {}; - fields.forEach(function(field) { + fields.forEach(function (field) { field = helpers.fieldToString(field); data[field] = ''; }); - db.collection('objects').findOne({_key: key}, {fields: data}, function(err, item) { + db.collection('objects').findOne({_key: key}, {fields: data}, function (err, item) { if (err) { return callback(err); } var results = []; - fields.forEach(function(field, index) { + fields.forEach(function (field, index) { results[index] = !!item && item[field] !== undefined && item[field] !== null; }); @@ -196,11 +196,11 @@ module.exports = function(db, module) { }); }; - module.deleteObjectField = function(key, field, callback) { + module.deleteObjectField = function (key, field, callback) { module.deleteObjectFields(key, [field], callback); }; - module.deleteObjectFields = function(key, fields, callback) { + module.deleteObjectFields = function (key, fields, callback) { callback = callback || helpers.noop; if (!key || !Array.isArray(fields) || !fields.length) { return callback(); @@ -211,34 +211,36 @@ module.exports = function(db, module) { } var data = {}; - fields.forEach(function(field) { + fields.forEach(function (field) { field = helpers.fieldToString(field); data[field] = ''; }); - db.collection('objects').update({_key: key}, {$unset : data}, function(err) { + db.collection('objects').update({_key: key}, {$unset : data}, function (err) { callback(err); }); }; - module.incrObjectField = function(key, field, callback) { + module.incrObjectField = function (key, field, callback) { module.incrObjectFieldBy(key, field, 1, callback); }; - module.decrObjectField = function(key, field, callback) { + module.decrObjectField = function (key, field, callback) { module.incrObjectFieldBy(key, field, -1, callback); }; - module.incrObjectFieldBy = function(key, field, value, callback) { + module.incrObjectFieldBy = function (key, field, value, callback) { callback = callback || helpers.noop; - if (!key) { + value = parseInt(value, 10); + if (!key || isNaN(value)) { return callback(); } + var data = {}; field = helpers.fieldToString(field); data[field] = value; - db.collection('objects').findAndModify({_key: key}, {}, {$inc: data}, {new: true, upsert: true}, function(err, result) { + db.collection('objects').findAndModify({_key: key}, {}, {$inc: data}, {new: true, upsert: true}, function (err, result) { callback(err, result && result.value ? result.value[field] : null); }); }; diff --git a/src/database/mongo/helpers.js b/src/database/mongo/helpers.js index db84dbb369..0985e62617 100644 --- a/src/database/mongo/helpers.js +++ b/src/database/mongo/helpers.js @@ -2,16 +2,16 @@ var helpers = {}; -helpers.toMap = function(data) { +helpers.toMap = function (data) { var map = {}; - for (var i = 0; i<data.length; ++i) { + for (var i = 0; i < data.length; ++i) { map[data[i]._key] = data[i]; data[i]._key = undefined; } return map; }; -helpers.fieldToString = function(field) { +helpers.fieldToString = function (field) { if(field === null || field === undefined) { return field; } @@ -24,7 +24,7 @@ helpers.fieldToString = function(field) { return field; }; -helpers.valueToString = function(value) { +helpers.valueToString = function (value) { if(value === null || value === undefined) { return value; } @@ -32,6 +32,6 @@ helpers.valueToString = function(value) { return value.toString(); }; -helpers.noop = function() {}; +helpers.noop = function () {}; module.exports = helpers; \ No newline at end of file diff --git a/src/database/mongo/list.js b/src/database/mongo/list.js index 40e294a488..c4b4ffbb6c 100644 --- a/src/database/mongo/list.js +++ b/src/database/mongo/list.js @@ -1,9 +1,9 @@ "use strict"; -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.listPrepend = function(key, value, callback) { + module.listPrepend = function (key, value, callback) { callback = callback || helpers.noop; if (!key) { @@ -12,13 +12,13 @@ module.exports = function(db, module) { value = helpers.valueToString(value); - module.isObjectField(key, 'array', function(err, exists) { + module.isObjectField(key, 'array', function (err, exists) { if (err) { return callback(err); } if (exists) { - db.collection('objects').update({_key:key}, {$push: {array: {$each: [value], $position: 0}}}, {upsert:true, w:1 }, function(err, res) { + db.collection('objects').update({_key:key}, {$push: {array: {$each: [value], $position: 0}}}, {upsert:true, w:1 }, function (err, res) { callback(err); }); } else { @@ -27,67 +27,67 @@ module.exports = function(db, module) { }); }; - module.listAppend = function(key, value, callback) { + module.listAppend = function (key, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').update({ _key: key }, { $push: { array: value } }, {upsert:true, w:1}, function(err, res) { + db.collection('objects').update({ _key: key }, { $push: { array: value } }, {upsert:true, w:1}, function (err, res) { callback(err); }); }; - module.listRemoveLast = function(key, callback) { + module.listRemoveLast = function (key, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - module.getListRange(key, -1, -1, function(err, value) { + module.getListRange(key, -1, -1, function (err, value) { if (err) { return callback(err); } - db.collection('objects').update({_key: key }, { $pop: { array: 1 } }, function(err, result) { + db.collection('objects').update({_key: key }, { $pop: { array: 1 } }, function (err, result) { callback(err, (value && value.length) ? value[0] : null); }); }); }; - module.listRemoveAll = function(key, value, callback) { + module.listRemoveAll = function (key, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').update({_key: key }, { $pull: { array: value } }, function(err, res) { + db.collection('objects').update({_key: key }, { $pull: { array: value } }, function (err, res) { callback(err); }); }; - module.listTrim = function(key, start, stop, callback) { + module.listTrim = function (key, start, stop, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - module.getListRange(key, start, stop, function(err, value) { + module.getListRange(key, start, stop, function (err, value) { if (err) { return callback(err); } - db.collection('objects').update({_key: key}, {$set: {array: value}}, function(err, res) { + db.collection('objects').update({_key: key}, {$set: {array: value}}, function (err, res) { callback(err); }); }); }; - module.getListRange = function(key, start, stop, callback) { + module.getListRange = function (key, start, stop, callback) { if (!key) { return callback(); } - db.collection('objects').findOne({_key:key}, { array: 1}, function(err, data) { + db.collection('objects').findOne({_key:key}, { array: 1}, function (err, data) { if(err || !(data && data.array)) { return callback(err, []); } diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 777565df06..ea76836065 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -2,51 +2,51 @@ var winston = require('winston'); -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.flushdb = function(callback) { + module.flushdb = function (callback) { callback = callback || helpers.noop; db.dropDatabase(callback); }; - module.exists = function(key, callback) { + module.exists = function (key, callback) { if (!key) { return callback(); } - db.collection('objects').findOne({_key: key}, function(err, item) { + db.collection('objects').findOne({_key: key}, function (err, item) { callback(err, item !== undefined && item !== null); }); }; - module.delete = function(key, callback) { + module.delete = function (key, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - db.collection('objects').remove({_key: key}, function(err, res) { + db.collection('objects').remove({_key: key}, function (err, res) { callback(err); }); }; - module.deleteAll = function(keys, callback) { + module.deleteAll = function (keys, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { return callback(); } - db.collection('objects').remove({_key: {$in: keys}}, function(err, res) { + db.collection('objects').remove({_key: {$in: keys}}, function (err, res) { callback(err); }); }; - module.get = function(key, callback) { + module.get = function (key, callback) { if (!key) { return callback(); } module.getObjectField(key, 'value', callback); }; - module.set = function(key, value, callback) { + module.set = function (key, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); @@ -55,36 +55,36 @@ module.exports = function(db, module) { module.setObject(key, data, callback); }; - module.increment = function(key, callback) { + module.increment = function (key, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } - db.collection('objects').findAndModify({_key: key}, {}, {$inc: {value: 1}}, {new: true, upsert: true}, function(err, result) { + db.collection('objects').findAndModify({_key: key}, {}, {$inc: {value: 1}}, {new: true, upsert: true}, function (err, result) { callback(err, result && result.value ? result.value.value : null); }); }; - module.rename = function(oldKey, newKey, callback) { + module.rename = function (oldKey, newKey, callback) { callback = callback || helpers.noop; - db.collection('objects').update({_key: oldKey}, {$set:{_key: newKey}}, {multi: true}, function(err, res) { + db.collection('objects').update({_key: oldKey}, {$set:{_key: newKey}}, {multi: true}, function (err, res) { callback(err); }); }; - module.expire = function(key, seconds, callback) { + module.expire = function (key, seconds, callback) { module.expireAt(key, Math.round(Date.now() / 1000) + seconds, callback); }; - module.expireAt = function(key, timestamp, callback) { + module.expireAt = function (key, timestamp, callback) { module.setObjectField(key, 'expireAt', new Date(timestamp * 1000), callback); }; - module.pexpire = function(key, ms, callback) { + module.pexpire = function (key, ms, callback) { module.pexpireAt(key, Date.now() + parseInt(ms, 10), callback); }; - module.pexpireAt = function(key, timestamp, callback) { + module.pexpireAt = function (key, timestamp, callback) { module.setObjectField(key, 'expireAt', new Date(timestamp), callback); }; }; \ No newline at end of file diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js index efcb4ff4c8..0a035a3398 100644 --- a/src/database/mongo/sets.js +++ b/src/database/mongo/sets.js @@ -1,15 +1,15 @@ "use strict"; -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.setAdd = function(key, value, callback) { + module.setAdd = function (key, value, callback) { callback = callback || helpers.noop; if(!Array.isArray(value)) { value = [value]; } - value.forEach(function(element, index, array) { + value.forEach(function (element, index, array) { array[index] = helpers.valueToString(element); }); @@ -24,12 +24,12 @@ module.exports = function(db, module) { }, { upsert: true, w: 1 - }, function(err, res) { + }, function (err, res) { callback(err); }); }; - module.setsAdd = function(keys, value, callback) { + module.setsAdd = function (keys, value, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { @@ -40,13 +40,13 @@ module.exports = function(db, module) { value = [value]; } - value.forEach(function(element, index, array) { + value.forEach(function (element, index, array) { array[index] = helpers.valueToString(element); }); var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { bulk.find({_key: keys[i]}).upsert().updateOne({ $addToSet: { members: { $each: value @@ -54,27 +54,27 @@ module.exports = function(db, module) { }}); } - bulk.execute(function(err, res) { + bulk.execute(function (err, res) { callback(err); }); }; - module.setRemove = function(key, value, callback) { + module.setRemove = function (key, value, callback) { callback = callback || helpers.noop; if(!Array.isArray(value)) { value = [value]; } - value.forEach(function(element, index, array) { + value.forEach(function (element, index, array) { array[index] = helpers.valueToString(element); }); - db.collection('objects').update({_key: key}, {$pullAll: {members: value}}, function(err, res) { + db.collection('objects').update({_key: key}, {$pullAll: {members: value}}, function (err, res) { callback(err); }); }; - module.setsRemove = function(keys, value, callback) { + module.setsRemove = function (keys, value, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { return callback(); @@ -83,43 +83,43 @@ module.exports = function(db, module) { var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { bulk.find({_key: keys[i]}).updateOne({$pull: { members: value }}); } - bulk.execute(function(err, res) { + bulk.execute(function (err, res) { callback(err); }); }; - module.isSetMember = function(key, value, callback) { + module.isSetMember = function (key, value, callback) { if (!key) { return callback(null, false); } value = helpers.valueToString(value); - db.collection('objects').findOne({_key: key, members: value}, {_id: 0, members: 0},function(err, item) { + db.collection('objects').findOne({_key: key, members: value}, {_id: 0, members: 0},function (err, item) { callback(err, item !== null && item !== undefined); }); }; - module.isSetMembers = function(key, values, callback) { + module.isSetMembers = function (key, values, callback) { if (!key || !Array.isArray(values) || !values.length) { return callback(null, []); } - for (var i=0; i<values.length; ++i) { + for (var i = 0; i < values.length; ++i) { values[i] = helpers.valueToString(values[i]); } - db.collection('objects').findOne({_key: key}, {_id: 0, _key: 0}, function(err, items) { + db.collection('objects').findOne({_key: key}, {_id: 0, _key: 0}, function (err, items) { if (err) { return callback(err); } - values = values.map(function(value) { + values = values.map(function (value) { return !!(items && Array.isArray(items.members) && items.members.indexOf(value) !== -1); }); @@ -127,22 +127,22 @@ module.exports = function(db, module) { }); }; - module.isMemberOfSets = function(sets, value, callback) { + module.isMemberOfSets = function (sets, value, callback) { if (!Array.isArray(sets) || !sets.length) { return callback(null, []); } value = helpers.valueToString(value); - db.collection('objects').find({_key: {$in : sets}, members: value}, {_id:0, members: 0}).toArray(function(err, result) { + db.collection('objects').find({_key: {$in : sets}, members: value}, {_id:0, members: 0}).toArray(function (err, result) { if (err) { return callback(err); } - result = result.map(function(item) { + result = result.map(function (item) { return item._key; }); - result = sets.map(function(set) { + result = sets.map(function (set) { return result.indexOf(set) !== -1; }); @@ -150,69 +150,69 @@ module.exports = function(db, module) { }); }; - module.getSetMembers = function(key, callback) { + module.getSetMembers = function (key, callback) { if (!key) { return callback(null, []); } - db.collection('objects').findOne({_key: key}, {members: 1}, {_id: 0, _key: 0}, function(err, data) { + db.collection('objects').findOne({_key: key}, {members: 1}, {_id: 0, _key: 0}, function (err, data) { callback(err, data ? data.members : []); }); }; - module.getSetsMembers = function(keys, callback) { + module.getSetsMembers = function (keys, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } - db.collection('objects').find({_key: {$in: keys}}, {_id: 0, _key: 1, members: 1}).toArray(function(err, data) { + db.collection('objects').find({_key: {$in: keys}}, {_id: 0, _key: 1, members: 1}).toArray(function (err, data) { if (err) { return callback(err); } var sets = {}; - data.forEach(function(set) { + data.forEach(function (set) { sets[set._key] = set.members || []; }); var returnData = new Array(keys.length); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { returnData[i] = sets[keys[i]] || []; } callback(null, returnData); }); }; - module.setCount = function(key, callback) { + module.setCount = function (key, callback) { if (!key) { return callback(null, 0); } - db.collection('objects').findOne({_key: key}, {_id: 0}, function(err, data) { + db.collection('objects').findOne({_key: key}, {_id: 0}, function (err, data) { callback(err, data ? data.members.length : 0); }); }; - module.setsCount = function(keys, callback) { - module.getSetsMembers(keys, function(err, setsMembers) { + module.setsCount = function (keys, callback) { + module.getSetsMembers(keys, function (err, setsMembers) { if (err) { return callback(err); } - var counts = setsMembers.map(function(members) { + var counts = setsMembers.map(function (members) { return (members && members.length) || 0; }); callback(null, counts); }); }; - module.setRemoveRandom = function(key, callback) { - callback = callback || function() {}; - db.collection('objects').findOne({_key:key}, function(err, data) { + module.setRemoveRandom = function (key, callback) { + callback = callback || function () {}; + db.collection('objects').findOne({_key:key}, function (err, data) { if(err || !data) { return callback(err); } var randomIndex = Math.floor(Math.random() * data.members.length); var value = data.members[randomIndex]; - module.setRemove(data._key, value, function(err) { + module.setRemove(data._key, value, function (err) { callback(err, value); }); }); diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index d8a9205ce0..1a36f6eeb0 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -1,11 +1,12 @@ "use strict"; var async = require('async'); +var utils = require('../../../public/src/utils'); -module.exports = function(db, module) { +module.exports = function (db, module) { var helpers = module.helpers.mongo; - module.sortedSetAdd = function(key, score, value, callback) { + module.sortedSetAdd = function (key, score, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); @@ -16,7 +17,10 @@ module.exports = function(db, module) { value = helpers.valueToString(value); - db.collection('objects').update({_key: key, value: value}, {$set: {score: parseInt(score, 10)}}, {upsert:true, w: 1}, function(err) { + db.collection('objects').update({_key: key, value: value}, {$set: {score: parseInt(score, 10)}}, {upsert:true, w: 1}, function (err) { + if (err && err.message.startsWith('E11000 duplicate key error')) { + return module.sortedSetAdd(key, score, value, callback); + } callback(err); }); }; @@ -33,16 +37,16 @@ module.exports = function(db, module) { var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for(var i=0; i<scores.length; ++i) { + for(var i = 0; i < scores.length; ++i) { bulk.find({_key: key, value: values[i]}).upsert().updateOne({$set: {score: parseInt(scores[i], 10)}}); } - bulk.execute(function(err) { + bulk.execute(function (err) { callback(err); }); } - module.sortedSetsAdd = function(keys, score, value, callback) { + module.sortedSetsAdd = function (keys, score, value, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { return callback(); @@ -51,16 +55,16 @@ module.exports = function(db, module) { var bulk = db.collection('objects').initializeUnorderedBulkOp(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { bulk.find({_key: keys[i], value: value}).upsert().updateOne({$set: {score: parseInt(score, 10)}}); } - bulk.execute(function(err) { + bulk.execute(function (err) { callback(err); }); }; - module.sortedSetRemove = function(key, value, callback) { + module.sortedSetRemove = function (key, value, callback) { function done(err) { callback(err); } @@ -78,19 +82,19 @@ module.exports = function(db, module) { } }; - module.sortedSetsRemove = function(keys, value, callback) { + module.sortedSetsRemove = function (keys, value, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').remove({_key: {$in: keys}, value: value}, function(err) { + db.collection('objects').remove({_key: {$in: keys}, value: value}, function (err) { callback(err); }); }; - module.sortedSetsRemoveRangeByScore = function(keys, min, max, callback) { + module.sortedSetsRemoveRangeByScore = function (keys, min, max, callback) { callback = callback || helpers.noop; if (!Array.isArray(keys) || !keys.length) { return callback(); @@ -105,24 +109,24 @@ module.exports = function(db, module) { query.score.$lte = max; } - db.collection('objects').remove(query, function(err) { + db.collection('objects').remove(query, function (err) { callback(err); }); }; - module.getSortedSetRange = function(key, start, stop, callback) { + module.getSortedSetRange = function (key, start, stop, callback) { getSortedSetRange(key, start, stop, 1, false, callback); }; - module.getSortedSetRevRange = function(key, start, stop, callback) { + module.getSortedSetRevRange = function (key, start, stop, callback) { getSortedSetRange(key, start, stop, -1, false, callback); }; - module.getSortedSetRangeWithScores = function(key, start, stop, callback) { + module.getSortedSetRangeWithScores = function (key, start, stop, callback) { getSortedSetRange(key, start, stop, 1, true, callback); }; - module.getSortedSetRevRangeWithScores = function(key, start, stop, callback) { + module.getSortedSetRevRangeWithScores = function (key, start, stop, callback) { getSortedSetRange(key, start, stop, -1, true, callback); }; @@ -140,17 +144,22 @@ module.exports = function(db, module) { key = {$in: key}; } + var limit = stop - start + 1; + if (limit <= 0) { + limit = 0; + } + db.collection('objects').find({_key: key}, {fields: fields}) - .limit(stop - start + 1) + .limit(limit) .skip(start) .sort({score: sort}) - .toArray(function(err, data) { + .toArray(function (err, data) { if (err || !data) { return callback(err); } if (!withScores) { - data = data.map(function(item) { + data = data.map(function (item) { return item.value; }); } @@ -159,19 +168,19 @@ module.exports = function(db, module) { }); } - module.getSortedSetRangeByScore = function(key, start, count, min, max, callback) { + module.getSortedSetRangeByScore = function (key, start, count, min, max, callback) { getSortedSetRangeByScore(key, start, count, min, max, 1, false, callback); }; - module.getSortedSetRevRangeByScore = function(key, start, count, max, min, callback) { + module.getSortedSetRevRangeByScore = function (key, start, count, max, min, callback) { getSortedSetRangeByScore(key, start, count, min, max, -1, false, callback); }; - module.getSortedSetRangeByScoreWithScores = function(key, start, count, min, max, callback) { + module.getSortedSetRangeByScoreWithScores = function (key, start, count, min, max, callback) { getSortedSetRangeByScore(key, start, count, min, max, 1, true, callback); }; - module.getSortedSetRevRangeByScoreWithScores = function(key, start, count, max, min, callback) { + module.getSortedSetRevRangeByScoreWithScores = function (key, start, count, max, min, callback) { getSortedSetRangeByScore(key, start, count, min, max, -1, true, callback); }; @@ -202,13 +211,13 @@ module.exports = function(db, module) { .limit(count) .skip(start) .sort({score: sort}) - .toArray(function(err, data) { + .toArray(function (err, data) { if(err) { return callback(err); } if (!withScores) { - data = data.map(function(item) { + data = data.map(function (item) { return item.value; }); } @@ -217,7 +226,7 @@ module.exports = function(db, module) { }); } - module.sortedSetCount = function(key, min, max, callback) { + module.sortedSetCount = function (key, min, max, callback) { if (!key) { return callback(); } @@ -231,22 +240,22 @@ module.exports = function(db, module) { query.score.$lte = max; } - db.collection('objects').count(query, function(err, count) { + db.collection('objects').count(query, function (err, count) { callback(err, count ? count : 0); }); }; - module.sortedSetCard = function(key, callback) { + module.sortedSetCard = function (key, callback) { if (!key) { return callback(null, 0); } - db.collection('objects').count({_key: key}, function(err, count) { + db.collection('objects').count({_key: key}, function (err, count) { count = parseInt(count, 10); callback(err, count ? count : 0); }); }; - module.sortedSetsCard = function(keys, callback) { + module.sortedSetsCard = function (keys, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(); } @@ -255,7 +264,7 @@ module.exports = function(db, module) { { $group: { _id: {_key: '$_key'}, count: { $sum: 1 } } }, { $project: { _id: 1, count: '$count' } } ]; - db.collection('objects').aggregate(pipeline, function(err, results) { + db.collection('objects').aggregate(pipeline, function (err, results) { if (err) { return callback(err); } @@ -265,24 +274,24 @@ module.exports = function(db, module) { } var map = {}; - results.forEach(function(item) { + results.forEach(function (item) { if (item && item._id._key) { map[item._id._key] = item.count; } }); - results = keys.map(function(key) { + results = keys.map(function (key) { return map[key] || 0; }); callback(null, results); }); }; - module.sortedSetRank = function(key, value, callback) { + module.sortedSetRank = function (key, value, callback) { getSortedSetRank(module.getSortedSetRange, key, value, callback); }; - module.sortedSetRevRank = function(key, value, callback) { + module.sortedSetRevRank = function (key, value, callback) { getSortedSetRank(module.getSortedSetRevRange, key, value, callback); }; @@ -291,7 +300,7 @@ module.exports = function(db, module) { return callback(); } value = helpers.valueToString(value); - method(key, 0, -1, function(err, result) { + method(key, 0, -1, function (err, result) { if(err) { return callback(err); } @@ -301,27 +310,27 @@ module.exports = function(db, module) { }); } - module.sortedSetsRanks = function(keys, values, callback) { + module.sortedSetsRanks = function (keys, values, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } var data = new Array(values.length); - for (var i=0; i<values.length; ++i) { + for (var i = 0; i < values.length; ++i) { data[i] = {key: keys[i], value: values[i]}; } - async.map(data, function(item, next) { + async.map(data, function (item, next) { getSortedSetRank(module.getSortedSetRange, item.key, item.value, next); }, callback); }; - module.sortedSetRanks = function(key, values, callback) { - module.getSortedSetRange(key, 0, -1, function(err, sortedSet) { + module.sortedSetRanks = function (key, values, callback) { + module.getSortedSetRange(key, 0, -1, function (err, sortedSet) { if (err) { return callback(err); } - var result = values.map(function(value) { + var result = values.map(function (value) { if (!value) { return null; } @@ -333,22 +342,22 @@ module.exports = function(db, module) { }); }; - module.sortedSetScore = function(key, value, callback) { + module.sortedSetScore = function (key, value, callback) { if (!key) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').findOne({_key: key, value: value}, {fields:{_id: 0, score: 1}}, function(err, result) { + db.collection('objects').findOne({_key: key, value: value}, {fields:{_id: 0, score: 1}}, function (err, result) { callback(err, result ? result.score : null); }); }; - module.sortedSetsScore = function(keys, value, callback) { + module.sortedSetsScore = function (keys, value, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').find({_key:{$in:keys}, value: value}, {_id:0, _key:1, score: 1}).toArray(function(err, result) { + db.collection('objects').find({_key:{$in:keys}, value: value}, {_id:0, _key:1, score: 1}).toArray(function (err, result) { if (err) { return callback(err); } @@ -357,7 +366,7 @@ module.exports = function(db, module) { returnData = [], item; - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { item = map[keys[i]]; returnData.push(item ? item.score : null); } @@ -366,164 +375,198 @@ module.exports = function(db, module) { }); }; - module.sortedSetScores = function(key, values, callback) { + module.sortedSetScores = function (key, values, callback) { if (!key) { return callback(); } values = values.map(helpers.valueToString); - db.collection('objects').find({_key: key, value: {$in: values}}, {_id: 0, value: 1, score: 1}).toArray(function(err, result) { + db.collection('objects').find({_key: key, value: {$in: values}}, {_id: 0, value: 1, score: 1}).toArray(function (err, result) { if (err) { return callback(err); } var map = {}; - result.forEach(function(item) { + result.forEach(function (item) { map[item.value] = item.score; }); - var returnData = new Array(values.length), - score; + var returnData = new Array(values.length); + var score; - for(var i=0; i<values.length; ++i) { + for(var i = 0; i < values.length; ++i) { score = map[values[i]]; - returnData[i] = score ? score : null; + returnData[i] = utils.isNumber(score) ? score : null; } callback(null, returnData); }); }; - module.isSortedSetMember = function(key, value, callback) { + module.isSortedSetMember = function (key, value, callback) { if (!key) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').findOne({_key: key, value: value}, {_id: 0, value: 1}, function(err, result) { + db.collection('objects').findOne({_key: key, value: value}, {_id: 0, value: 1}, function (err, result) { callback(err, !!result); }); }; - module.isSortedSetMembers = function(key, values, callback) { + module.isSortedSetMembers = function (key, values, callback) { if (!key) { return callback(); } values = values.map(helpers.valueToString); - db.collection('objects').find({_key: key, value: {$in: values}}, {fields: {_id: 0, value: 1}}).toArray(function(err, results) { + db.collection('objects').find({_key: key, value: {$in: values}}, {fields: {_id: 0, value: 1}}).toArray(function (err, results) { if (err) { return callback(err); } - results = results.map(function(item) { + results = results.map(function (item) { return item.value; }); - values = values.map(function(value) { + values = values.map(function (value) { return results.indexOf(value) !== -1; }); callback(null, values); }); }; - module.isMemberOfSortedSets = function(keys, value, callback) { + module.isMemberOfSortedSets = function (keys, value, callback) { if (!Array.isArray(keys)) { return callback(); } value = helpers.valueToString(value); - db.collection('objects').find({_key: {$in: keys}, value: value}, {fields: {_id: 0, _key: 1, value: 1}}).toArray(function(err, results) { + db.collection('objects').find({_key: {$in: keys}, value: value}, {fields: {_id: 0, _key: 1, value: 1}}).toArray(function (err, results) { if (err) { return callback(err); } - results = results.map(function(item) { + results = results.map(function (item) { return item._key; }); - results = keys.map(function(key) { + results = keys.map(function (key) { return results.indexOf(key) !== -1; }); callback(null, results); }); }; - module.getSortedSetsMembers = function(keys, callback) { + module.getSortedSetsMembers = function (keys, callback) { if (!Array.isArray(keys) || !keys.length) { return callback(null, []); } - db.collection('objects').find({_key: {$in: keys}}, {_id: 0, _key: 1, value: 1}).toArray(function(err, data) { + db.collection('objects').find({_key: {$in: keys}}, {_id: 0, _key: 1, value: 1}).toArray(function (err, data) { if (err) { return callback(err); } var sets = {}; - data.forEach(function(set) { + data.forEach(function (set) { sets[set._key] = sets[set._key] || []; sets[set._key].push(set.value); }); var returnData = new Array(keys.length); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { returnData[i] = sets[keys[i]] || []; } callback(null, returnData); }); }; - module.getSortedSetUnion = function(sets, start, stop, callback) { - getSortedSetUnion(sets, 1, start, stop, callback); + module.sortedSetUnionCard = function (keys, callback) { + if (!Array.isArray(keys) || !keys.length) { + return callback(null, 0); + } + + var pipeline = [ + { $match: { _key: {$in: keys} } }, + { $group: { _id: {value: '$value' } } }, + { $group: { _id: null, count: { $sum: 1 } } } + ]; + + var project = { _id: 0, count: '$count' }; + pipeline.push({ $project: project }); + + db.collection('objects').aggregate(pipeline, function (err, data) { + callback(err, Array.isArray(data) && data.length ? data[0].count : 0); + }); }; - module.getSortedSetRevUnion = function(sets, start, stop, callback) { - getSortedSetUnion(sets, -1, start, stop, callback); + module.getSortedSetUnion = function (params, callback) { + params.sort = 1; + getSortedSetUnion(params, callback); }; + module.getSortedSetRevUnion = function (params, callback) { + params.sort = -1; + getSortedSetUnion(params, callback); + }; - function getSortedSetUnion(sets, sort, start, stop, callback) { - if (!Array.isArray(sets) || !sets.length) { + function getSortedSetUnion(params, callback) { + if (!Array.isArray(params.sets) || !params.sets.length) { return callback(); } - var limit = stop - start + 1; + var limit = params.stop - params.start + 1; if (limit <= 0) { limit = 0; } + var aggregate = {}; + if (params.aggregate) { + aggregate['$' + params.aggregate.toLowerCase()] = '$score'; + } else { + aggregate.$sum = '$score'; + } + var pipeline = [ - { $match: { _key: {$in: sets}} }, - { $group: { _id: {value: '$value'}, totalScore: {$sum : "$score"}} }, - { $sort: { totalScore: sort} } + { $match: { _key: {$in: params.sets}} }, + { $group: { _id: {value: '$value'}, totalScore: aggregate} }, + { $sort: { totalScore: params.sort} } ]; - if (start) { - pipeline.push({ $skip: start }); + if (params.start) { + pipeline.push({ $skip: params.start }); } if (limit > 0) { pipeline.push({ $limit: limit }); } - pipeline.push({ $project: { _id: 0, value: '$_id.value' }}); + var project = { _id: 0, value: '$_id.value' }; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); - db.collection('objects').aggregate(pipeline, function(err, data) { + db.collection('objects').aggregate(pipeline, function (err, data) { if (err || !data) { return callback(err); } - data = data.map(function(item) { - return item.value; - }); + if (!params.withScores) { + data = data.map(function (item) { + return item.value; + }); + } + callback(null, data); }); } - module.sortedSetIncrBy = function(key, increment, value, callback) { + module.sortedSetIncrBy = function (key, increment, value, callback) { callback = callback || helpers.noop; if (!key) { return callback(); } var data = {}; - value = helpers.fieldToString(value); + value = helpers.valueToString(value); data.score = parseInt(increment, 10); - db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function(err, result) { + db.collection('objects').findAndModify({_key: key, value: value}, {}, {$inc: data}, {new: true, upsert: true}, function (err, result) { // if there is duplicate key error retry the upsert // https://github.com/NodeBB/NodeBB/issues/4467 // https://jira.mongodb.org/browse/SERVER-14322 @@ -535,7 +578,7 @@ module.exports = function(db, module) { }); }; - module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) { + module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { var query = {_key: key}; if (min !== '-') { query.value = {$gte: min}; @@ -548,18 +591,18 @@ module.exports = function(db, module) { .sort({value: 1}) .skip(start) .limit(count === -1 ? 0 : count) - .toArray(function(err, data) { + .toArray(function (err, data) { if (err) { return callback(err); } - data = data.map(function(item) { + data = data.map(function (item) { return item && item.value; }); callback(err, data); }); }; - module.processSortedSet = function(setKey, process, batch, callback) { + module.processSortedSet = function (setKey, process, batch, callback) { var done = false; var ids = []; var cursor = db.collection('objects').find({_key: setKey}) @@ -568,11 +611,11 @@ module.exports = function(db, module) { .batchSize(batch); async.whilst( - function() { + function () { return !done; }, - function(next) { - cursor.next(function(err, item) { + function (next) { + cursor.next(function (err, item) { if (err) { return next(err); } @@ -586,7 +629,7 @@ module.exports = function(db, module) { return next(null); } - process(ids, function(err) { + process(ids, function (err) { ids = []; return next(err); }); @@ -595,4 +638,99 @@ module.exports = function(db, module) { callback ); }; + + + module.sortedSetIntersectCard = function (keys, callback) { + if (!Array.isArray(keys) || !keys.length) { + return callback(null, 0); + } + + var pipeline = [ + { $match: { _key: {$in: keys}} }, + { $group: { _id: {value: '$value'}, count: {$sum: 1}} }, + { $match: { count: keys.length} }, + { $group: { _id: null, count: { $sum: 1 } } } + ]; + + db.collection('objects').aggregate(pipeline, function (err, data) { + callback(err, Array.isArray(data) && data.length ? data[0].count : 0); + }); + }; + + module.getSortedSetIntersect = function (params, callback) { + params.sort = 1; + getSortedSetRevIntersect(params, callback); + }; + + module.getSortedSetRevIntersect = function (params, callback) { + params.sort = -1; + getSortedSetRevIntersect(params, callback); + }; + + function getSortedSetRevIntersect(params, callback) { + var sets = params.sets; + var start = params.hasOwnProperty('start') ? params.start : 0; + var stop = params.hasOwnProperty('stop') ? params.stop : -1; + var weights = params.weights || []; + var aggregate = {}; + + if (params.aggregate) { + aggregate['$' + params.aggregate.toLowerCase()] = '$score'; + } else { + aggregate.$sum = '$score'; + } + + var limit = stop - start + 1; + if (limit <= 0) { + limit = 0; + } + + var pipeline = [{ $match: { _key: {$in: sets}} }]; + + weights.forEach(function (weight, index) { + if (weight !== 1) { + pipeline.push({ + $project: { + value: 1, + score: { + $cond: { if: { $eq: [ "$_key", sets[index] ] }, then: { $multiply: [ '$score', weight ] }, else: '$score' } + } + } + }); + } + }); + + pipeline.push({ $group: { _id: {value: '$value'}, totalScore: aggregate, count: {$sum: 1}} }); + pipeline.push({ $match: { count: sets.length} }); + pipeline.push({ $sort: { totalScore: params.sort} }); + + if (start) { + pipeline.push({ $skip: start }); + } + + if (limit > 0) { + pipeline.push({ $limit: limit }); + } + + var project = { _id: 0, value: '$_id.value'}; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); + + db.collection('objects').aggregate(pipeline, function (err, data) { + if (err || !data) { + return callback(err); + } + + if (!params.withScores) { + data = data.map(function (item) { + return item.value; + }); + } + + callback(null, data); + }); + } + }; diff --git a/src/database/redis.js b/src/database/redis.js index 239090aa95..3b05148c9e 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -1,6 +1,6 @@ 'use strict'; -(function(module) { +(function (module) { var winston = require('winston'), nconf = require('nconf'), @@ -25,7 +25,8 @@ name: 'redis:password', description: 'Password of your Redis database', hidden: true, - before: function(value) { value = value || nconf.get('redis:password') || ''; return value; } + default: nconf.get('redis:password') || '', + before: function (value) { value = value || nconf.get('redis:password') || ''; return value; } }, { name: "redis:database", @@ -34,7 +35,7 @@ } ]; - module.init = function(callback) { + module.init = function (callback) { try { redis = require('redis'); connectRedis = require('connect-redis')(session); @@ -63,16 +64,19 @@ } }; - module.connect = function(options) { - var redis_socket_or_host = nconf.get('redis:host'), - cxn, dbIdx; - - options = options || {}; + module.connect = function (options) { + var redis_socket_or_host = nconf.get('redis:host'); + var cxn; if (!redis) { redis = require('redis'); } + options = options || {}; + if (nconf.get('redis:password')) { + options.auth_pass = nconf.get('redis:password'); + } + if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) { /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ cxn = redis.createClient(nconf.get('redis:host'), options); @@ -90,9 +94,9 @@ cxn.auth(nconf.get('redis:password')); } - dbIdx = parseInt(nconf.get('redis:database'), 10); + var dbIdx = parseInt(nconf.get('redis:database'), 10); if (dbIdx) { - cxn.select(dbIdx, function(error) { + cxn.select(dbIdx, function (error) { if(error) { winston.error("NodeBB could not connect to your Redis database. Redis returned the following error: " + error.message); process.exit(); @@ -103,8 +107,8 @@ return cxn; }; - module.checkCompatibility = function(callback) { - module.info(module.client, function(err, info) { + module.checkCompatibility = function (callback) { + module.info(module.client, function (err, info) { if (err) { return callback(err); } @@ -118,11 +122,11 @@ }); }; - module.close = function() { + module.close = function () { redisClient.quit(); }; - module.info = function(cxn, callback) { + module.info = function (cxn, callback) { cxn.info(function (err, data) { if (err) { return callback(err); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index ce79d0bc6f..a728852f1a 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -1,50 +1,50 @@ "use strict"; -module.exports = function(redisClient, module) { +module.exports = function (redisClient, module) { var helpers = module.helpers.redis; - module.setObject = function(key, data, callback) { - callback = callback || function() {}; - redisClient.hmset(key, data, function(err) { + module.setObject = function (key, data, callback) { + callback = callback || function () {}; + redisClient.hmset(key, data, function (err) { callback(err); }); }; - module.setObjectField = function(key, field, value, callback) { - callback = callback || function() {}; - redisClient.hset(key, field, value, function(err) { + module.setObjectField = function (key, field, value, callback) { + callback = callback || function () {}; + redisClient.hset(key, field, value, function (err) { callback(err); }); }; - module.getObject = function(key, callback) { + module.getObject = function (key, callback) { redisClient.hgetall(key, callback); }; - module.getObjects = function(keys, callback) { + module.getObjects = function (keys, callback) { helpers.multiKeys(redisClient, 'hgetall', keys, callback); }; - module.getObjectField = function(key, field, callback) { - module.getObjectFields(key, [field], function(err, data) { + module.getObjectField = function (key, field, callback) { + module.getObjectFields(key, [field], function (err, data) { callback(err, data ? data[field] : null); }); }; - module.getObjectFields = function(key, fields, callback) { - module.getObjectsFields([key], fields, function(err, results) { + module.getObjectFields = function (key, fields, callback) { + module.getObjectsFields([key], fields, function (err, results) { callback(err, results ? results[0] : null); }); }; - module.getObjectsFields = function(keys, fields, callback) { + module.getObjectsFields = function (keys, fields, callback) { if (!Array.isArray(fields) || !fields.length) { - return callback(null, keys.map(function() { return {}; })); + return callback(null, keys.map(function () { return {}; })); } var multi = redisClient.multi(); - for(var x=0; x<keys.length; ++x) { + for(var x = 0; x < keys.length; ++x) { multi.hmget.apply(multi, [keys[x]].concat(fields)); } @@ -57,7 +57,7 @@ module.exports = function(redisClient, module) { return obj; } - multi.exec(function(err, results) { + multi.exec(function (err, results) { if (err) { return callback(err); } @@ -67,48 +67,48 @@ module.exports = function(redisClient, module) { }); }; - module.getObjectKeys = function(key, callback) { + module.getObjectKeys = function (key, callback) { redisClient.hkeys(key, callback); }; - module.getObjectValues = function(key, callback) { + module.getObjectValues = function (key, callback) { redisClient.hvals(key, callback); }; - module.isObjectField = function(key, field, callback) { - redisClient.hexists(key, field, function(err, exists) { + module.isObjectField = function (key, field, callback) { + redisClient.hexists(key, field, function (err, exists) { callback(err, exists === 1); }); }; - module.isObjectFields = function(key, fields, callback) { - helpers.multiKeyValues(redisClient, 'hexists', key, fields, function(err, results) { + module.isObjectFields = function (key, fields, callback) { + helpers.multiKeyValues(redisClient, 'hexists', key, fields, function (err, results) { callback(err, Array.isArray(results) ? helpers.resultsToBool(results) : null); }); }; - module.deleteObjectField = function(key, field, callback) { - callback = callback || function() {}; - redisClient.hdel(key, field, function(err, res) { + module.deleteObjectField = function (key, field, callback) { + callback = callback || function () {}; + redisClient.hdel(key, field, function (err, res) { callback(err); }); }; - module.deleteObjectFields = function(key, fields, callback) { - helpers.multiKeyValues(redisClient, 'hdel', key, fields, function(err, results) { + module.deleteObjectFields = function (key, fields, callback) { + helpers.multiKeyValues(redisClient, 'hdel', key, fields, function (err, results) { callback(err); }); }; - module.incrObjectField = function(key, field, callback) { + module.incrObjectField = function (key, field, callback) { redisClient.hincrby(key, field, 1, callback); }; - module.decrObjectField = function(key, field, callback) { + module.decrObjectField = function (key, field, callback) { redisClient.hincrby(key, field, -1, callback); }; - module.incrObjectFieldBy = function(key, field, value, callback) { + module.incrObjectFieldBy = function (key, field, value, callback) { redisClient.hincrby(key, field, value, callback); }; }; \ No newline at end of file diff --git a/src/database/redis/helpers.js b/src/database/redis/helpers.js index f1fe02b3be..a4ca7e116f 100644 --- a/src/database/redis/helpers.js +++ b/src/database/redis/helpers.js @@ -2,35 +2,35 @@ var helpers = {}; -helpers.multiKeys = function(redisClient, command, keys, callback) { - callback = callback || function() {}; +helpers.multiKeys = function (redisClient, command, keys, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for (var i=0; i<keys.length; ++i) { + for (var i = 0; i < keys.length; ++i) { multi[command](keys[i]); } multi.exec(callback); }; -helpers.multiKeysValue = function(redisClient, command, keys, value, callback) { - callback = callback || function() {}; +helpers.multiKeysValue = function (redisClient, command, keys, value, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for (var i=0; i<keys.length; ++i) { + for (var i = 0; i < keys.length; ++i) { multi[command](keys[i], value); } multi.exec(callback); }; -helpers.multiKeyValues = function(redisClient, command, key, values, callback) { - callback = callback || function() {}; +helpers.multiKeyValues = function (redisClient, command, key, values, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for (var i=0; i<values.length; ++i) { + for (var i = 0; i < values.length; ++i) { multi[command](key, values[i]); } multi.exec(callback); }; -helpers.resultsToBool = function(results) { - for (var i=0; i<results.length; ++i) { +helpers.resultsToBool = function (results) { + for (var i = 0; i < results.length; ++i) { results[i] = results[i] === 1; } return results; diff --git a/src/database/redis/list.js b/src/database/redis/list.js index 7ebc068d53..c44c23b475 100644 --- a/src/database/redis/list.js +++ b/src/database/redis/list.js @@ -1,41 +1,41 @@ "use strict"; -module.exports = function(redisClient, module) { - module.listPrepend = function(key, value, callback) { - callback = callback || function() {}; - redisClient.lpush(key, value, function(err, res) { +module.exports = function (redisClient, module) { + module.listPrepend = function (key, value, callback) { + callback = callback || function () {}; + redisClient.lpush(key, value, function (err, res) { callback(err); }); }; - module.listAppend = function(key, value, callback) { - callback = callback || function() {}; - redisClient.rpush(key, value, function(err, res) { + module.listAppend = function (key, value, callback) { + callback = callback || function () {}; + redisClient.rpush(key, value, function (err, res) { callback(err); }); }; - module.listRemoveLast = function(key, callback) { - callback = callback || function() {}; + module.listRemoveLast = function (key, callback) { + callback = callback || function () {}; redisClient.rpop(key, callback); }; - module.listRemoveAll = function(key, value, callback) { - callback = callback || function() {}; - redisClient.lrem(key, 0, value, function(err, res) { + module.listRemoveAll = function (key, value, callback) { + callback = callback || function () {}; + redisClient.lrem(key, 0, value, function (err, res) { callback(err); }); }; - module.listTrim = function(key, start, stop, callback) { - callback = callback || function() {}; - redisClient.ltrim(key, start, stop, function(err, res) { + module.listTrim = function (key, start, stop, callback) { + callback = callback || function () {}; + redisClient.ltrim(key, start, stop, function (err, res) { callback(err); }); }; - module.getListRange = function(key, start, stop, callback) { - callback = callback || function() {}; + module.getListRange = function (key, start, stop, callback) { + callback = callback || function () {}; redisClient.lrange(key, start, stop, callback); }; }; \ No newline at end of file diff --git a/src/database/redis/main.js b/src/database/redis/main.js index 7fc2890d87..66379f1774 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -1,86 +1,86 @@ "use strict"; -module.exports = function(redisClient, module) { +module.exports = function (redisClient, module) { - module.flushdb = function(callback) { - redisClient.send_command('flushdb', [], function(err) { + module.flushdb = function (callback) { + redisClient.send_command('flushdb', [], function (err) { if (typeof callback === 'function') { callback(err); } }); }; - module.exists = function(key, callback) { - redisClient.exists(key, function(err, exists) { + module.exists = function (key, callback) { + redisClient.exists(key, function (err, exists) { callback(err, exists === 1); }); }; - module.delete = function(key, callback) { - callback = callback || function() {}; - redisClient.del(key, function(err, res) { + module.delete = function (key, callback) { + callback = callback || function () {}; + redisClient.del(key, function (err, res) { callback(err); }); }; - module.deleteAll = function(keys, callback) { - callback = callback || function() {}; + module.deleteAll = function (keys, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { multi.del(keys[i]); } - multi.exec(function(err, res) { + multi.exec(function (err, res) { callback(err); }); }; - module.get = function(key, callback) { + module.get = function (key, callback) { redisClient.get(key, callback); }; - module.set = function(key, value, callback) { - callback = callback || function() {}; - redisClient.set(key, value, function(err) { + module.set = function (key, value, callback) { + callback = callback || function () {}; + redisClient.set(key, value, function (err) { callback(err); }); }; - module.increment = function(key, callback) { - callback = callback || function() {}; + module.increment = function (key, callback) { + callback = callback || function () {}; redisClient.incr(key, callback); }; - module.rename = function(oldKey, newKey, callback) { - callback = callback || function() {}; - redisClient.rename(oldKey, newKey, function(err, res) { + module.rename = function (oldKey, newKey, callback) { + callback = callback || function () {}; + redisClient.rename(oldKey, newKey, function (err, res) { callback(err && err.message !== 'ERR no such key' ? err : null); }); }; - module.expire = function(key, seconds, callback) { - callback = callback || function() {}; - redisClient.expire(key, seconds, function(err) { + module.expire = function (key, seconds, callback) { + callback = callback || function () {}; + redisClient.expire(key, seconds, function (err) { callback(err); }); }; - module.expireAt = function(key, timestamp, callback) { - callback = callback || function() {}; - redisClient.expireat(key, timestamp, function(err) { + module.expireAt = function (key, timestamp, callback) { + callback = callback || function () {}; + redisClient.expireat(key, timestamp, function (err) { callback(err); }); }; - module.pexpire = function(key, ms, callback) { - callback = callback || function() {}; - redisClient.pexpire(key, ms, function(err) { + module.pexpire = function (key, ms, callback) { + callback = callback || function () {}; + redisClient.pexpire(key, ms, function (err) { callback(err); }); }; - module.pexpireAt = function(key, timestamp, callback) { - callback = callback || function() {}; - redisClient.pexpireat(key, timestamp, function(err) { + module.pexpireAt = function (key, timestamp, callback) { + callback = callback || function () {}; + redisClient.pexpireat(key, timestamp, function (err) { callback(err); }); }; diff --git a/src/database/redis/sets.js b/src/database/redis/sets.js index 9443ed928f..5b2b803868 100644 --- a/src/database/redis/sets.js +++ b/src/database/redis/sets.js @@ -1,72 +1,78 @@ "use strict"; -module.exports = function(redisClient, module) { +module.exports = function (redisClient, module) { var helpers = module.helpers.redis; - module.setAdd = function(key, value, callback) { - callback = callback || function() {}; - redisClient.sadd(key, value, function(err, res) { + module.setAdd = function (key, value, callback) { + callback = callback || function () {}; + if (!Array.isArray(value)) { + value = [value]; + } + if (!value.length) { + return callback(); + } + redisClient.sadd(key, value, function (err, res) { callback(err); }); }; - module.setsAdd = function(keys, value, callback) { - callback = callback || function() {}; - helpers.multiKeysValue(redisClient, 'sadd', keys, value, function(err, res) { + module.setsAdd = function (keys, value, callback) { + callback = callback || function () {}; + helpers.multiKeysValue(redisClient, 'sadd', keys, value, function (err, res) { callback(err); }); }; - module.setRemove = function(key, value, callback) { - callback = callback || function() {}; - redisClient.srem(key, value, function(err, res) { + module.setRemove = function (key, value, callback) { + callback = callback || function () {}; + redisClient.srem(key, value, function (err, res) { callback(err); }); }; - module.setsRemove = function(keys, value, callback) { - callback = callback || function() {}; - helpers.multiKeysValue(redisClient, 'srem', keys, value, function(err, res) { + module.setsRemove = function (keys, value, callback) { + callback = callback || function () {}; + helpers.multiKeysValue(redisClient, 'srem', keys, value, function (err, res) { callback(err); }); }; - module.isSetMember = function(key, value, callback) { - redisClient.sismember(key, value, function(err, result) { + module.isSetMember = function (key, value, callback) { + redisClient.sismember(key, value, function (err, result) { callback(err, result === 1); }); }; - module.isSetMembers = function(key, values, callback) { - helpers.multiKeyValues(redisClient, 'sismember', key, values, function(err, results) { + module.isSetMembers = function (key, values, callback) { + helpers.multiKeyValues(redisClient, 'sismember', key, values, function (err, results) { callback(err, results ? helpers.resultsToBool(results) : null); }); }; - module.isMemberOfSets = function(sets, value, callback) { - helpers.multiKeysValue(redisClient, 'sismember', sets, value, function(err, results) { + module.isMemberOfSets = function (sets, value, callback) { + helpers.multiKeysValue(redisClient, 'sismember', sets, value, function (err, results) { callback(err, results ? helpers.resultsToBool(results) : null); }); }; - module.getSetMembers = function(key, callback) { + module.getSetMembers = function (key, callback) { redisClient.smembers(key, callback); }; - module.getSetsMembers = function(keys, callback) { + module.getSetsMembers = function (keys, callback) { helpers.multiKeys(redisClient, 'smembers', keys, callback); }; - module.setCount = function(key, callback) { + module.setCount = function (key, callback) { redisClient.scard(key, callback); }; - module.setsCount = function(keys, callback) { + module.setsCount = function (keys, callback) { helpers.multiKeys(redisClient, 'scard', keys, callback); }; - module.setRemoveRandom = function(key, callback) { - callback = callback || function() {}; + module.setRemoveRandom = function (key, callback) { + callback = callback || function () {}; redisClient.spop(key, callback); }; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 0341c043f7..e02366c08f 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -1,15 +1,15 @@ "use strict"; -module.exports = function(redisClient, module) { +module.exports = function (redisClient, module) { var helpers = module.helpers.redis; - module.sortedSetAdd = function(key, score, value, callback) { - callback = callback || function() {}; + module.sortedSetAdd = function (key, score, value, callback) { + callback = callback || function () {}; if (Array.isArray(score) && Array.isArray(value)) { return sortedSetAddMulti(key, score, value, callback); } - redisClient.zadd(key, score, value, function(err) { + redisClient.zadd(key, score, value, function (err) { callback(err); }); }; @@ -25,75 +25,75 @@ module.exports = function(redisClient, module) { var args = [key]; - for(var i=0; i<scores.length; ++i) { + for(var i = 0; i < scores.length; ++i) { args.push(scores[i], values[i]); } - redisClient.zadd(args, function(err, res) { + redisClient.zadd(args, function (err) { callback(err); }); } - module.sortedSetsAdd = function(keys, score, value, callback) { - callback = callback || function() {}; + module.sortedSetsAdd = function (keys, score, value, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { multi.zadd(keys[i], score, value); } - multi.exec(function(err, res) { + multi.exec(function (err) { callback(err); }); }; - module.sortedSetRemove = function(key, value, callback) { - callback = callback || function() {}; + module.sortedSetRemove = function (key, value, callback) { + callback = callback || function () {}; if (!Array.isArray(value)) { value = [value]; } - helpers.multiKeyValues(redisClient, 'zrem', key, value, function(err, result) { + helpers.multiKeyValues(redisClient, 'zrem', key, value, function (err) { callback(err); }); }; - module.sortedSetsRemove = function(keys, value, callback) { - helpers.multiKeysValue(redisClient, 'zrem', keys, value, function(err, result) { + module.sortedSetsRemove = function (keys, value, callback) { + helpers.multiKeysValue(redisClient, 'zrem', keys, value, function (err) { callback(err); }); }; - module.sortedSetsRemoveRangeByScore = function(keys, min, max, callback) { - callback = callback || function() {}; + module.sortedSetsRemoveRangeByScore = function (keys, min, max, callback) { + callback = callback || function () {}; var multi = redisClient.multi(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { multi.zremrangebyscore(keys[i], min, max); } - multi.exec(function(err, result) { + multi.exec(function (err) { callback(err); }); }; - module.getSortedSetRange = function(key, start, stop, callback) { + module.getSortedSetRange = function (key, start, stop, callback) { sortedSetRange('zrange', key, start, stop, false, callback); }; - module.getSortedSetRevRange = function(key, start, stop, callback) { + module.getSortedSetRevRange = function (key, start, stop, callback) { sortedSetRange('zrevrange', key, start, stop, false, callback); }; - module.getSortedSetRangeWithScores = function(key, start, stop, callback) { + module.getSortedSetRangeWithScores = function (key, start, stop, callback) { sortedSetRange('zrange', key, start, stop, true, callback); }; - module.getSortedSetRevRangeWithScores = function(key, start, stop, callback) { + module.getSortedSetRevRangeWithScores = function (key, start, stop, callback) { sortedSetRange('zrevrange', key, start, stop, true, callback); }; function sortedSetRange(method, key, start, stop, withScores, callback) { if (Array.isArray(key)) { - return sortedSetUnion(method, key, start, stop, withScores, callback); + return sortedSetUnion({method: method, sets: key, start: start, stop: stop, withScores: withScores}, callback); } var params = [key, start, stop]; @@ -101,7 +101,7 @@ module.exports = function(redisClient, module) { params.push('WITHSCORES'); } - redisClient[method](params, function(err, data) { + redisClient[method](params, function (err, data) { if (err) { return callback(err); } @@ -109,105 +109,105 @@ module.exports = function(redisClient, module) { return callback(null, data); } var objects = []; - for(var i=0; i<data.length; i+=2) { - objects.push({value: data[i], score: data[i + 1]}); + for(var i = 0; i < data.length; i += 2) { + objects.push({value: data[i], score: parseInt(data[i + 1], 10)}); } callback(null, objects); }); } - module.getSortedSetRangeByScore = function(key, start, count, min, max, callback) { + module.getSortedSetRangeByScore = function (key, start, count, min, max, callback) { redisClient.zrangebyscore([key, min, max, 'LIMIT', start, count], callback); }; - module.getSortedSetRevRangeByScore = function(key, start, count, max, min, callback) { + module.getSortedSetRevRangeByScore = function (key, start, count, max, min, callback) { redisClient.zrevrangebyscore([key, max, min, 'LIMIT', start, count], callback); }; - module.getSortedSetRangeByScoreWithScores = function(key, start, count, min, max, callback) { + module.getSortedSetRangeByScoreWithScores = function (key, start, count, min, max, callback) { sortedSetRangeByScoreWithScores('zrangebyscore', key, start, count, min, max, callback); }; - module.getSortedSetRevRangeByScoreWithScores = function(key, start, count, max, min, callback) { + module.getSortedSetRevRangeByScoreWithScores = function (key, start, count, max, min, callback) { sortedSetRangeByScoreWithScores('zrevrangebyscore', key, start, count, max, min, callback); }; function sortedSetRangeByScoreWithScores(method, key, start, count, min, max, callback) { - redisClient[method]([key, min, max, 'WITHSCORES', 'LIMIT', start, count], function(err, data) { + redisClient[method]([key, min, max, 'WITHSCORES', 'LIMIT', start, count], function (err, data) { if (err) { return callback(err); } var objects = []; - for(var i=0; i<data.length; i+=2) { - objects.push({value: data[i], score: data[i+1]}); + for(var i = 0; i < data.length; i += 2) { + objects.push({value: data[i], score: parseInt(data[i + 1], 10)}); } callback(null, objects); }); } - module.sortedSetCount = function(key, min, max, callback) { + module.sortedSetCount = function (key, min, max, callback) { redisClient.zcount(key, min, max, callback); }; - module.sortedSetCard = function(key, callback) { + module.sortedSetCard = function (key, callback) { redisClient.zcard(key, callback); }; - module.sortedSetsCard = function(keys, callback) { + module.sortedSetsCard = function (keys, callback) { if (Array.isArray(keys) && !keys.length) { return callback(null, []); } var multi = redisClient.multi(); - for(var i=0; i<keys.length; ++i) { + for(var i = 0; i < keys.length; ++i) { multi.zcard(keys[i]); } multi.exec(callback); }; - module.sortedSetRank = function(key, value, callback) { + module.sortedSetRank = function (key, value, callback) { redisClient.zrank(key, value, callback); }; - module.sortedSetsRanks = function(keys, values, callback) { + module.sortedSetsRanks = function (keys, values, callback) { var multi = redisClient.multi(); - for(var i=0; i<values.length; ++i) { + for(var i = 0; i < values.length; ++i) { multi.zrank(keys[i], values[i]); } multi.exec(callback); }; - module.sortedSetRanks = function(key, values, callback) { + module.sortedSetRanks = function (key, values, callback) { var multi = redisClient.multi(); - for(var i=0; i<values.length; ++i) { + for(var i = 0; i < values.length; ++i) { multi.zrank(key, values[i]); } multi.exec(callback); }; - module.sortedSetRevRank = function(key, value, callback) { + module.sortedSetRevRank = function (key, value, callback) { redisClient.zrevrank(key, value, callback); }; - module.sortedSetScore = function(key, value, callback) { + module.sortedSetScore = function (key, value, callback) { redisClient.zscore(key, value, callback); }; - module.sortedSetsScore = function(keys, value, callback) { + module.sortedSetsScore = function (keys, value, callback) { helpers.multiKeysValue(redisClient, 'zscore', keys, value, callback); }; - module.sortedSetScores = function(key, values, callback) { + module.sortedSetScores = function (key, values, callback) { helpers.multiKeyValues(redisClient, 'zscore', key, values, callback); }; - module.isSortedSetMember = function(key, value, callback) { - module.sortedSetScore(key, value, function(err, score) { + module.isSortedSetMember = function (key, value, callback) { + module.sortedSetScore(key, value, function (err, score) { callback(err, !!score); }); }; - module.isSortedSetMembers = function(key, values, callback) { - helpers.multiKeyValues(redisClient, 'zscore', key, values, function(err, results) { + module.isSortedSetMembers = function (key, values, callback) { + helpers.multiKeyValues(redisClient, 'zscore', key, values, function (err, results) { if (err) { return callback(err); } @@ -215,8 +215,8 @@ module.exports = function(redisClient, module) { }); }; - module.isMemberOfSortedSets = function(keys, value, callback) { - helpers.multiKeysValue(redisClient, 'zscore', keys, value, function(err, results) { + module.isMemberOfSortedSets = function (keys, value, callback) { + helpers.multiKeysValue(redisClient, 'zscore', keys, value, function (err, results) { if (err) { return callback(err); } @@ -224,56 +224,75 @@ module.exports = function(redisClient, module) { }); }; - module.getSortedSetsMembers = function(keys, callback) { + module.getSortedSetsMembers = function (keys, callback) { var multi = redisClient.multi(); - for (var i=0; i<keys.length; ++i) { + for (var i = 0; i < keys.length; ++i) { multi.zrange(keys[i], 0, -1); } multi.exec(callback); }; - module.getSortedSetUnion = function(sets, start, stop, callback) { - sortedSetUnion('zrange', sets, start, stop, false, callback); + + module.sortedSetUnionCard = function (keys, callback) { + var tempSetName = 'temp_' + Date.now(); + + var multi = redisClient.multi(); + multi.zunionstore([tempSetName, keys.length].concat(keys)); + multi.zcard(tempSetName); + multi.del(tempSetName); + multi.exec(function (err, results) { + if (err) { + return callback(err); + } + + callback(null, Array.isArray(results) && results.length ? results[1] : 0); + }); + }; + + module.getSortedSetUnion = function (params, callback) { + params.method = 'zrange'; + sortedSetUnion(params, callback); }; - module.getSortedSetRevUnion = function(sets, start, stop, callback) { - sortedSetUnion('zrevrange', sets, start, stop, false, callback); + module.getSortedSetRevUnion = function (params, callback) { + params.method = 'zrevrange'; + sortedSetUnion(params, callback); }; - function sortedSetUnion(method, sets, start, stop, withScores, callback) { + function sortedSetUnion(params, callback) { var tempSetName = 'temp_' + Date.now(); - var params = [tempSetName, start, stop]; - if (withScores) { - params.push('WITHSCORES'); + var rangeParams = [tempSetName, params.start, params.stop]; + if (params.withScores) { + rangeParams.push('WITHSCORES'); } - var multi = redisClient.multi(); - multi.zunionstore([tempSetName, sets.length].concat(sets)); - multi[method](params); + var multi = redisClient.multi(); + multi.zunionstore([tempSetName, params.sets.length].concat(params.sets)); + multi[params.method](rangeParams); multi.del(tempSetName); - multi.exec(function(err, results) { + multi.exec(function (err, results) { if (err) { return callback(err); } - if (!withScores) { + if (!params.withScores) { return callback(null, results ? results[1] : null); } results = results[1] || []; var objects = []; - for(var i=0; i<results.length; i+=2) { - objects.push({value: results[i], score: results[i + 1]}); + for(var i = 0; i < results.length; i += 2) { + objects.push({value: results[i], score: parseInt(results[i + 1], 10)}); } callback(null, objects); }); } - module.sortedSetIncrBy = function(key, increment, value, callback) { + module.sortedSetIncrBy = function (key, increment, value, callback) { redisClient.zincrby(key, increment, value, callback); }; - module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) { + module.getSortedSetRangeByLex = function (key, min, max, start, count, callback) { if (min !== '-') { min = '[' + min; } @@ -282,4 +301,78 @@ module.exports = function(redisClient, module) { } redisClient.zrangebylex([key, min, max, 'LIMIT', start, count], callback); }; + + module.sortedSetIntersectCard = function (keys, callback) { + if (!Array.isArray(keys) || !keys.length) { + return callback(null, 0); + } + var tempSetName = 'temp_' + Date.now(); + + var interParams = [tempSetName, keys.length].concat(keys); + + var multi = redisClient.multi(); + multi.zinterstore(interParams); + multi.zcard(tempSetName); + multi.del(tempSetName); + multi.exec(function (err, results) { + if (err) { + return callback(err); + } + + callback(null, results[1] || 0); + }); + }; + + module.getSortedSetIntersect = function (params, callback) { + params.method = 'zrange'; + getSortedSetRevIntersect(params, callback); + }; + + module.getSortedSetRevIntersect = function (params, callback) { + params.method = 'zrevrange'; + getSortedSetRevIntersect(params, callback); + }; + + function getSortedSetRevIntersect(params, callback) { + var sets = params.sets; + var start = params.hasOwnProperty('start') ? params.start : 0; + var stop = params.hasOwnProperty('stop') ? params.stop : -1; + var weights = params.weights || []; + + var tempSetName = 'temp_' + Date.now(); + + var interParams = [tempSetName, sets.length].concat(sets); + if (weights.length) { + interParams = interParams.concat(['WEIGHTS'].concat(weights)); + } + + if (params.aggregate) { + interParams = interParams.concat(['AGGREGATE', params.aggregate]); + } + + var rangeParams = [tempSetName, start, stop]; + if (params.withScores) { + rangeParams.push('WITHSCORES'); + } + + var multi = redisClient.multi(); + multi.zinterstore(interParams); + multi[params.method](rangeParams); + multi.del(tempSetName); + multi.exec(function (err, results) { + if (err) { + return callback(err); + } + + if (!params.withScores) { + return callback(null, results ? results[1] : null); + } + results = results[1] || []; + var objects = []; + for(var i = 0; i < results.length; i += 2) { + objects.push({value: results[i], score: parseFloat(results[i + 1])}); + } + callback(null, objects); + }); + } }; \ No newline at end of file diff --git a/src/emailer.js b/src/emailer.js index 34b5250908..4196fbb333 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -23,8 +23,8 @@ var transports = { var app; var fallbackTransport; -(function(Emailer) { - Emailer.registerApp = function(expressApp) { +(function (Emailer) { + Emailer.registerApp = function (expressApp) { app = expressApp; // Enable Gmail transport if enabled in ACP @@ -45,21 +45,21 @@ var fallbackTransport; return Emailer; }; - Emailer.send = function(template, uid, params, callback) { - callback = callback || function() {}; + Emailer.send = function (template, uid, params, callback) { + callback = callback || function () {}; if (!app) { winston.warn('[emailer] App not ready!'); return callback(); } async.waterfall([ - function(next) { + function (next) { async.parallel({ email: async.apply(User.getUserField, uid, 'email'), settings: async.apply(User.getSettings, uid) }, next); }, - function(results, next) { + function (results, next) { if (!results.email) { winston.warn('uid : ' + uid + ' has no email, not sending.'); return next(); @@ -70,19 +70,19 @@ var fallbackTransport; ], callback); }; - Emailer.sendToEmail = function(template, email, language, params, callback) { - callback = callback || function() {}; + Emailer.sendToEmail = function (template, email, language, params, callback) { + callback = callback || function () {}; var lang = language || meta.config.defaultLang || 'en_GB'; async.waterfall([ function (next) { async.parallel({ - html: function(next) { + html: function (next) { renderAndTranslate('emails/' + template, params, lang, next); }, - subject: function(next) { - translator.translate(params.subject, lang, function(translated) { + subject: function (next) { + translator.translate(params.subject, lang, function (translated) { next(null, translated); }); } @@ -90,6 +90,7 @@ var fallbackTransport; }, function (results, next) { var data = { + _raw: params, to: email, from: meta.config['email:from'] || 'no-reply@' + getHostname(), from_name: meta.config['email:from_name'] || 'NodeBB', @@ -113,11 +114,15 @@ var fallbackTransport; } } ], function (err) { - callback(err); + if (err && err.code === 'ENOENT') { + callback(new Error('[[error:sendmail-not-found]]')); + } else { + callback(err); + } }); }; - Emailer.sendViaFallback = function(data, callback) { + Emailer.sendViaFallback = function (data, callback) { // Some minor alterations to the data to conform to nodemailer standard data.text = data.plaintext; delete data.plaintext; @@ -140,8 +145,8 @@ var fallbackTransport; } function renderAndTranslate(tpl, params, lang, callback) { - render(tpl, params, function(err, html) { - translator.translate(html, lang, function(translated) { + render(tpl, params, function (err, html) { + translator.translate(html, lang, function (translated) { callback(err, translated); }); }); diff --git a/src/emitter.js b/src/emitter.js index 468f604aaf..ca262257b7 100644 --- a/src/emitter.js +++ b/src/emitter.js @@ -3,11 +3,11 @@ var eventEmitter = new (require('events')).EventEmitter(); -eventEmitter.all = function(events, callback) { +eventEmitter.all = function (events, callback) { var eventList = events.slice(0); events.forEach(function onEvent(event) { - eventEmitter.on(event, function() { + eventEmitter.on(event, function () { var index = eventList.indexOf(event); if (index === -1) { return; @@ -20,9 +20,9 @@ eventEmitter.all = function(events, callback) { }); }; -eventEmitter.any = function(events, callback) { +eventEmitter.any = function (events, callback) { events.forEach(function onEvent(event) { - eventEmitter.on(event, function() { + eventEmitter.on(event, function () { if (events !== null) { callback(); } diff --git a/src/events.js b/src/events.js index 968e1b2f39..cc1260bcef 100644 --- a/src/events.js +++ b/src/events.js @@ -1,70 +1,78 @@ 'use strict'; -var async = require('async'), - - db = require('./database'), - batch = require('./batch'), - user = require('./user'), - utils = require('../public/src/utils'); - - -(function(events) { - events.log = function(data, callback) { - callback = callback || function() {}; +var async = require('async'); +var validator = require('validator'); + +var db = require('./database'); +var batch = require('./batch'); +var user = require('./user'); +var utils = require('../public/src/utils'); + +(function (events) { + events.log = function (data, callback) { + callback = callback || function () {}; async.waterfall([ - function(next) { + function (next) { db.incrObjectField('global', 'nextEid', next); }, - function(eid, next) { + function (eid, next) { data.timestamp = Date.now(); data.eid = eid; async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('events:time', data.timestamp, eid, next); }, - function(next) { + function (next) { db.setObject('event:' + eid, data, next); } ], next); } - ], function(err, result) { + ], function (err, result) { callback(err); }); }; - events.getEvents = function(start, stop, callback) { + events.getEvents = function (start, stop, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('events:time', start, stop, next); }, - function(eids, next) { - var keys = eids.map(function(eid) { + function (eids, next) { + var keys = eids.map(function (eid) { return 'event:' + eid; }); db.getObjects(keys, next); }, - function(eventsData, next) { - eventsData.forEach(function(event) { + function (eventsData, next) { + addUserData(eventsData, 'uid', 'user', next); + }, + function (eventsData, next) { + addUserData(eventsData, 'targetUid', 'targetUser', next); + }, + function (eventsData, next) { + eventsData.forEach(function (event) { + Object.keys(event).forEach(function (key) { + if (typeof event[key] === 'string') { + event[key] = validator.escape(String(event[key] || '')); + } + }); var e = utils.merge(event); - e.eid = e.uid = e.type = e.ip = undefined; + e.eid = e.uid = e.type = e.ip = e.user = undefined; event.jsonString = JSON.stringify(e, null, 4); event.timestampISO = new Date(parseInt(event.timestamp, 10)).toUTCString(); }); - addUserData(eventsData, 'uid', 'user', next); - }, - function(eventsData, next) { - addUserData(eventsData, 'targetUid', 'targetUser', next); + next(null, eventsData); } ], callback); }; function addUserData(eventsData, field, objectName, callback) { - var uids = eventsData.map(function(event) { + var uids = eventsData.map(function (event) { return event && event[field]; - }).filter(function(uid, index, array) { + }).filter(function (uid, index, array) { return uid && array.indexOf(uid) === index; }); @@ -73,13 +81,13 @@ var async = require('async'), } async.parallel({ - isAdmin: function(next) { + isAdmin: function (next) { user.isAdministrator(uids, next); }, - userData: function(next) { + userData: function (next) { user.getUsersFields(uids, ['username', 'userslug', 'picture'], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -87,12 +95,12 @@ var async = require('async'), var userData = results.userData; var map = {}; - userData.forEach(function(user, index) { + userData.forEach(function (user, index) { user.isAdmin = results.isAdmin[index]; map[user.uid] = user; }); - eventsData.forEach(function(event) { + eventsData.forEach(function (event) { if (map[event[field]]) { event[objectName] = map[event[field]]; } @@ -101,26 +109,26 @@ var async = require('async'), }); } - events.deleteEvents = function(eids, callback) { - callback = callback || function() {}; + events.deleteEvents = function (eids, callback) { + callback = callback || function () {}; async.parallel([ - function(next) { - var keys = eids.map(function(eid) { + function (next) { + var keys = eids.map(function (eid) { return 'event:' + eid; }); db.deleteAll(keys, next); }, - function(next) { + function (next) { db.sortedSetRemove('events:time', eids, next); } ], callback); }; - events.deleteAll = function(callback) { - callback = callback || function() {}; + events.deleteAll = function (callback) { + callback = callback || function () {}; - batch.processSortedSet('events:time', function(eids, next) { - events.deleteEvents(eids, callback); + batch.processSortedSet('events:time', function (eids, next) { + events.deleteEvents(eids, next); }, {alwaysStartAt: 0}, callback); }; diff --git a/src/file.js b/src/file.js index 1c1cab7781..df820cc47f 100644 --- a/src/file.js +++ b/src/file.js @@ -1,35 +1,35 @@ "use strict"; -var fs = require('fs'), - nconf = require('nconf'), - path = require('path'), - winston = require('winston'), - jimp = require('jimp'), +var fs = require('fs'); +var nconf = require('nconf'); +var path = require('path'); +var winston = require('winston'); +var jimp = require('jimp'); - utils = require('../public/src/utils'); +var utils = require('../public/src/utils'); var file = {}; -file.saveFileToLocal = function(filename, folder, tempPath, callback) { +file.saveFileToLocal = function (filename, folder, tempPath, callback) { /* * remarkable doesn't allow spaces in hyperlinks, once that's fixed, remove this. */ filename = filename.split('.'); - filename.forEach(function(name, idx) { + filename.forEach(function (name, idx) { filename[idx] = utils.slugify(name); }); filename = filename.join('.'); var uploadPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), folder, filename); - winston.verbose('Saving file '+ filename +' to : ' + uploadPath); + winston.verbose('Saving file ' + filename + ' to : ' + uploadPath); var is = fs.createReadStream(tempPath); var os = fs.createWriteStream(uploadPath); - is.on('end', function () { callback(null, { - url: nconf.get('upload_url') + folder + '/' + filename + url: nconf.get('upload_url') + '/' + folder + '/' + filename, + path: uploadPath }); }); @@ -38,32 +38,39 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) { is.pipe(os); }; -file.base64ToLocal = function(imageData, uploadPath, callback) { +file.base64ToLocal = function (imageData, uploadPath, callback) { var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); uploadPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), uploadPath); fs.writeFile(uploadPath, buffer, { encoding: 'base64' - }, function(err) { + }, function (err) { callback(err, uploadPath); }); }; -file.isFileTypeAllowed = function(path, callback) { +file.isFileTypeAllowed = function (path, callback) { + var plugins = require('./plugins'); + if (plugins.hasListeners('filter:file.isFileTypeAllowed')) { + return plugins.fireHook('filter:file.isFileTypeAllowed', path, function (err) { + callback(err); + }); + } + // Attempt to read the file, if it passes, file type is allowed - jimp.read(path, function(err) { + jimp.read(path, function (err) { callback(err); }); }; -file.allowedExtensions = function() { +file.allowedExtensions = function () { var meta = require('./meta'); var allowedExtensions = (meta.config.allowedFileExtensions || '').trim(); if (!allowedExtensions) { return []; } allowedExtensions = allowedExtensions.split(','); - allowedExtensions = allowedExtensions.filter(Boolean).map(function(extension) { + allowedExtensions = allowedExtensions.filter(Boolean).map(function (extension) { extension = extension.trim(); if (!extension.startsWith('.')) { extension = '.' + extension; @@ -78,13 +85,13 @@ file.allowedExtensions = function() { return allowedExtensions; }; -file.exists = function(path, callback) { - fs.stat(path, function(err, stat) { +file.exists = function (path, callback) { + fs.stat(path, function (err, stat) { callback(!err && stat); }); }; -file.existsSync = function(path) { +file.existsSync = function (path) { var exists = false; try { exists = fs.statSync(path); @@ -95,4 +102,4 @@ file.existsSync = function(path) { return !!exists; }; -module.exports = file; \ No newline at end of file +module.exports = file; diff --git a/src/groups.js b/src/groups.js index 2df5dc3ce2..7d71a6e0fa 100644 --- a/src/groups.js +++ b/src/groups.js @@ -10,7 +10,7 @@ var posts = require('./posts'); var privileges = require('./privileges'); var utils = require('../public/src/utils'); -(function(Groups) { +(function (Groups) { require('./groups/create')(Groups); require('./groups/delete')(Groups); @@ -23,7 +23,7 @@ var utils = require('../public/src/utils'); var ephemeralGroups = ['guests'], internals = { - getEphemeralGroup: function(groupName) { + getEphemeralGroup: function (groupName) { return { name: groupName, slug: utils.slugify(groupName), @@ -33,7 +33,7 @@ var utils = require('../public/src/utils'); system: '1' }; }, - removeEphemeralGroups: function(groups) { + removeEphemeralGroups: function (groups) { var x = groups.length; while(x--) { if (ephemeralGroups.indexOf(groups[x]) !== -1) { @@ -48,15 +48,15 @@ var utils = require('../public/src/utils'); Groups.internals = internals; var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/; - Groups.isPrivilegeGroup = function(groupName) { + Groups.isPrivilegeGroup = function (groupName) { return isPrivilegeGroupRegex.test(groupName); }; - Groups.getEphemeralGroups = function() { + Groups.getEphemeralGroups = function () { return ephemeralGroups; }; - Groups.getGroupsFromSet = function(set, uid, start, stop, callback) { + Groups.getGroupsFromSet = function (set, uid, start, stop, callback) { var method; var args; if (set === 'groups:visible:name') { @@ -74,7 +74,7 @@ var utils = require('../public/src/utils'); } if (set === 'groups:visible:name') { - groupNames = groupNames.map(function(name) { + groupNames = groupNames.map(function (name) { return name.split(':')[1]; }); } @@ -83,16 +83,16 @@ var utils = require('../public/src/utils'); } }; - Groups.getGroups = function(set, start, stop, callback) { + Groups.getGroups = function (set, start, stop, callback) { db.getSortedSetRevRange(set, start, stop, callback); }; - Groups.getGroupsAndMembers = function(groupNames, callback) { + Groups.getGroupsAndMembers = function (groupNames, callback) { async.parallel({ - groups: function(next) { + groups: function (next) { Groups.getGroupsData(groupNames, next); }, - members: function(next) { + members: function (next) { Groups.getMemberUsers(groupNames, 0, 3, next); } }, function (err, data) { @@ -100,7 +100,7 @@ var utils = require('../public/src/utils'); return callback(err); } - data.groups.forEach(function(group, index) { + data.groups.forEach(function (group, index) { if (!group) { return; } @@ -113,7 +113,7 @@ var utils = require('../public/src/utils'); }); }; - Groups.get = function(groupName, options, callback) { + Groups.get = function (groupName, options, callback) { if (!groupName) { return callback(new Error('[[error:invalid-group]]')); } @@ -133,20 +133,20 @@ var utils = require('../public/src/utils'); }, pending: function (next) { async.waterfall([ - function(next) { + function (next) { db.getSetMembers('group:' + groupName + ':pending', next); }, - function(uids, next) { + function (uids, next) { user.getUsersData(uids, next); } ], next); }, invited: function (next) { async.waterfall([ - function(next) { + function (next) { db.getSetMembers('group:' + groupName + ':invited', next); }, - function(uids, next) { + function (uids, next) { user.getUsersData(uids, next); } ], next); @@ -165,7 +165,7 @@ var utils = require('../public/src/utils'); results.base['cover:url'] = results.base['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName); results.base['cover:position'] = results.base['cover:position'] || '50% 50%'; - plugins.fireHook('filter:parse.raw', results.base.description, function(err, descriptionParsed) { + plugins.fireHook('filter:parse.raw', results.base.description, function (err, descriptionParsed) { if (err) { return callback(err); } @@ -182,6 +182,7 @@ var utils = require('../public/src/utils'); 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.memberCount = parseInt(results.base.memberCount, 10); results.base.private = (results.base.private === null || results.base.private === undefined) ? true : !!parseInt(results.base.private, 10); results.base.disableJoinRequests = parseInt(results.base.disableJoinRequests, 10) === 1; results.base.isMember = results.isMember; @@ -189,25 +190,25 @@ var utils = require('../public/src/utils'); results.base.isInvited = results.isInvited; results.base.isOwner = results.isOwner; - plugins.fireHook('filter:group.get', {group: results.base}, function(err, data) { + plugins.fireHook('filter:group.get', {group: results.base}, function (err, data) { callback(err, data ? data.group : null); }); }); }); }; - Groups.getOwners = function(groupName, callback) { + Groups.getOwners = function (groupName, callback) { db.getSetMembers('group:' + groupName + ':owners', callback); }; - Groups.getOwnersAndMembers = function(groupName, uid, start, stop, callback) { + Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) { async.parallel({ owners: function (next) { async.waterfall([ - function(next) { + function (next) { db.getSetMembers('group:' + groupName + ':owners', next); }, - function(uids, next) { + function (uids, next) { user.getUsers(uids, uid, next); } ], next); @@ -215,20 +216,20 @@ var utils = require('../public/src/utils'); members: function (next) { user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var ownerUids = []; - results.owners.forEach(function(user) { + results.owners.forEach(function (user) { if (user) { user.isOwner = true; ownerUids.push(user.uid.toString()); } }); - results.members = results.members.filter(function(user) { + results.members = results.members.filter(function (user) { return user && user.uid && ownerUids.indexOf(user.uid.toString()) === -1; }); results.members = results.owners.concat(results.members); @@ -237,7 +238,7 @@ var utils = require('../public/src/utils'); }); }; - Groups.escapeGroupData = function(group) { + Groups.escapeGroupData = function (group) { if (group) { group.nameEncoded = encodeURIComponent(group.name); group.displayName = validator.escape(String(group.name)); @@ -246,36 +247,36 @@ var utils = require('../public/src/utils'); } }; - Groups.getByGroupslug = function(slug, options, callback) { - db.getObjectField('groupslug:groupname', slug, function(err, groupName) { + Groups.getByGroupslug = function (slug, options, callback) { + db.getObjectField('groupslug:groupname', slug, function (err, groupName) { if (err) { return callback(err); } else if (!groupName) { return callback(new Error('[[error:no-group]]')); } - Groups.get.call(Groups, groupName, options, callback); + Groups.get(groupName, options, callback); }); }; - Groups.getGroupNameByGroupSlug = function(slug, callback) { + Groups.getGroupNameByGroupSlug = function (slug, callback) { db.getObjectField('groupslug:groupname', slug, callback); }; - Groups.getGroupFields = function(groupName, fields, callback) { - Groups.getMultipleGroupFields([groupName], fields, function(err, groups) { + Groups.getGroupFields = function (groupName, fields, callback) { + Groups.getMultipleGroupFields([groupName], fields, function (err, groups) { callback(err, groups ? groups[0] : null); }); }; - Groups.getMultipleGroupFields = function(groups, fields, callback) { - db.getObjectsFields(groups.map(function(group) { + Groups.getMultipleGroupFields = function (groups, fields, callback) { + db.getObjectsFields(groups.map(function (group) { return 'group:' + group; }), fields, callback); }; - Groups.setGroupField = function(groupName, field, value, callback) { - db.setObjectField('group:' + groupName, field, value, function(err) { + Groups.setGroupField = function (groupName, field, value, callback) { + db.setObjectField('group:' + groupName, field, value, function (err) { if (err) { return callback(err); } @@ -284,45 +285,43 @@ var utils = require('../public/src/utils'); }); }; - Groups.isPrivate = function(groupName, callback) { - db.getObjectField('group:' + groupName, 'private', function(err, isPrivate) { - isPrivate = isPrivate || isPrivate === null; - - if (typeof isPrivate === 'string') { - isPrivate = (isPrivate === '0' ? false : true); + Groups.isPrivate = function (groupName, callback) { + db.getObjectField('group:' + groupName, 'private', function (err, isPrivate) { + if (err) { + return callback(err); } - callback(err, isPrivate); // Private, if not set at all + callback(null, (parseInt(isPrivate, 10) === 0) ? false : true); }); }; - Groups.isHidden = function(groupName, callback) { - Groups.getGroupFields(groupName, ['hidden'], function(err, values) { + Groups.isHidden = function (groupName, callback) { + db.getObjectField(groupName, 'hidden', function (err, isHidden) { if (err) { return callback(err); } - callback(null, parseInt(values.hidden, 10) === 1); + callback(null, parseInt(isHidden, 10) === 1); }); }; - Groups.exists = function(name, callback) { + Groups.exists = function (name, callback) { if (Array.isArray(name)) { - var slugs = name.map(function(groupName) { + var slugs = name.map(function (groupName) { return utils.slugify(groupName); }); async.parallel([ function (next) { - next(null, slugs.map(function(slug) { + next(null, slugs.map(function (slug) { return ephemeralGroups.indexOf(slug) !== -1; })); }, async.apply(db.isSortedSetMembers, 'groups:createtime', name) - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } - callback(null, name.map(function(n, index) { + callback(null, name.map(function (n, index) { return results[0][index] || results[1][index]; })); }); @@ -333,13 +332,13 @@ var utils = require('../public/src/utils'); next(null, ephemeralGroups.indexOf(slug) !== -1); }, async.apply(db.isSortedSetMember, 'groups:createtime', name) - ], function(err, results) { + ], function (err, results) { callback(err, !err ? (results[0] || results[1]) : null); }); } }; - Groups.existsBySlug = function(slug, callback) { + Groups.existsBySlug = function (slug, callback) { if (Array.isArray(slug)) { db.isObjectFields('groupslug:groupname', slug, callback); } else { @@ -347,61 +346,63 @@ var utils = require('../public/src/utils'); } }; - Groups.getLatestMemberPosts = function(groupName, max, uid, callback) { + Groups.getLatestMemberPosts = function (groupName, max, uid, callback) { async.waterfall([ - async.apply(Groups.getMembers, groupName, 0, -1), - function(uids, next) { + function (next) { + Groups.getMembers(groupName, 0, -1, next); + }, + function (uids, next) { if (!Array.isArray(uids) || !uids.length) { return callback(null, []); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':posts'; }); db.getSortedSetRevRange(keys, 0, max - 1, next); }, - function(pids, next) { + function (pids, next) { privileges.posts.filter('read', pids, uid, next); }, - function(pids, next) { + function (pids, next) { posts.getPostSummaryByPids(pids, uid, {stripTags: false}, next); } ], callback); }; - Groups.getGroupData = function(groupName, callback) { - Groups.getGroupsData([groupName], function(err, groupsData) { + Groups.getGroupData = function (groupName, callback) { + Groups.getGroupsData([groupName], function (err, groupsData) { callback(err, Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null); }); }; - Groups.getGroupsData = function(groupNames, callback) { + Groups.getGroupsData = function (groupNames, callback) { if (!Array.isArray(groupNames) || !groupNames.length) { return callback(null, []); } - var keys = groupNames.map(function(groupName) { + var keys = groupNames.map(function (groupName) { return 'group:' + groupName; }); - var ephemeralIdx = groupNames.reduce(function(memo, cur, idx) { + var ephemeralIdx = groupNames.reduce(function (memo, cur, idx) { if (ephemeralGroups.indexOf(cur) !== -1) { memo.push(idx); } return memo; }, []); - db.getObjects(keys, function(err, groupData) { + db.getObjects(keys, function (err, groupData) { if (err) { return callback(err); } if (ephemeralIdx.length) { - ephemeralIdx.forEach(function(idx) { + ephemeralIdx.forEach(function (idx) { groupData[idx] = internals.getEphemeralGroup(groupNames[idx]); }); } - groupData.forEach(function(group) { + groupData.forEach(function (group) { if (group) { Groups.escapeGroupData(group); group.userTitleEnabled = group.userTitleEnabled ? parseInt(group.userTitleEnabled, 10) === 1 : true; @@ -418,34 +419,34 @@ var utils = require('../public/src/utils'); } }); - plugins.fireHook('filter:groups.get', {groups: groupData}, function(err, data) { + plugins.fireHook('filter:groups.get', {groups: groupData}, function (err, data) { callback(err, data ? data.groups : null); }); }); }; - Groups.getUserGroups = function(uids, callback) { + Groups.getUserGroups = function (uids, callback) { Groups.getUserGroupsFromSet('groups:visible:createtime', uids, callback); }; Groups.getUserGroupsFromSet = function (set, uids, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange(set, 0, -1, next); }, - function(groupNames, next) { - var groupSets = groupNames.map(function(name) { + function (groupNames, next) { + var groupSets = groupNames.map(function (name) { return 'group:' + name + ':members'; }); - async.map(uids, function(uid, next) { - db.isMemberOfSortedSets(groupSets, uid, function(err, isMembers) { + async.map(uids, function (uid, next) { + db.isMemberOfSortedSets(groupSets, uid, function (err, isMembers) { if (err) { return next(err); } var memberOf = []; - isMembers.forEach(function(isMember, index) { + isMembers.forEach(function (isMember, index) { if (isMember) { memberOf.push(groupNames[index]); } diff --git a/src/groups/cover.js b/src/groups/cover.js index 89ef97df94..f2ed4e05ea 100644 --- a/src/groups/cover.js +++ b/src/groups/cover.js @@ -6,21 +6,23 @@ var path = require('path'); var fs = require('fs'); var crypto = require('crypto'); var Jimp = require('jimp'); +var mime = require('mime'); +var winston = require('winston'); var db = require('../database'); var file = require('../file'); var uploadsController = require('../controllers/uploads'); -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.updateCoverPosition = function(groupName, position, callback) { + Groups.updateCoverPosition = function (groupName, position, callback) { if (!groupName) { return callback(new Error('[[error:invalid-data]]')); } Groups.setGroupField(groupName, 'cover:position', position, callback); }; - Groups.updateCover = function(uid, data, callback) { + Groups.updateCover = function (uid, data, callback) { // Position only? That's fine if (!data.imageData && data.position) { @@ -29,6 +31,7 @@ module.exports = function(Groups) { var tempPath = data.file ? data.file : ''; var url; + var type = data.file ? mime.lookup(data.file) : 'image/png'; async.waterfall([ function (next) { @@ -41,7 +44,8 @@ module.exports = function(Groups) { tempPath = _tempPath; uploadsController.uploadGroupCover(uid, { name: 'groupCover', - path: tempPath + path: tempPath, + type: type }, next); }, function (uploadData, next) { @@ -54,7 +58,8 @@ module.exports = function(Groups) { function (next) { uploadsController.uploadGroupCover(uid, { name: 'groupCoverThumb', - path: tempPath + path: tempPath, + type: type }, next); }, function (uploadData, next) { @@ -65,13 +70,17 @@ module.exports = function(Groups) { } ], function (err) { if (err) { - return fs.unlink(tempPath, function(unlinkErr) { + return fs.unlink(tempPath, function (unlinkErr) { + if (unlinkErr) { + winston.error(unlinkErr); + } + callback(err); // send back original error }); } if (data.position) { - Groups.updateCoverPosition(data.groupName, data.position, function(err) { + Groups.updateCoverPosition(data.groupName, data.position, function (err) { callback(err, {url: url}); }); } else { @@ -104,18 +113,18 @@ module.exports = function(Groups) { md5sum = md5sum.digest('hex'); // Save image - var tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum); + var tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum) + '.png'; var buffer = new Buffer(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); fs.writeFile(tempPath, buffer, { encoding: 'base64' - }, function(err) { + }, function (err) { callback(err, tempPath); }); } - Groups.removeCover = function(data, callback) { - db.deleteObjectField('group:' + data.groupName, 'cover:url', callback); + Groups.removeCover = function (data, callback) { + db.deleteObjectFields('group:' + data.groupName, ['cover:url', 'cover:thumb:url'], callback); }; }; \ No newline at end of file diff --git a/src/groups/create.js b/src/groups/create.js index 2910d96294..b5864e5c44 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -1,15 +1,17 @@ 'use strict'; -var async = require('async'), - meta = require('../meta'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'), - db = require('../database'); +var async = require('async'); +var meta = require('../meta'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); +var db = require('../database'); -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.create = function(data, callback) { - var system = data.system === true || parseInt(data.system, 10) === 1 || data.name === 'administrators' || data.name === 'registered-users' || data.name === 'Global Moderators' || Groups.isPrivilegeGroup(data.name); + Groups.create = function (data, callback) { + var system = data.system === true || parseInt(data.system, 10) === 1 || + data.name === 'administrators' || data.name === 'registered-users' || data.name === 'Global Moderators' || + Groups.isPrivilegeGroup(data.name); var groupData; var timestamp = data.timestamp || Date.now(); @@ -79,6 +81,10 @@ module.exports = function(Groups) { return callback(new Error('[[error:group-name-too-short]]')); } + if (!Groups.isPrivilegeGroup(name) && name.length > (parseInt(meta.config.maximumGroupNameLength, 10) || 255)) { + return callback(new Error('[[error:group-name-too-long]]')); + } + if (name.indexOf('/') !== -1) { return callback(new Error('[[error:invalid-group-name]]')); } diff --git a/src/groups/delete.js b/src/groups/delete.js index 8e665249a8..0838dd2407 100644 --- a/src/groups/delete.js +++ b/src/groups/delete.js @@ -1,14 +1,14 @@ 'use strict'; -var async = require('async'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'), - db = require('./../database'); +var async = require('async'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); +var db = require('./../database'); -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.destroy = function(groupName, callback) { - Groups.getGroupsData([groupName], function(err, groupsData) { + Groups.destroy = function (groupName, callback) { + Groups.getGroupsData([groupName], function (err, groupsData) { if (err) { return callback(err); } @@ -16,6 +16,7 @@ module.exports = function(Groups) { return callback(); } var groupObj = groupsData[0]; + plugins.fireHook('action:group.destroy', groupObj); async.parallel([ @@ -29,17 +30,23 @@ module.exports = function(Groups) { 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) { - db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) { + function (next) { + db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) { if (err) { return next(err); } - async.each(groups, function(group, next) { + async.each(groups, function (group, next) { db.sortedSetRemove('group:' + group + ':members', groupName, next); }, next); }); } - ], callback); + ], function (err) { + if (err) { + return callback(err); + } + Groups.resetCache(); + callback(); + }); }); }; }; diff --git a/src/groups/membership.js b/src/groups/membership.js index 747fa9d3d7..d03dba3e59 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -1,86 +1,112 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - _ = require('underscore'), - - user = require('../user'), - utils = require('../../public/src/utils'), - plugins = require('../plugins'), - notifications = require('../notifications'), - db = require('./../database'); - -module.exports = function(Groups) { - Groups.join = function(groupName, uid, callback) { - function join() { - var tasks = [ - async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid), - async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount') - ]; +var async = require('async'); +var winston = require('winston'); +var _ = require('underscore'); - async.waterfall([ - function(next) { - async.parallel({ - isAdmin: function(next) { - user.isAdministrator(uid, next); - }, - isHidden: function(next) { - Groups.isHidden(groupName, next); - } - }, next); - }, - function(results, next) { - if (results.isAdmin) { - tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid)); - } - if (!results.isHidden) { - tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName)); - } - async.parallel(tasks, next); - }, - function(results, next) { - user.setGroupTitle(groupName, uid, next); - }, - function(next) { - plugins.fireHook('action:group.join', { - groupName: groupName, - uid: uid - }); - next(); - } - ], callback); - } +var user = require('../user'); +var utils = require('../../public/src/utils'); +var plugins = require('../plugins'); +var notifications = require('../notifications'); +var db = require('../database'); + +var pubsub = require('../pubsub'); +var LRU = require('lru-cache'); - callback = callback || function() {}; +var cache = LRU({ + max: 40000, + maxAge: 1000 * 60 * 60 +}); + +module.exports = function (Groups) { + + Groups.cache = cache; + + Groups.join = function (groupName, uid, callback) { + callback = callback || function () {}; if (!groupName) { return callback(new Error('[[error:invalid-data]]')); } - Groups.exists(groupName, function(err, exists) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + Groups.isMember(uid, groupName, next); + }, + function (isMember, next) { + if (isMember) { + return callback(); + } + Groups.exists(groupName, next); + }, + function (exists, next) { + if (exists) { + return next(); + } + Groups.create({ + name: groupName, + description: '', + hidden: 1 + }, function (err) { + if (err && err.message !== '[[error:group-already-exists]]') { + winston.error('[groups.join] Could not create new hidden group: ' + err.message); + return callback(err); + } + next(); + }); + }, + function (next) { + async.parallel({ + isAdmin: function (next) { + user.isAdministrator(uid, next); + }, + isHidden: function (next) { + Groups.isHidden(groupName, next); + } + }, next); + }, + function (results, next) { + var tasks = [ + async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid), + async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount') + ]; + if (results.isAdmin) { + tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid)); + } + if (!results.isHidden) { + tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName)); + } + async.parallel(tasks, next); + }, + function (results, next) { + clearCache(uid, groupName); + setGroupTitleIfNotSet(groupName, uid, next); + }, + function (next) { + plugins.fireHook('action:group.join', { + groupName: groupName, + uid: uid + }); + next(); } + ], callback); + }; - if (exists) { - return join(); + function setGroupTitleIfNotSet(groupName, uid, callback) { + if (groupName === 'registered-users' || Groups.isPrivilegeGroup(groupName)) { + return callback(); + } + + db.getObjectField('user:' + uid, 'groupTitle', function (err, currentTitle) { + if (err || (currentTitle || currentTitle === '')) { + return callback(err); } - Groups.create({ - name: groupName, - description: '', - hidden: 1 - }, function(err) { - if (err && err.message !== '[[error:group-already-exists]]') { - winston.error('[groups.join] Could not create new hidden group: ' + err.message); - return callback(err); - } - join(); - }); + user.setUserField(uid, 'groupTitle', groupName, callback); }); - }; + } - Groups.requestMembership = function(groupName, uid, callback) { + Groups.requestMembership = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'request'), function (next) { @@ -88,7 +114,7 @@ module.exports = function(Groups) { }, function (username, next) { async.parallel({ - notification: function(next) { + notification: function (next) { notifications.create({ bodyShort: '[[groups:request.notification_title, ' + username + ']]', bodyLong: '[[groups:request.notification_text, ' + username + ', ' + groupName + ']]', @@ -97,7 +123,7 @@ module.exports = function(Groups) { from: uid }, next); }, - owners: function(next) { + owners: function (next) { Groups.getOwners(groupName, next); } }, next); @@ -111,7 +137,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.acceptMembership = function(groupName, uid, callback) { + Groups.acceptMembership = function (groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.waterfall([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), @@ -120,7 +146,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.rejectMembership = function(groupName, uid, callback) { + Groups.rejectMembership = function (groupName, uid, callback) { // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.parallel([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), @@ -128,7 +154,7 @@ module.exports = function(Groups) { ], callback); }; - Groups.invite = function(groupName, uid, callback) { + Groups.invite = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'invite'), async.apply(notifications.create, { @@ -138,11 +164,7 @@ module.exports = function(Groups) { path: '/groups/' + utils.slugify(groupName) }), function (notification, next) { - if (!notification) { - return next(); - } - - notifications.push(notification, [uid]); + notifications.push(notification, [uid], next); } ], callback); }; @@ -155,7 +177,7 @@ module.exports = function(Groups) { var set = type === 'invite' ? 'group:' + groupName + ':invited' : 'group:' + groupName + ':pending'; async.waterfall([ - function(next) { + function (next) { async.parallel({ exists: async.apply(Groups.exists, groupName), isMember: async.apply(Groups.isMember, uid, groupName), @@ -163,20 +185,20 @@ module.exports = function(Groups) { isInvited: async.apply(Groups.isInvited, uid, groupName) }, next); }, - function(checks, next) { + function (checks, next) { if (!checks.exists) { return next(new Error('[[error:no-group]]')); } else if (checks.isMember) { - return next(new Error('[[error:group-already-member]]')); + return callback(); } else if (type === 'invite' && checks.isInvited) { - return next(new Error('[[error:group-already-invited]]')); + return callback(); } else if (type === 'request' && checks.isPending) { return next(new Error('[[error:group-already-requested]]')); } db.setAdd(set, uid, next); }, - function(next) { + function (next) { plugins.fireHook(hookName, { groupName: groupName, uid: uid @@ -186,53 +208,68 @@ module.exports = function(Groups) { ], callback); } - Groups.leave = function(groupName, uid, callback) { - callback = callback || function() {}; + Groups.leave = function (groupName, uid, callback) { + callback = callback || function () {}; - var tasks = [ - async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid), - async.apply(db.setRemove, 'group:' + groupName + ':owners', uid), - async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount') - ]; - - async.parallel(tasks, function(err) { - if (err) { - return callback(err); - } - - plugins.fireHook('action:group.leave', { - groupName: groupName, - uid: uid - }); - - Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) { - if (err || !groupData) { - return callback(err); + async.waterfall([ + function (next) { + Groups.isMember(uid, groupName, next); + }, + function (isMember, next) { + if (!isMember) { + return callback(); } - if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) { - Groups.destroy(groupName, callback); + Groups.exists(groupName, next); + }, + function (exists, next) { + if (!exists) { + return callback(); + } + async.parallel([ + async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid), + async.apply(db.setRemove, 'group:' + groupName + ':owners', uid), + async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount') + ], next); + }, + function (results, next) { + clearCache(uid, groupName); + Groups.getGroupFields(groupName, ['hidden', 'memberCount'], next); + }, + function (groupData, next) { + if (!groupData) { + return callback(); + } + if (Groups.isPrivilegeGroup(groupName) && parseInt(groupData.memberCount, 10) === 0) { + Groups.destroy(groupName, next); } else { if (parseInt(groupData.hidden, 10) !== 1) { - db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, callback); + db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, next); } else { - callback(); + next(); } } - }); - }); + }, + function (next) { + plugins.fireHook('action:group.leave', { + groupName: groupName, + uid: uid + }); + next(); + } + ], callback); }; - Groups.leaveAllGroups = function(uid, callback) { + Groups.leaveAllGroups = function (uid, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange('groups:createtime', 0, -1, next); }, - function(groups, next) { - async.each(groups, function(groupName, next) { + function (groups, next) { + async.each(groups, function (groupName, next) { async.parallel([ - function(next) { - Groups.isMember(uid, groupName, function(err, isMember) { + function (next) { + Groups.isMember(uid, groupName, function (err, isMember) { if (!err && isMember) { Groups.leave(groupName, uid, next); } else { @@ -240,7 +277,7 @@ module.exports = function(Groups) { } }); }, - function(next) { + function (next) { Groups.rejectMembership(groupName, uid, next); } ], next); @@ -249,13 +286,13 @@ module.exports = function(Groups) { ], callback); }; - Groups.getMembers = function(groupName, start, stop, callback) { + Groups.getMembers = function (groupName, start, stop, callback) { db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, callback); }; - Groups.getMemberUsers = function(groupNames, start, stop, callback) { - async.map(groupNames, function(groupName, next) { - Groups.getMembers(groupName, start, stop, function(err, uids) { + Groups.getMemberUsers = function (groupNames, start, stop, callback) { + async.map(groupNames, function (groupName, next) { + Groups.getMembers(groupName, start, stop, function (err, uids) { if (err) { return next(err); } @@ -265,36 +302,129 @@ module.exports = function(Groups) { }, callback); }; - Groups.getMembersOfGroups = function(groupNames, callback) { - db.getSortedSetsMembers(groupNames.map(function(name) { + Groups.getMembersOfGroups = function (groupNames, callback) { + db.getSortedSetsMembers(groupNames.map(function (name) { return 'group:' + name + ':members'; }), callback); }; - Groups.isMember = function(uid, groupName, callback) { + Groups.resetCache = function () { + pubsub.publish('group:cache:reset'); + cache.reset(); + }; + + pubsub.on('group:cache:reset', function () { + cache.reset(); + }); + + function clearCache(uid, groupName) { + pubsub.publish('group:cache:del', {uid: uid, groupName: groupName}); + cache.del(uid + ':' + groupName); + } + + pubsub.on('group:cache:del', function (data) { + cache.del(data.uid + ':' + data.groupName); + }); + + Groups.isMember = function (uid, groupName, callback) { if (!uid || parseInt(uid, 10) <= 0) { return callback(null, false); } - db.isSortedSetMember('group:' + groupName + ':members', uid, callback); + + var cacheKey = uid + ':' + groupName; + if (cache.has(cacheKey)) { + return process.nextTick(callback, null, cache.get(cacheKey)); + } + + db.isSortedSetMember('group:' + groupName + ':members', uid, function (err, isMember) { + if (err) { + return callback(err); + } + + cache.set(cacheKey, isMember); + callback(null, isMember); + }); }; - Groups.isMembers = function(uids, groupName, callback) { - db.isSortedSetMembers('group:' + groupName + ':members', uids, callback); + Groups.isMembers = function (uids, groupName, callback) { + if (!groupName || !uids.length) { + return callback(null, uids.map(function () {return false;})); + } + + var nonCachedUids = []; + uids.forEach(function (uid) { + if (!cache.has(uid + ':' + groupName)) { + nonCachedUids.push(uid); + } + }); + + if (!nonCachedUids.length) { + var result = uids.map(function (uid) { + return cache.get(uid + ':' + groupName); + }); + return process.nextTick(callback, null, result); + } + + db.isSortedSetMembers('group:' + groupName + ':members', nonCachedUids, function (err, isMembers) { + if (err) { + return callback(err); + } + + nonCachedUids.forEach(function (uid, index) { + cache.set(uid + ':' + groupName, isMembers[index]); + }); + + var result = uids.map(function (uid) { + return cache.get(uid + ':' + groupName); + }); + + callback(null, result); + }); }; - Groups.isMemberOfGroups = function(uid, groups, callback) { - if (!uid || parseInt(uid, 10) <= 0) { - return callback(null, groups.map(function() {return false;})); + Groups.isMemberOfGroups = function (uid, groups, callback) { + if (!uid || parseInt(uid, 10) <= 0 || !groups.length) { + return callback(null, groups.map(function () {return false;})); } - groups = groups.map(function(groupName) { + + var nonCachedGroups = []; + + groups.forEach(function (groupName) { + if (!cache.has(uid + ':' + groupName)) { + nonCachedGroups.push(groupName); + } + }); + + // are they all cached? + if (!nonCachedGroups.length) { + var result = groups.map(function (groupName) { + return cache.get(uid + ':' + groupName); + }); + return process.nextTick(callback, null, result); + } + + var nonCachedGroupsMemberSets = nonCachedGroups.map(function (groupName) { return 'group:' + groupName + ':members'; }); - db.isMemberOfSortedSets(groups, uid, callback); + db.isMemberOfSortedSets(nonCachedGroupsMemberSets, uid, function (err, isMembers) { + if (err) { + return callback(err); + } + + nonCachedGroups.forEach(function (groupName, index) { + cache.set(uid + ':' + groupName, isMembers[index]); + }); + + var result = groups.map(function (groupName) { + return cache.get(uid + ':' + groupName); + }); + callback(null, result); + }); }; - Groups.getMemberCount = function(groupName, callback) { - db.getObjectField('group:' + groupName, 'memberCount', function(err, count) { + Groups.getMemberCount = function (groupName, callback) { + db.getObjectField('group:' + groupName, 'memberCount', function (err, count) { if (err) { return callback(err); } @@ -302,8 +432,8 @@ module.exports = function(Groups) { }); }; - Groups.isMemberOfGroupList = function(uid, groupListKey, callback) { - db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) { + Groups.isMemberOfGroupList = function (uid, groupListKey, callback) { + db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) { if (err) { return callback(err); } @@ -312,7 +442,7 @@ module.exports = function(Groups) { return callback(null, false); } - Groups.isMemberOfGroups(uid, groupNames, function(err, isMembers) { + Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) { if (err) { return callback(err); } @@ -322,12 +452,12 @@ module.exports = function(Groups) { }); }; - Groups.isMemberOfGroupsList = function(uid, groupListKeys, callback) { - var sets = groupListKeys.map(function(groupName) { + Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) { + var sets = groupListKeys.map(function (groupName) { return 'group:' + groupName + ':members'; }); - db.getSortedSetsMembers(sets, function(err, members) { + db.getSortedSetsMembers(sets, function (err, members) { if (err) { return callback(err); } @@ -335,19 +465,19 @@ module.exports = function(Groups) { var uniqueGroups = _.unique(_.flatten(members)); uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups); - Groups.isMemberOfGroups(uid, uniqueGroups, function(err, isMembers) { + Groups.isMemberOfGroups(uid, uniqueGroups, function (err, isMembers) { if (err) { return callback(err); } var map = {}; - uniqueGroups.forEach(function(groupName, index) { + uniqueGroups.forEach(function (groupName, index) { map[groupName] = isMembers[index]; }); - var result = members.map(function(groupNames) { - for (var i=0; i<groupNames.length; ++i) { + var result = members.map(function (groupNames) { + for (var i = 0; i < groupNames.length; ++i) { if (map[groupNames[i]]) { return true; } @@ -360,14 +490,14 @@ module.exports = function(Groups) { }); }; - Groups.isMembersOfGroupList = function(uids, groupListKey, callback) { - db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) { + Groups.isMembersOfGroupList = function (uids, groupListKey, callback) { + db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) { if (err) { return callback(err); } var results = []; - uids.forEach(function() { + uids.forEach(function () { results.push(false); }); @@ -376,46 +506,46 @@ module.exports = function(Groups) { return callback(null, results); } - async.each(groupNames, function(groupName, next) { - Groups.isMembers(uids, groupName, function(err, isMembers) { + async.each(groupNames, function (groupName, next) { + Groups.isMembers(uids, groupName, function (err, isMembers) { if (err) { return next(err); } - results.forEach(function(isMember, index) { + results.forEach(function (isMember, index) { if (!isMember && isMembers[index]) { results[index] = true; } }); next(); }); - }, function(err) { + }, function (err) { callback(err, results); }); }); }; - Groups.isInvited = function(uid, groupName, callback) { + Groups.isInvited = function (uid, groupName, callback) { if (!uid) { return callback(null, false); } db.isSetMember('group:' + groupName + ':invited', uid, callback); }; - Groups.isPending = function(uid, groupName, callback) { + Groups.isPending = function (uid, groupName, callback) { if (!uid) { return callback(null, false); } db.isSetMember('group:' + groupName + ':pending', uid, callback); }; - Groups.getPending = function(groupName, callback) { + Groups.getPending = function (groupName, callback) { if (!groupName) { return callback(null, []); } db.getSetMembers('group:' + groupName + ':pending', callback); }; - - Groups.kick = function(uid, groupName, isOwner, callback) { + + Groups.kick = function (uid, groupName, isOwner, callback) { if (isOwner) { // If the owners set only contains one member, error out! async.waterfall([ diff --git a/src/groups/ownership.js b/src/groups/ownership.js index aa085f8213..f397abe5e7 100644 --- a/src/groups/ownership.js +++ b/src/groups/ownership.js @@ -4,18 +4,18 @@ var async = require('async'), db = require('../database'), plugins = require('../plugins'); -module.exports = function(Groups) { +module.exports = function (Groups) { Groups.ownership = {}; - Groups.ownership.isOwner = function(uid, groupName, callback) { + Groups.ownership.isOwner = function (uid, groupName, callback) { if (!uid) { return callback(null, false); } db.isSetMember('group:' + groupName + ':owners', uid, callback); }; - Groups.ownership.isOwners = function(uids, groupName, callback) { + Groups.ownership.isOwners = function (uids, groupName, callback) { if (!Array.isArray(uids)) { return callback(null, []); } @@ -23,20 +23,20 @@ module.exports = function(Groups) { db.isSetMembers('group:' + groupName + ':owners', uids, callback); }; - Groups.ownership.grant = function(toUid, groupName, callback) { + Groups.ownership.grant = function (toUid, groupName, callback) { // Note: No ownership checking is done here on purpose! async.waterfall([ - function(next) { + function (next) { db.setAdd('group:' + groupName + ':owners', toUid, next); }, - function(next) { + function (next) { plugins.fireHook('action:group.grantOwnership', {uid: toUid, groupName: groupName}); next(); } ], callback); }; - Groups.ownership.rescind = function(toUid, groupName, callback) { + Groups.ownership.rescind = function (toUid, groupName, callback) { // Note: No ownership checking is done here on purpose! // If the owners set only contains one member, error out! diff --git a/src/groups/search.js b/src/groups/search.js index 482f7c97ff..74b30c3188 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -6,28 +6,28 @@ var async = require('async'), db = require('./../database'), groups = module.parent.exports; -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.search = function(query, options, callback) { + Groups.search = function (query, options, callback) { if (!query) { return callback(null, []); } query = query.toLowerCase(); async.waterfall([ async.apply(db.getObjectValues, 'groupslug:groupname'), - function(groupNames, next) { + function (groupNames, next) { // Ephemeral groups and the registered-users groups are searchable groupNames = groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); - groupNames = groupNames.filter(function(name) { + groupNames = groupNames.filter(function (name) { return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators' && !Groups.isPrivilegeGroup(name); }); groupNames = groupNames.slice(0, 100); Groups.getGroupsData(groupNames, next); }, - function(groupsData, next) { + function (groupsData, next) { groupsData = groupsData.filter(Boolean); if (options.filterHidden) { - groupsData = groupsData.filter(function(group) { + groupsData = groupsData.filter(function (group) { return !group.hidden; }); } @@ -37,25 +37,25 @@ module.exports = function(Groups) { ], callback); }; - Groups.sort = function(strategy, groups, next) { + Groups.sort = function (strategy, groups, next) { switch(strategy) { case 'count': - groups = groups.sort(function(a, b) { + groups = groups.sort(function (a, b) { return a.slug > b.slug; - }).sort(function(a, b) { + }).sort(function (a, b) { return b.memberCount - a.memberCount; }); break; case 'date': - groups = groups.sort(function(a, b) { + groups = groups.sort(function (a, b) { return b.createtime - a.createtime; }); break; case 'alpha': // intentional fall-through default: - groups = groups.sort(function(a, b) { + groups = groups.sort(function (a, b) { return a.slug > b.slug ? 1 : -1; }); } @@ -63,7 +63,7 @@ module.exports = function(Groups) { next(null, groups); }; - Groups.searchMembers = function(data, callback) { + Groups.searchMembers = function (data, callback) { function findUids(query, searchBy, callback) { if (!query) { @@ -73,15 +73,15 @@ module.exports = function(Groups) { query = query.toLowerCase(); async.waterfall([ - function(next) { + function (next) { Groups.getMembers(data.groupName, 0, -1, next); }, - function(members, next) { + function (members, next) { user.getUsersFields(members, ['uid'].concat([searchBy]), next); }, - function(users, next) { + function (users, next) { var uids = []; - for(var i=0; i<users.length; ++i) { + for(var i = 0; i < users.length; ++i) { var field = users[i][searchBy]; if (field.toLowerCase().startsWith(query)) { uids.push(users[i].uid); @@ -93,7 +93,7 @@ module.exports = function(Groups) { } if (!data.query) { - Groups.getOwnersAndMembers(data.groupName, data.uid, 0, 19, function(err, users) { + Groups.getOwnersAndMembers(data.groupName, data.uid, 0, 19, function (err, users) { if (err) { return callback(err); } @@ -105,25 +105,25 @@ module.exports = function(Groups) { data.findUids = findUids; var results; async.waterfall([ - function(next) { + function (next) { user.search(data, next); }, - function(_results, next) { + function (_results, next) { results = _results; - var uids = results.users.map(function(user) { + var uids = results.users.map(function (user) { return user && user.uid; }); Groups.ownership.isOwners(uids, data.groupName, next); }, - function(isOwners, next) { + function (isOwners, next) { - results.users.forEach(function(user, index) { + results.users.forEach(function (user, index) { if (user) { user.isOwner = isOwners[index]; } }); - results.users.sort(function(a,b) { + results.users.sort(function (a,b) { if (a.isOwner && !b.isOwner) { return -1; } else if (!a.isOwner && b.isOwner) { diff --git a/src/groups/update.js b/src/groups/update.js index f48ff96d21..fa998bafe6 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -8,63 +8,72 @@ var utils = require('../../public/src/utils'); var db = require('../database'); -module.exports = function(Groups) { +module.exports = function (Groups) { - Groups.update = function(groupName, values, callback) { - callback = callback || function() {}; + Groups.update = function (groupName, values, callback) { + callback = callback || function () {}; db.exists('group:' + groupName, function (err, exists) { if (err || !exists) { return callback(err || new Error('[[error:no-group]]')); } - var payload = { - description: values.description || '', - icon: values.icon || '', - labelColor: values.labelColor || '#000000' - }; - - if (values.hasOwnProperty('userTitle')) { - payload.userTitle = values.userTitle || ''; - } - - if (values.hasOwnProperty('userTitleEnabled')) { - payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0'; - } - - if (values.hasOwnProperty('hidden')) { - payload.hidden = values.hidden ? '1' : '0'; - } - - if (values.hasOwnProperty('private')) { - payload.private = values.private ? '1' : '0'; - } - - if (values.hasOwnProperty('disableJoinRequests')) { - payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0'; - } - - async.series([ - async.apply(checkNameChange, groupName, values.name), - async.apply(updatePrivacy, groupName, values.private), - function(next) { - if (values.hasOwnProperty('hidden')) { - updateVisibility(groupName, values.hidden, next); - } else { - next(); - } - }, - async.apply(db.setObject, 'group:' + groupName, payload), - async.apply(renameGroup, groupName, values.name) - ], function(err) { + plugins.fireHook('filter:group.update', { + groupName: groupName, + values: values + }, function (err) { if (err) { return callback(err); } - plugins.fireHook('action:group.update', { - name: groupName, - values: values + var payload = { + description: values.description || '', + icon: values.icon || '', + labelColor: values.labelColor || '#000000' + }; + + if (values.hasOwnProperty('userTitle')) { + payload.userTitle = values.userTitle || ''; + } + + if (values.hasOwnProperty('userTitleEnabled')) { + payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0'; + } + + if (values.hasOwnProperty('hidden')) { + payload.hidden = values.hidden ? '1' : '0'; + } + + if (values.hasOwnProperty('private')) { + payload.private = values.private ? '1' : '0'; + } + + if (values.hasOwnProperty('disableJoinRequests')) { + payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0'; + } + + async.series([ + async.apply(checkNameChange, groupName, values.name), + async.apply(updatePrivacy, groupName, values.private), + function (next) { + if (values.hasOwnProperty('hidden')) { + updateVisibility(groupName, values.hidden, next); + } else { + next(); + } + }, + async.apply(db.setObject, 'group:' + groupName, payload), + async.apply(renameGroup, groupName, values.name) + ], function (err) { + if (err) { + return callback(err); + } + + plugins.fireHook('action:group.update', { + name: groupName, + values: values + }); + callback(); }); - callback(); }); }); }; @@ -77,7 +86,7 @@ module.exports = function(Groups) { async.apply(db.sortedSetRemove, 'groups:visible:name', groupName.toLowerCase() + ':' + groupName), ], callback); } else { - db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], function(err, groupData) { + db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], function (err, groupData) { if (err) { return callback(err); } @@ -90,21 +99,21 @@ module.exports = function(Groups) { } } - Groups.hide = function(groupName, callback) { + Groups.hide = function (groupName, callback) { showHide(groupName, 'hidden', callback); }; - Groups.show = function(groupName, callback) { + Groups.show = function (groupName, callback) { showHide(groupName, 'show', callback); }; function showHide(groupName, hidden, callback) { hidden = hidden === 'hidden'; - callback = callback || function() {}; + callback = callback || function () {}; async.parallel([ async.apply(db.setObjectField, 'group:' + groupName, 'hidden', hidden ? 1 : 0), async.apply(updateVisibility, groupName, hidden) - ], function(err) { + ], function (err) { callback(err); }); } @@ -114,7 +123,7 @@ module.exports = function(Groups) { return callback(); } - Groups.getGroupFields(groupName, ['private'], function(err, currentValue) { + Groups.getGroupFields(groupName, ['private'], function (err, currentValue) { if (err) { return callback(err); } @@ -122,12 +131,12 @@ module.exports = function(Groups) { if (currentValue !== newValue && currentValue === true) { // Group is now public, so all pending users are automatically considered members - db.getSetMembers('group:' + groupName + ':pending', function(err, uids) { + db.getSetMembers('group:' + groupName + ':pending', function (err, uids) { if (err) { return callback(err); } else if (!uids) { return callback(); } // No pending users, we're good to go var now = Date.now(), - scores = uids.map(function() { return now; }); // There's probably a better way to initialise an Array of size x with the same value... + scores = uids.map(function () { return now; }); // There's probably a better way to initialise an Array of size x with the same value... winston.verbose('[groups.update] Group is now public, automatically adding ' + uids.length + ' new members, who were pending prior.'); async.series([ @@ -150,7 +159,7 @@ module.exports = function(Groups) { if (currentSlug === newSlug) { return callback(); } - Groups.existsBySlug(newSlug, function(err, exists) { + Groups.existsBySlug(newSlug, function (err, exists) { if (err || exists) { return callback(err || new Error('[[error:group-already-exists]]')); } @@ -163,16 +172,16 @@ module.exports = function(Groups) { return callback(); } - db.getObject('group:' + oldName, function(err, group) { + db.getObject('group:' + oldName, function (err, group) { if (err || !group) { return callback(err); } - if (parseInt(group.system, 10) === 1 || parseInt(group.hidden, 10) === 1) { + if (parseInt(group.system, 10) === 1) { return callback(); } - Groups.exists(newName, function(err, exists) { + Groups.exists(newName, function (err, exists) { if (err || exists) { return callback(err || new Error('[[error:group-already-exists]]')); } @@ -182,12 +191,12 @@ module.exports = function(Groups) { async.apply(db.setObjectField, 'group:' + oldName, 'slug', utils.slugify(newName)), async.apply(db.deleteObjectField, 'groupslug:groupname', group.slug), async.apply(db.setObjectField, 'groupslug:groupname', utils.slugify(newName), newName), - function(next) { - db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) { + function (next) { + db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) { if (err) { return next(err); } - async.each(groups, function(group, next) { + async.each(groups, function (group, next) { renameGroupMember('group:' + group + ':members', oldName, newName, next); }, next); }); @@ -202,7 +211,7 @@ module.exports = function(Groups) { async.apply(renameGroupMember, 'groups:visible:createtime', oldName, newName), async.apply(renameGroupMember, 'groups:visible:memberCount', oldName, newName), async.apply(renameGroupMember, 'groups:visible:name', oldName.toLowerCase() + ':' + oldName, newName.toLowerCase() + ':' + newName), - function(next) { + function (next) { plugins.fireHook('action:group.rename', { old: oldName, new: newName @@ -216,7 +225,7 @@ module.exports = function(Groups) { } function renameGroupMember(group, oldName, newName, callback) { - db.isSortedSetMember(group, oldName, function(err, isMember) { + db.isSortedSetMember(group, oldName, function (err, isMember) { if (err || !isMember) { return callback(err); } diff --git a/src/hotswap.js b/src/hotswap.js index 608a6c27e0..d5d3749b7b 100644 --- a/src/hotswap.js +++ b/src/hotswap.js @@ -4,13 +4,13 @@ var HotSwap = {}, winston = require('winston'), stack; -HotSwap.prepare = function(app) { +HotSwap.prepare = function (app) { stack = app._router.stack; }; -HotSwap.find = function(id) { +HotSwap.find = function (id) { if (stack) { - for(var x=0,numEntries=stack.length;x<numEntries;x++) { + for(var x = 0,numEntries = stack.length;x < numEntries;x++) { if (stack[x].handle.hotswapId === id) { return x; } @@ -20,7 +20,7 @@ HotSwap.find = function(id) { } }; -HotSwap.replace = function(id, router) { +HotSwap.replace = function (id, router) { var idx = HotSwap.find(id); if (idx) { delete stack[idx].handle; // Destroy the old router diff --git a/src/image.js b/src/image.js index f4b85acd4d..1cfbdccbe3 100644 --- a/src/image.js +++ b/src/image.js @@ -1,88 +1,115 @@ 'use strict'; -var fs = require('fs'), - Jimp = require('jimp'), - async = require('async'), - plugins = require('./plugins'); +var fs = require('fs'); +var Jimp = require('jimp'); +var async = require('async'); +var plugins = require('./plugins'); -var image = {}; +var image = module.exports; -image.resizeImage = function(data, callback) { +image.resizeImage = function (data, callback) { if (plugins.hasListeners('filter:image.resize')) { plugins.fireHook('filter:image.resize', { path: data.path, + target: data.target, extension: data.extension, width: data.width, height: data.height - }, function(err, data) { + }, function (err) { callback(err); }); } else { - new Jimp(data.path, function(err, image) { + new Jimp(data.path, function (err, image) { if (err) { return callback(err); } var w = image.bitmap.width, h = image.bitmap.height, - origRatio = w/h, - desiredRatio = data.width/data.height, + origRatio = w / h, + desiredRatio = data.width && data.height ? data.width / data.height : origRatio, x = 0, y = 0, crop; - if (desiredRatio > origRatio) { - desiredRatio = 1/desiredRatio; - } - if (origRatio >= 1) { - y = 0; // height is the smaller dimension here - x = Math.floor((w/2) - (h * desiredRatio / 2)); - crop = async.apply(image.crop.bind(image), x, y, h * desiredRatio, h); + if (origRatio !== desiredRatio) { + if (desiredRatio > origRatio) { + desiredRatio = 1 / desiredRatio; + } + if (origRatio >= 1) { + y = 0; // height is the smaller dimension here + x = Math.floor((w / 2) - (h * desiredRatio / 2)); + crop = async.apply(image.crop.bind(image), x, y, h * desiredRatio, h); + } else { + x = 0; // width is the smaller dimension here + y = Math.floor(h / 2 - (w * desiredRatio / 2)); + crop = async.apply(image.crop.bind(image), x, y, w, w * desiredRatio); + } } else { - x = 0; // width is the smaller dimension here - y = Math.floor(h/2 - (w * desiredRatio / 2)); - crop = async.apply(image.crop.bind(image), x, y, w, w * desiredRatio); + // Simple resize given either width, height, or both + crop = async.apply(setImmediate); } async.waterfall([ crop, - function(image, next) { - image.resize(data.width, data.height, next); + function (_image, next) { + if (typeof _image === 'function' && !next) { + next = _image; + _image = image; + } + + if ((data.width && data.height) || (w > data.width) || (h > data.height)) { + _image.resize(data.width || Jimp.AUTO, data.height || Jimp.AUTO, next); + } else { + next(null, image); + } }, - function(image, next) { + function (image, next) { image.write(data.target || data.path, next); } - ], function(err) { + ], function (err) { callback(err); }); }); } }; -image.normalise = function(path, extension, callback) { +image.normalise = function (path, extension, callback) { if (plugins.hasListeners('filter:image.normalise')) { plugins.fireHook('filter:image.normalise', { path: path, extension: extension - }, function(err, data) { + }, function (err) { callback(err); }); } else { - new Jimp(path, function(err, image) { + new Jimp(path, function (err, image) { if (err) { return callback(err); } - image.write(path + '.png', function(err) { + image.write(path + '.png', function (err) { callback(err); }); }); } }; -image.convertImageToBase64 = function(path, callback) { - fs.readFile(path, function(err, data) { +image.size = function (path, callback) { + if (plugins.hasListeners('filter:image.size')) { + plugins.fireHook('filter:image.size', { + path: path, + }, function (err, image) { + callback(err, image); + }); + } else { + new Jimp(path, function (err, data) { + callback(err, data ? data.bitmap : null); + }); + } +}; + +image.convertImageToBase64 = function (path, callback) { + fs.readFile(path, function (err, data) { callback(err, data ? data.toString('base64') : null); }); }; - -module.exports = image; diff --git a/src/install.js b/src/install.js index aae92e5c23..711932bb00 100644 --- a/src/install.js +++ b/src/install.js @@ -43,8 +43,8 @@ questions.optional = [ ]; function checkSetupFlag(next) { - var envSetupKeys = ['database'], - setupVal; + var setupVal; + try { if (nconf.get('setup')) { setupVal = JSON.parse(nconf.get('setup')); @@ -74,14 +74,10 @@ function checkSetupFlag(next) { process.exit(); } - } else if (envSetupKeys.every(function(key) { - return nconf.stores.env.store.hasOwnProperty(key); - })) { - install.values = envSetupKeys.reduce(function(config, key) { - config[key] = nconf.stores.env.store[key]; - return config; - }, {}); - + } else if (nconf.get('database')) { + install.values = { + database: nconf.get('database') + }; next(); } else { next(); @@ -129,14 +125,14 @@ function setupConfig(next) { prompt.colors = false; if (!install.values) { - prompt.get(questions.main, function(err, config) { + prompt.get(questions.main, function (err, config) { if (err) { process.stdout.write('\n\n'); winston.warn('NodeBB setup ' + err.message); process.exit(); } - configureDatabases(config, function(err, config) { + configureDatabases(config, function (err, config) { completeConfigSetup(err, config, next); }); }); @@ -151,7 +147,7 @@ function setupConfig(next) { config[question.name] = install.values[question.name] || question['default'] || undefined; }); - configureDatabases(config, function(err, config) { + configureDatabases(config, function (err, config) { completeConfigSetup(err, config, next); }); } @@ -172,7 +168,7 @@ function completeConfigSetup(err, config, next) { } } - install.save(config, function(err) { + install.save(config, function (err) { if (err) { return next(err); } @@ -183,12 +179,10 @@ function completeConfigSetup(err, config, next) { function setupDefaultConfigs(next) { process.stdout.write('Populating database with default configs, if not already set...\n'); - var meta = require('./meta'), - defaults = require(path.join(__dirname, '../', 'install/data/defaults.json')); + var meta = require('./meta'); + var defaults = require(path.join(__dirname, '../', 'install/data/defaults.json')); - async.each(Object.keys(defaults), function (key, next) { - meta.configs.setOnEmpty(key, defaults[key], next); - }, function (err) { + meta.configs.setOnEmpty(defaults, function (err) { if (err) { return next(err); } @@ -200,7 +194,7 @@ function setupDefaultConfigs(next) { function enableDefaultTheme(next) { var meta = require('./meta'); - meta.configs.get('theme:id', function(err, id) { + meta.configs.get('theme:id', function (err, id) { if (err || id) { process.stdout.write('Previous theme detected, skipping enabling default theme\n'); return next(err); @@ -260,7 +254,10 @@ function createAdmin(callback) { hidden: true, type: 'string' }], - success = function(err, results) { + success = function (err, results) { + if (err) { + return callback(err); + } if (!results) { return callback(new Error('aborted')); } @@ -271,20 +268,20 @@ function createAdmin(callback) { } var adminUid; async.waterfall([ - function(next) { + function (next) { User.create({username: results.username, password: results.password, email: results.email}, next); }, - function(uid, next) { + function (uid, next) { adminUid = uid; Groups.join('administrators', uid, next); }, - function(next) { + function (next) { Groups.show('administrators', next); }, - function(next) { + function (next) { Groups.ownership.grant(adminUid, 'administrators', next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -385,7 +382,7 @@ function createCategories(next) { function createMenuItems(next) { var db = require('./database'); - db.exists('navigation:enabled', function(err, exists) { + db.exists('navigation:enabled', function (err, exists) { if (err || exists) { return next(err); } @@ -401,13 +398,13 @@ function createWelcomePost(next) { Topics = require('./topics'); async.parallel([ - function(next) { + function (next) { fs.readFile(path.join(__dirname, '../', 'install/data/welcome.md'), next); }, - function(next) { + function (next) { db.getObjectField('global', 'topicCount', next); } - ], function(err, results) { + ], function (err, results) { if (err) { return next(err); } @@ -441,7 +438,7 @@ function enableDefaultPlugins(next) { 'nodebb-rewards-essentials', 'nodebb-plugin-soundpack-default', 'nodebb-plugin-emoji-extended', - 'nodebb-plugin-emoji-apple' + 'nodebb-plugin-emoji-one' ], customDefaults = nconf.get('defaultPlugins'); @@ -457,14 +454,14 @@ function enableDefaultPlugins(next) { } } - defaultEnabled = defaultEnabled.filter(function(plugin, index, array) { + defaultEnabled = defaultEnabled.filter(function (plugin, index, array) { return array.indexOf(plugin) === index; }); winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled); var db = require('./database'); - var order = defaultEnabled.map(function(plugin, index) { + var order = defaultEnabled.map(function (plugin, index) { return index; }); db.sortedSetAdd('plugins:active', order, defaultEnabled, next); @@ -473,13 +470,13 @@ function enableDefaultPlugins(next) { function setCopyrightWidget(next) { var db = require('./database'); async.parallel({ - footerJSON: function(next) { + footerJSON: function (next) { fs.readFile(path.join(__dirname, '../', 'install/data/footer.json'), next); }, - footer: function(next) { + footer: function (next) { db.getObjectField('widgets:global', 'footer', next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } @@ -510,7 +507,7 @@ install.setup = function (callback) { setCopyrightWidget, function (next) { var upgrade = require('./upgrade'); - upgrade.check(function(err, uptodate) { + upgrade.check(function (err, uptodate) { if (err) { return next(err); } diff --git a/src/languages.js b/src/languages.js index de1d82bcff..51def2f922 100644 --- a/src/languages.js +++ b/src/languages.js @@ -3,20 +3,67 @@ var fs = require('fs'), path = require('path'), async = require('async'), + LRU = require('lru-cache'), + _ = require('underscore'); - Languages = {}; +var plugins = require('./plugins'); -Languages.list = function(callback) { +var Languages = {}; + +Languages.init = function (next) { + if (Languages.hasOwnProperty('_cache')) { + Languages._cache.reset(); + } else { + Languages._cache = LRU(100); + } + + next(); +}; + +Languages.get = function (code, key, callback) { + var combined = [code, key].join('/'); + + if (Languages._cache && Languages._cache.has(combined)) { + return callback(null, Languages._cache.get(combined)); + } + + var languageData; + + fs.readFile(path.join(__dirname, '../public/language/', code, key), { encoding: 'utf-8' }, function (err, data) { + if (err && err.code !== 'ENOENT') { + return callback(err); + } + + // If language file in core cannot be read, then no language file present + try { + languageData = JSON.parse(data) || {}; + } catch (e) { + languageData = {}; + } + + if (plugins.customLanguages.hasOwnProperty(combined)) { + _.extendOwn(languageData, plugins.customLanguages[combined]); + } + + if (Languages._cache) { + Languages._cache.set(combined, languageData); + } + + callback(null, languageData); + }); +}; + +Languages.list = function (callback) { var languagesPath = path.join(__dirname, '../public/language'), languages = []; - fs.readdir(languagesPath, function(err, files) { + fs.readdir(languagesPath, function (err, files) { if (err) { return callback(err); } - async.each(files, function(folder, next) { - fs.stat(path.join(languagesPath, folder), function(err, stat) { + async.each(files, function (folder, next) { + fs.stat(path.join(languagesPath, folder), function (err, stat) { if (err) { return next(err); } @@ -27,7 +74,7 @@ Languages.list = function(callback) { var configPath = path.join(languagesPath, folder, 'language.json'); - fs.readFile(configPath, function(err, stream) { + fs.readFile(configPath, function (err, stream) { if (err) { next(); } @@ -35,12 +82,12 @@ Languages.list = function(callback) { next(); }); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } // Sort alphabetically - languages = languages.sort(function(a, b) { + languages = languages.sort(function (a, b) { return a.code > b.code ? 1 : -1; }); diff --git a/src/logger.js b/src/logger.js index e646046bd9..4d6b8f717c 100644 --- a/src/logger.js +++ b/src/logger.js @@ -30,20 +30,20 @@ var opts = { /* -- Logger -- */ -(function(Logger) { +(function (Logger) { - Logger.init = function(app) { + Logger.init = function (app) { opts.express.app = app; /* Open log file stream & initialize express logging if meta.config.logger* variables are set */ Logger.setup(); }; - Logger.setup = function() { + Logger.setup = function () { Logger.setup_one('loggerPath', meta.config.loggerPath); }; - Logger.setup_one = function(key, value) { + Logger.setup_one = function (key, value) { /* * 1. Open the logger stream: stdout or file * 2. Re-initialize the express logger hijack @@ -54,7 +54,7 @@ var opts = { } }; - Logger.setup_one_log = function(value) { + Logger.setup_one_log = function (value) { /* * If logging is currently enabled, create a stream. * Otherwise, close the current stream @@ -72,7 +72,7 @@ var opts = { } }; - Logger.open = function(value) { + Logger.open = function (value) { /* Open the streams to log to: either a path or stdout */ var stream; if(value) { @@ -91,7 +91,7 @@ var opts = { } if(stream) { - stream.on('error', function(err) { + stream.on('error', function (err) { winston.error(err.message); }); } @@ -101,14 +101,14 @@ var opts = { return stream; }; - Logger.close = function(stream) { + Logger.close = function (stream) { if(stream.f !== process.stdout && stream.f) { stream.end(); } stream.f = null; }; - Logger.monitorConfig = function(socket, data) { + Logger.monitorConfig = function (socket, data) { /* * This monitor's when a user clicks "save" in the Logger section of the admin panel */ @@ -117,7 +117,7 @@ var opts = { Logger.io(socket); }; - Logger.express_open = function() { + Logger.express_open = function () { if(opts.express.set !== 1) { opts.express.set = 1; opts.express.app.use(Logger.expressLogger); @@ -128,7 +128,7 @@ var opts = { opts.express.ofn = morgan('combined', {stream : opts.streams.log.f}); }; - Logger.expressLogger = function(req,res,next) { + Logger.expressLogger = function (req,res,next) { /* * The new express.logger * @@ -141,21 +141,21 @@ var opts = { } }; - Logger.prepare_io_string = function(_type, _uid, _args) { + Logger.prepare_io_string = function (_type, _uid, _args) { /* * This prepares the output string for intercepted socket.io events * * The format is: io: <uid> <event> <args> */ try { - return 'io: '+_uid+' '+_type+' '+util.inspect(Array.prototype.slice.call(_args))+'\n'; + return 'io: ' + _uid + ' ' + _type + ' ' + util.inspect(Array.prototype.slice.call(_args)) + '\n'; } catch(err) { winston.info("Logger.prepare_io_string: Failed", err); return "error"; } }; - Logger.io_close = function(socket) { + Logger.io_close = function (socket) { /* * Restore all hijacked sockets to their original emit/on functions */ @@ -177,7 +177,7 @@ var opts = { } }; - Logger.io = function(socket) { + Logger.io = function (socket) { /* * Go through all of the currently established sockets & hook their .emit/.on */ @@ -194,12 +194,12 @@ var opts = { } }; - Logger.io_one = function(socket, uid) { + Logger.io_one = function (socket, uid) { /* * This function replaces a socket's .emit/.on functions in order to intercept events */ function override(method, name, errorMsg) { - return function() { + return function () { if(opts.streams.log.f) { opts.streams.log.f.write(Logger.prepare_io_string(name, uid, arguments)); } diff --git a/src/messaging.js b/src/messaging.js index 1974313c81..a88ceabbbe 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -1,20 +1,19 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - S = require('string'), +var async = require('async'); +var winston = require('winston'); +var S = require('string'); +var db = require('./database'); +var user = require('./user'); +var plugins = require('./plugins'); +var meta = require('./meta'); +var utils = require('../public/src/utils'); +var notifications = require('./notifications'); +var userNotifications = require('./user/notifications'); - db = require('./database'), - user = require('./user'), - plugins = require('./plugins'), - meta = require('./meta'), - utils = require('../public/src/utils'), - notifications = require('./notifications'), - userNotifications = require('./user/notifications'); - -(function(Messaging) { +(function (Messaging) { require('./messaging/create')(Messaging); require('./messaging/delete')(Messaging); @@ -23,62 +22,66 @@ var async = require('async'), require('./messaging/unread')(Messaging); require('./messaging/notifications')(Messaging); - var terms = { - day: 86400000, - week: 604800000, - month: 2592000000, - threemonths: 7776000000 - }; - - Messaging.getMessageField = function(mid, field, callback) { - Messaging.getMessageFields(mid, [field], function(err, fields) { + Messaging.getMessageField = function (mid, field, callback) { + Messaging.getMessageFields(mid, [field], function (err, fields) { callback(err, fields ? fields[field] : null); }); }; - Messaging.getMessageFields = function(mid, fields, callback) { + Messaging.getMessageFields = function (mid, fields, callback) { db.getObjectFields('message:' + mid, fields, callback); }; - Messaging.setMessageField = function(mid, field, content, callback) { + Messaging.setMessageField = function (mid, field, content, callback) { db.setObjectField('message:' + mid, field, content, callback); }; - Messaging.setMessageFields = function(mid, data, callback) { + Messaging.setMessageFields = function (mid, data, callback) { db.setObject('message:' + mid, data, callback); }; - Messaging.getMessages = function(params, callback) { - var uid = params.uid, - roomId = params.roomId, - since = params.since, - isNew = params.isNew, - count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250, - markRead = params.markRead || true; + Messaging.getMessages = function (params, callback) { + var uid = params.uid; + var roomId = params.roomId; + var isNew = params.isNew || false; + var start = params.hasOwnProperty('start') ? params.start : 0; + var stop = parseInt(start, 10) + ((params.count || 50) - 1); + var markRead = params.markRead || true; - var min = params.count ? 0 : Date.now() - (terms[since] || terms.day); + var indices = {}; + async.waterfall([ + function (next) { + canGetMessages(params.callerUid, params.uid, next); + }, + function (canGet, next) { + if (!canGet) { + return callback(null, null); + } + db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', start, stop, next); + }, + function (mids, next) { + if (!Array.isArray(mids) || !mids.length) { + return callback(null, []); + } - if (since === 'recent') { - count = 49; - min = 0; - } + mids.forEach(function (mid, index) { + indices[mid] = start + index; + }); - db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) { - if (err) { - return callback(err); - } + mids.reverse(); - if (!Array.isArray(mids) || !mids.length) { - return callback(null, []); + Messaging.getMessagesData(mids, uid, roomId, isNew, next); + }, + function (messageData, next) { + messageData.forEach(function (messageData) { + messageData.index = indices[messageData.messageId.toString()]; + }); + next(null, messageData); } - - mids.reverse(); - - Messaging.getMessagesData(mids, uid, roomId, isNew, callback); - }); + ], callback); if (markRead) { - notifications.markRead('chat_' + roomId + '_' + uid, uid, function(err) { + notifications.markRead('chat_' + roomId + '_' + uid, uid, function (err) { if (err) { winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message); } @@ -88,9 +91,19 @@ var async = require('async'), } }; - Messaging.getMessagesData = function(mids, uid, roomId, isNew, callback) { + function canGetMessages(callerUid, uid, callback) { + plugins.fireHook('filter:messaging.canGetMessages', { + callerUid: callerUid, + uid: uid, + canGet: parseInt(callerUid, 10) === parseInt(uid, 10) + }, function (err, data) { + callback(err, data ? data.canGet : false); + }); + } + + Messaging.getMessagesData = function (mids, uid, roomId, isNew, callback) { - var keys = mids.map(function(mid) { + var keys = mids.map(function (mid) { return 'message:' + mid; }); @@ -101,48 +114,52 @@ var async = require('async'), db.getObjects(keys, next); }, function (_messages, next) { - messages = _messages.map(function(msg, idx) { + messages = _messages.map(function (msg, idx) { if (msg) { msg.messageId = parseInt(mids[idx], 10); } return msg; }).filter(Boolean); - var uids = messages.map(function(msg) { + var uids = messages.map(function (msg) { return msg && msg.fromuid; }); user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next); }, function (users, next) { - messages.forEach(function(message, index) { + messages.forEach(function (message, index) { message.fromUser = users[index]; var self = parseInt(message.fromuid, 10) === parseInt(uid, 10); message.self = self ? 1 : 0; message.timestampISO = utils.toISOString(message.timestamp); message.newSet = false; + message.roomId = String(message.roomId || roomId); if (message.hasOwnProperty('edited')) { message.editedISO = new Date(parseInt(message.edited, 10)).toISOString(); } }); - async.map(messages, function(message, next) { - Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function(result) { + async.map(messages, function (message, next) { + Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function (err, result) { + if (err) { + return next(err); + } message.content = result; message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s; next(null, message); }); }, next); }, - function(messages, next) { + function (messages, next) { if (messages.length > 1) { // Add a spacer in between messages with time gaps between them - messages = messages.map(function(message, index) { + messages = messages.map(function (message, index) { // Compare timestamps with the previous message, and check if a spacer needs to be added - if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index-1].timestamp, 10) + (1000*60*5)) { + if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + (1000 * 60 * 5)) { // If it's been 5 minutes, this is a new set of messages message.newSet = true; - } else if (index > 0 && message.fromuid !== messages[index-1].fromuid) { + } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) { // If the previous message was from the other person, this is also a new set message.newSet = true; } @@ -156,25 +173,25 @@ var async = require('async'), var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; async.waterfall([ async.apply(db.sortedSetRank, key, messages[0].messageId), - function(index, next) { + function (index, next) { // Continue only if this isn't the first message in sorted set if (index > 0) { - db.getSortedSetRange(key, index-1, index-1, next); + db.getSortedSetRange(key, index - 1, index - 1, next); } else { messages[0].newSet = true; return next(undefined, messages); } }, - function(mid, next) { + function (mid, next) { Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next); } - ], function(err, fields) { + ], function (err, fields) { if (err) { return next(err); } if ( - (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) || + (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000 * 60 * 5)) || (parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10)) ) { // If it's been 5 minutes, this is a new set of messages @@ -192,9 +209,9 @@ var async = require('async'), }; Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { - plugins.fireHook('filter:parse.raw', message, function(err, parsed) { + plugins.fireHook('filter:parse.raw', message, function (err, parsed) { if (err) { - return callback(message); + return callback(err); } var messageData = { @@ -207,20 +224,20 @@ var async = require('async'), parsedMessage: parsed }; - plugins.fireHook('filter:messaging.parse', messageData, function(err, messageData) { - callback(messageData.parsedMessage); + plugins.fireHook('filter:messaging.parse', messageData, function (err, messageData) { + callback(err, messageData ? messageData.parsedMessage : ''); }); }); }; - Messaging.isNewSet = function(uid, roomId, timestamp, callback) { + Messaging.isNewSet = function (uid, roomId, timestamp, callback) { var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRangeWithScores(setKey, 0, 0, next); }, - function(messages, next) { + function (messages, next) { if (messages && messages.length) { next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5)); } else { @@ -231,66 +248,91 @@ var async = require('async'), }; - Messaging.getRecentChats = function(uid, start, stop, callback) { - db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, function(err, roomIds) { - if (err) { - return callback(err); - } - - async.parallel({ - unread: function(next) { - db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next); - }, - users: function(next) { - async.map(roomIds, function(roomId, next) { - db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 3, function(err, uids) { - if (err) { - return next(err); - } - uids = uids.filter(function(value) { - return value && parseInt(value, 10) !== parseInt(uid, 10); - }); - user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , next); - }); - }, next); - }, - teasers: function(next) { - async.map(roomIds, function(roomId, next) { - Messaging.getTeaser(uid, roomId, next); - }, next); - } - }, function(err, results) { - if (err) { - return callback(err); + Messaging.getRecentChats = function (callerUid, uid, start, stop, callback) { + async.waterfall([ + function (next) { + canGetRecentChats(callerUid, uid, next); + }, + function (canGet, next) { + if (!canGet) { + return callback(null, null); } - var rooms = results.users.map(function(users, index) { - var data = { - users: users, - unread: results.unread[index], - roomId: roomIds[index], - teaser: results.teasers[index] - }; - data.users.forEach(function(userData) { + db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, next); + }, + function (roomIds, next) { + async.parallel({ + roomData: function (next) { + Messaging.getRoomsData(roomIds, next); + }, + unread: function (next) { + db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next); + }, + users: function (next) { + async.map(roomIds, function (roomId, next) { + db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 9, function (err, uids) { + if (err) { + return next(err); + } + uids = uids.filter(function (value) { + return value && parseInt(value, 10) !== parseInt(uid, 10); + }); + user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'] , next); + }); + }, next); + }, + teasers: function (next) { + async.map(roomIds, function (roomId, next) { + Messaging.getTeaser(uid, roomId, next); + }, next); + } + }, next); + }, + function (results, next) { + results.roomData.forEach(function (room, index) { + room.users = results.users[index]; + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.unread = results.unread[index]; + room.teaser = results.teasers[index]; + + room.users.forEach(function (userData) { if (userData && parseInt(userData.uid, 10)) { userData.status = user.getStatus(userData); } }); - data.users = data.users.filter(function(user) { + room.users = room.users.filter(function (user) { return user && parseInt(user.uid, 10); }); - data.lastUser = data.users[0]; - data.usernames = data.users.map(function(user) { - return user.username; - }).join(', '); - return data; + room.lastUser = room.users[0]; + + room.usernames = Messaging.generateUsernames(room.users, uid); }); - callback(null, {rooms: rooms, nextStart: stop + 1}); - }); + next(null, {rooms: results.roomData, nextStart: stop + 1}); + } + ], callback); + }; + + Messaging.generateUsernames = function (users, excludeUid) { + users = users.filter(function (user) { + return user && parseInt(user.uid, 10) !== excludeUid; }); + return users.map(function (user) { + return user.username; + }).join(', '); }; + function canGetRecentChats(callerUid, uid, callback) { + plugins.fireHook('filter:messaging.canGetRecentChats', { + callerUid: callerUid, + uid: uid, + canGet: parseInt(callerUid, 10) === parseInt(uid, 10) + }, function (err, data) { + callback(err, data ? data.canGet : false); + }); + } + Messaging.getTeaser = function (uid, roomId, callback) { + var teaser; async.waterfall([ function (next) { db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next); @@ -299,20 +341,28 @@ var async = require('async'), if (!mids || !mids.length) { return next(null, null); } - Messaging.getMessageFields(mids[0], ['content', 'timestamp'], next); + Messaging.getMessageFields(mids[0], ['fromuid', 'content', 'timestamp'], next); }, - function (teaser, next) { - if (teaser && teaser.content) { + function (_teaser, next) { + teaser = _teaser; + if (!teaser) { + return callback(); + } + if (teaser.content) { teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s; - teaser.timestampISO = utils.toISOString(teaser.timestamp); - } + } + teaser.timestampISO = utils.toISOString(teaser.timestamp); + user.getUserFields(teaser.fromuid, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'] , next); + }, + function (user, next) { + teaser.user = user; next(null, teaser); } ], callback); }; - Messaging.canMessageUser = function(uid, toUid, callback) { + Messaging.canMessageUser = function (uid, toUid, callback) { if (parseInt(meta.config.disableChat) === 1 || !uid || uid === toUid) { return callback(new Error('[[error:chat-disabled]]')); } @@ -342,7 +392,7 @@ var async = require('async'), isFollowing: async.apply(user.isFollowing, toUid, uid) }, next); }, - function(results, next) { + function (results, next) { if (!results.settings.restrictChat || results.isAdmin || results.isFollowing) { return next(); } @@ -352,7 +402,7 @@ var async = require('async'), ], callback); }; - Messaging.canMessageRoom = function(uid, roomId, callback) { + Messaging.canMessageRoom = function (uid, roomId, callback) { if (parseInt(meta.config.disableChat) === 1 || !uid) { return callback(new Error('[[error:chat-disabled]]')); } @@ -368,7 +418,7 @@ var async = require('async'), Messaging.getUserCountInRoom(roomId, next); }, - function(count, next) { + function (count, next) { if (count < 2) { return next(new Error('[[error:no-users-in-room]]')); } @@ -389,7 +439,7 @@ var async = require('async'), ], callback); }; - Messaging.hasPrivateChat = function(uid, withUid, callback) { + Messaging.hasPrivateChat = function (uid, withUid, callback) { async.waterfall([ function (next) { async.parallel({ @@ -398,7 +448,7 @@ var async = require('async'), }, next); }, function (results, next) { - var roomIds = results.myRooms.filter(function(roomId) { + var roomIds = results.myRooms.filter(function (roomId) { return roomId && results.theirRooms.indexOf(roomId) !== -1; }); @@ -408,10 +458,10 @@ var async = require('async'), var index = 0; var roomId = 0; - async.whilst(function() { + async.whilst(function () { return index < roomIds.length && !roomId; - }, function(next) { - Messaging.getUserCountInRoom(roomIds[index], function(err, count) { + }, function (next) { + Messaging.getUserCountInRoom(roomIds[index], function (err, count) { if (err) { return next(err); } @@ -423,7 +473,7 @@ var async = require('async'), next(); } }); - }, function(err) { + }, function (err) { next(err, roomId); }); } diff --git a/src/messaging/create.js b/src/messaging/create.js index 3b2b5dcf26..face15f589 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -7,9 +7,9 @@ var plugins = require('../plugins'); var db = require('../database'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { - Messaging.sendMessage = function(uid, roomId, content, timestamp, callback) { + Messaging.sendMessage = function (uid, roomId, content, timestamp, callback) { async.waterfall([ function (next) { Messaging.checkContent(content, next); @@ -27,7 +27,7 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.checkContent = function(content, callback) { + Messaging.checkContent = function (content, callback) { if (!content) { return callback(new Error('[[error:invalid-chat-message]]')); } @@ -38,7 +38,7 @@ module.exports = function(Messaging) { callback(); }; - Messaging.addMessage = function(fromuid, roomId, content, timestamp, callback) { + Messaging.addMessage = function (fromuid, roomId, content, timestamp, callback) { var mid; var message; var isNewSet; @@ -55,7 +55,8 @@ module.exports = function(Messaging) { message = { content: content, timestamp: timestamp, - fromuid: fromuid + fromuid: fromuid, + roomId: roomId }; plugins.fireHook('filter:messaging.save', message, next); @@ -97,21 +98,21 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.addRoomToUsers = function(roomId, uids, timestamp, callback) { + Messaging.addRoomToUsers = function (roomId, uids, timestamp, callback) { if (!uids.length) { return callback(); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':chat:rooms'; }); db.sortedSetsAdd(keys, timestamp, roomId, callback); }; - Messaging.addMessageToUsers = function(roomId, uids, mid, timestamp, callback) { + Messaging.addMessageToUsers = function (roomId, uids, mid, timestamp, callback) { if (!uids.length) { return callback(); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':chat:room:' + roomId + ':mids'; }); db.sortedSetsAdd(keys, timestamp, mid, callback); diff --git a/src/messaging/delete.js b/src/messaging/delete.js index 518ab9fafb..e9f48232d1 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -3,9 +3,9 @@ var async = require('async'); var db = require('../database'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { - Messaging.deleteMessage = function(mid, roomId, callback) { + Messaging.deleteMessage = function (mid, roomId, callback) { async.waterfall([ function (next) { Messaging.getUidsInRoom(roomId, 0, -1, next); @@ -14,12 +14,12 @@ module.exports = function(Messaging) { if (!uids.length) { return next(); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':chat:room:' + roomId + 'mids'; }); db.sortedSetsRemove(keys, roomId, next); }, - function(next) { + function (next) { db.delete('message:' + mid, next); } ], callback); diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 5b2472c4e4..c60e264cbf 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -8,12 +8,12 @@ var user = require('../user'); var sockets = require('../socket.io'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { - Messaging.editMessage = function(uid, mid, roomId, content, callback) { + Messaging.editMessage = function (uid, mid, roomId, content, callback) { var uids; async.waterfall([ - function(next) { + function (next) { Messaging.getMessageField(mid, 'content', next); }, function (raw, next) { @@ -34,7 +34,7 @@ module.exports = function(Messaging) { Messaging.getMessagesData([mid], uid, roomId, true, next); }, function (messages, next) { - uids.forEach(function(uid) { + uids.forEach(function (uid) { sockets.in('uid_' + uid).emit('event:chats.edit', { messages: messages }); @@ -44,7 +44,7 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.canEdit = function(messageId, uid, callback) { + Messaging.canEdit = function (messageId, uid, callback) { if (parseInt(meta.config.disableChat) === 1) { return callback(null, false); } @@ -64,14 +64,14 @@ module.exports = function(Messaging) { Messaging.getMessageField(messageId, 'fromuid', next); }, - function(fromUid, next) { + function (fromUid, next) { if (parseInt(fromUid, 10) === parseInt(uid, 10)) { return callback(null, true); } user.isAdministrator(uid, next); }, - function(isAdmin, next) { + function (isAdmin, next) { next(null, isAdmin); } ], callback); diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index c93cb16590..6d9500c4ba 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -2,20 +2,20 @@ var async = require('async'); var nconf = require('nconf'); +var winston = require('winston'); var user = require('../user'); var emailer = require('../emailer'); var notifications = require('../notifications'); var meta = require('../meta'); -var utils = require('../../public/src/utils'); var sockets = require('../socket.io'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser - Messaging.notifyUsersInRoom = function(fromUid, roomId, messageObj) { - Messaging.getUidsInRoom(roomId, 0, -1, function(err, uids) { + Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) { + Messaging.getUidsInRoom(roomId, 0, -1, function (err, uids) { if (err) { return; } @@ -25,7 +25,7 @@ module.exports = function(Messaging) { fromUid: fromUid, message: messageObj }; - uids.forEach(function(uid) { + uids.forEach(function (uid) { data.self = parseInt(uid, 10) === parseInt(fromUid) ? 1 : 0; Messaging.pushUnreadCount(uid); sockets.in('uid_' + uid).emit('event:chats.receive', data); @@ -42,8 +42,8 @@ module.exports = function(Messaging) { }; } - queueObj.timeout = setTimeout(function() { - sendNotifications(fromUid, uids, roomId, queueObj.message, function(err) { + queueObj.timeout = setTimeout(function () { + sendNotifications(fromUid, uids, roomId, queueObj.message, function (err) { if (!err) { delete Messaging.notifyQueue[fromUid + ':' + roomId]; } @@ -53,12 +53,12 @@ module.exports = function(Messaging) { }; function sendNotifications(fromuid, uids, roomId, messageObj, callback) { - user.isOnline(uids, function(err, isOnline) { + user.isOnline(uids, function (err, isOnline) { if (err) { return callback(err); } - uids = uids.filter(function(uid, index) { + uids = uids.filter(function (uid, index) { return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10); }); @@ -72,33 +72,52 @@ module.exports = function(Messaging) { nid: 'chat_' + fromuid + '_' + roomId, from: fromuid, path: '/chats/' + messageObj.roomId - }, function(err, notification) { + }, function (err, notification) { if (!err && notification) { notifications.push(notification, uids, callback); } }); - if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) { - return callback(); + sendNotificationEmails(uids, messageObj); + }); + } + + function sendNotificationEmails(uids, messageObj) { + if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) { + return; + } + + async.parallel({ + userData: function (next) { + user.getUsersFields(uids, ['uid', 'username', 'userslug'], next); + }, + userSettings: function (next) { + user.getMultipleUserSettings(uids, next); + } + }, function (err, results) { + if (err) { + return winston.error(err); } - user.getMultipleUserSettings(uids, function(err, userSettings) { + results.userData = results.userData.filter(function (userData, index) { + return userData && results.userSettings[index] && results.userSettings[index].sendChatNotifications; + }); + + async.each(results.userData, function (userData, next) { + emailer.send('notif_chat', userData.uid, { + subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', + summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', + message: messageObj, + site_title: meta.config.title || 'NodeBB', + url: nconf.get('url'), + roomId: messageObj.roomId, + username: userData.username, + userslug: userData.userslug + }, next); + }, function (err) { if (err) { - return callback(err); + winston.error(err); } - userSettings = userSettings.filter(function(settings) { - return settings && settings.sendChatNotifications; - }); - async.each(userSettings, function(settings, next) { - emailer.send('notif_chat', settings.uid, { - subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.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) - }, next); - }, callback); }); }); } diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 0c9c5bb01f..f16843662f 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -5,23 +5,46 @@ var validator = require('validator'); var db = require('../database'); var user = require('../user'); +var plugins = require('../plugins'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { - Messaging.getRoomData = function(roomId, callback) { - db.getObject('chat:room:' + roomId, function(err, data) { + Messaging.getRoomData = function (roomId, callback) { + db.getObject('chat:room:' + roomId, function (err, data) { if (err || !data) { return callback(err || new Error('[[error:no-chat-room]]')); } - data.roomName = data.roomName || '[[modules:chat.roomname, ' + roomId + ']]'; - if (data.roomName) { - data.roomName = validator.escape(data.roomName); - } + modifyRoomData([data]); callback(null, data); }); }; - Messaging.newRoom = function(uid, toUids, callback) { + Messaging.getRoomsData = function (roomIds, callback) { + var keys = roomIds.map(function (roomId) { + return 'chat:room:' + roomId; + }); + db.getObjects(keys, function (err, roomData) { + if (err) { + return callback(err); + } + modifyRoomData(roomData); + callback(null, roomData); + }); + }; + + function modifyRoomData(rooms) { + rooms.forEach(function (data) { + if (data) { + data.roomName = data.roomName || ''; + data.roomName = validator.escape(String(data.roomName)); + if (data.hasOwnProperty('groupChat')) { + data.groupChat = parseInt(data.groupChat, 10) === 1; + } + } + }); + } + + Messaging.newRoom = function (uid, toUids, callback) { var roomId; var now = Date.now(); async.waterfall([ @@ -51,20 +74,30 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.isUserInRoom = function(uid, roomId, callback) { - db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, callback); + Messaging.isUserInRoom = function (uid, roomId, callback) { + async.waterfall([ + function (next) { + db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, next); + }, + function (inRoom, next) { + plugins.fireHook('filter:messaging.isUserInRoom', {uid: uid, roomId: roomId, inRoom: inRoom}, next); + }, + function (data, next) { + next(null, data.inRoom); + } + ], callback); }; - Messaging.roomExists = function(roomId, callback) { + Messaging.roomExists = function (roomId, callback) { db.exists('chat:room:' + roomId + ':uids', callback); }; - Messaging.getUserCountInRoom = function(roomId, callback) { + Messaging.getUserCountInRoom = function (roomId, callback) { db.sortedSetCard('chat:room:' + roomId + ':uids', callback); }; - Messaging.isRoomOwner = function(uid, roomId, callback) { - db.getObjectField('chat:room:' + roomId, 'owner', function(err, owner) { + Messaging.isRoomOwner = function (uid, roomId, callback) { + db.getObjectField('chat:room:' + roomId, 'owner', function (err, owner) { if (err) { return callback(err); } @@ -73,7 +106,7 @@ module.exports = function(Messaging) { }); }; - Messaging.addUsersToRoom = function(uid, uids, roomId, callback) { + Messaging.addUsersToRoom = function (uid, uids, roomId, callback) { async.waterfall([ function (next) { Messaging.isUserInRoom(uid, roomId, next); @@ -83,15 +116,27 @@ module.exports = function(Messaging) { return next(new Error('[[error:cant-add-users-to-chat-room]]')); } var now = Date.now(); - var timestamps = uids.map(function() { + var timestamps = uids.map(function () { return now; }); db.sortedSetAdd('chat:room:' + roomId + ':uids', timestamps, uids, next); + }, + function (next) { + async.parallel({ + userCount: async.apply(db.sortedSetCard, 'chat:room:' + roomId + ':uids'), + roomData: async.apply(db.getObject, 'chat:room:' + roomId) + }, next); + }, + function (results, next) { + if (!results.roomData.hasOwnProperty('groupChat') && results.userCount > 2) { + return db.setObjectField('chat:room:' + roomId, 'groupChat', 1, next); + } + next(); } ], callback); }; - Messaging.removeUsersFromRoom = function(uid, uids, roomId, callback) { + Messaging.removeUsersFromRoom = function (uid, uids, roomId, callback) { async.waterfall([ function (next) { async.parallel({ @@ -111,16 +156,16 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.leaveRoom = function(uids, roomId, callback) { + Messaging.leaveRoom = function (uids, roomId, callback) { async.waterfall([ function (next) { db.sortedSetRemove('chat:room:' + roomId + ':uids', uids, next); }, function (next) { - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':chat:rooms'; }); - keys.concat(uids.map(function(uid) { + keys.concat(uids.map(function (uid) { return 'uid:' + uid + ':chat:rooms:unread'; })); db.sortedSetsRemove(keys, roomId, next); @@ -128,22 +173,22 @@ module.exports = function(Messaging) { ], callback); }; - Messaging.getUidsInRoom = function(roomId, start, stop, callback) { + Messaging.getUidsInRoom = function (roomId, start, stop, callback) { db.getSortedSetRevRange('chat:room:' + roomId + ':uids', start, stop, callback); }; - Messaging.getUsersInRoom = function(roomId, start, stop, callback) { + Messaging.getUsersInRoom = function (roomId, start, stop, callback) { async.waterfall([ function (next) { Messaging.getUidsInRoom(roomId, start, stop, next); }, function (uids, next) { - user.getUsersFields(uids, ['username', 'uid', 'picture', 'status'], next); + user.getUsersFields(uids, ['uid', 'username', 'picture', 'status'], next); } ], callback); }; - Messaging.renameRoom = function(uid, roomId, newName, callback) { + Messaging.renameRoom = function (uid, roomId, newName, callback) { if (!newName) { return callback(new Error('[[error:invalid-name]]')); } diff --git a/src/messaging/unread.js b/src/messaging/unread.js index 0562551540..91c9a364ac 100644 --- a/src/messaging/unread.js +++ b/src/messaging/unread.js @@ -5,20 +5,20 @@ var async = require('async'); var db = require('../database'); var sockets = require('../socket.io'); -module.exports = function(Messaging) { +module.exports = function (Messaging) { - Messaging.getUnreadCount = function(uid, callback) { + Messaging.getUnreadCount = function (uid, callback) { if (!parseInt(uid, 10)) { return callback(null, 0); } db.sortedSetCard('uid:' + uid + ':chat:rooms:unread', callback); }; - Messaging.pushUnreadCount = function(uid) { + Messaging.pushUnreadCount = function (uid) { if (!parseInt(uid, 10)) { return callback(null, 0); } - Messaging.getUnreadCount(uid, function(err, unreadCount) { + Messaging.getUnreadCount(uid, function (err, unreadCount) { if (err) { return; } @@ -26,15 +26,15 @@ module.exports = function(Messaging) { }); }; - Messaging.markRead = function(uid, roomId, callback) { + Messaging.markRead = function (uid, roomId, callback) { db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomId, callback); }; - Messaging.markAllRead = function(uid, callback) { + Messaging.markAllRead = function (uid, callback) { db.delete('uid:' + uid + ':chat:rooms:unread', callback); }; - Messaging.markUnread = function(uids, roomId, callback) { + Messaging.markUnread = function (uids, roomId, callback) { async.waterfall([ function (next) { Messaging.roomExists(roomId, next); @@ -43,7 +43,7 @@ module.exports = function(Messaging) { if (!exists) { return next(new Error('[[error:chat-room-does-not-exist]]')); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'uid:' + uid + ':chat:rooms:unread'; }); diff --git a/src/meta.js b/src/meta.js index ffc85c98bf..c732de15f4 100644 --- a/src/meta.js +++ b/src/meta.js @@ -1,17 +1,12 @@ "use strict"; -var async = require('async'), - winston = require('winston'), - templates = require('templates.js'), - os = require('os'), - nconf = require('nconf'), +var async = require('async'); +var winston = require('winston'); +var os = require('os'); +var nconf = require('nconf'); - user = require('./user'), - groups = require('./groups'), - emitter = require('./emitter'), - pubsub = require('./pubsub'), - auth = require('./routes/authentication'), - utils = require('../public/src/utils'); +var pubsub = require('./pubsub'); +var utils = require('../public/src/utils'); (function (Meta) { Meta.reloadRequired = false; @@ -23,72 +18,40 @@ var async = require('async'), require('./meta/sounds')(Meta); require('./meta/settings')(Meta); require('./meta/logs')(Meta); + require('./meta/errors')(Meta); require('./meta/tags')(Meta); require('./meta/dependencies')(Meta); Meta.templates = require('./meta/templates'); Meta.blacklist = require('./meta/blacklist'); /* Assorted */ - Meta.userOrGroupExists = function(slug, callback) { + Meta.userOrGroupExists = function (slug, callback) { + var user = require('./user'); + var groups = require('./groups'); slug = utils.slugify(slug); async.parallel([ async.apply(user.existsBySlug, slug), async.apply(groups.existsBySlug, slug) - ], function(err, results) { - callback(err, results ? results.some(function(result) { return result; }) : false); + ], function (err, results) { + callback(err, results ? results.some(function (result) { return result; }) : false); }); }; - Meta.reload = function(callback) { - pubsub.publish('meta:reload', {hostname: os.hostname()}); - reload(callback); + /** + * Reload deprecated as of v1.1.2+, remove in v2.x + */ + Meta.reload = function (callback) { + restart(); + callback(); }; - pubsub.on('meta:reload', function(data) { - if (data.hostname !== os.hostname()) { - reload(); - } - }); - - function reload(callback) { - callback = callback || function() {}; - - var plugins = require('./plugins'); - async.series([ - function (next) { - plugins.fireHook('static:app.reload', {}, next); - }, - async.apply(plugins.clearRequireCache), - async.apply(plugins.reload), - async.apply(plugins.reloadRoutes), - async.apply(Meta.css.minify), - async.apply(Meta.js.minify, 'nodebb.min.js'), - async.apply(Meta.js.minify, 'acp.min.js'), - async.apply(Meta.sounds.init), - async.apply(Meta.templates.compile), - async.apply(auth.reloadRoutes), - function(next) { - Meta.config['cache-buster'] = utils.generateUUID(); - templates.flush(); - next(); - } - ], function(err) { - if (!err) { - emitter.emit('nodebb:ready'); - } - Meta.reloadRequired = false; - - callback(err); - }); - } - - Meta.restart = function() { + Meta.restart = function () { pubsub.publish('meta:restart', {hostname: os.hostname()}); restart(); }; if (nconf.get('isPrimary') === 'true') { - pubsub.on('meta:restart', function(data) { + pubsub.on('meta:restart', function (data) { if (data.hostname !== os.hostname()) { restart(); } diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 47381e487b..679dc3cec9 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -9,11 +9,11 @@ var Blacklist = { _rules: [] }; -Blacklist.load = function(callback) { +Blacklist.load = function (callback) { async.waterfall([ async.apply(db.get, 'ip-blacklist-rules'), async.apply(Blacklist.validate) - ], function(err, rules) { + ], function (err, rules) { if (err) { return callback(err); } @@ -33,8 +33,8 @@ Blacklist.load = function(callback) { }); }; -Blacklist.save = function(rules, callback) { - db.set('ip-blacklist-rules', rules, function(err) { +Blacklist.save = function (rules, callback) { + db.set('ip-blacklist-rules', rules, function (err) { if (err) { return callback(err); } @@ -42,15 +42,15 @@ Blacklist.save = function(rules, callback) { }); }; -Blacklist.get = function(callback) { +Blacklist.get = function (callback) { db.get('ip-blacklist-rules', callback); }; -Blacklist.test = function(clientIp, callback) { +Blacklist.test = function (clientIp, callback) { if ( Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list && Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list - && !Blacklist._rules.cidr.some(function(subnet) { + && !Blacklist._rules.cidr.some(function (subnet) { return ip.cidrSubnet(subnet).contains(clientIp); }) // not in a blacklisted cidr range ) { @@ -71,7 +71,7 @@ Blacklist.test = function(clientIp, callback) { } }; -Blacklist.validate = function(rules, callback) { +Blacklist.validate = function (rules, callback) { rules = (rules || '').split('\n'); var ipv4 = []; var ipv6 = []; @@ -84,13 +84,13 @@ Blacklist.validate = function(rules, callback) { // Filter out blank lines and lines starting with the hash character (comments) // Also trim inputs and remove inline comments - rules = rules.map(function(rule) { + rules = rules.map(function (rule) { rule = rule.replace(inlineCommentMatch, '').trim(); return rule.length && !rule.startsWith('#') ? rule : null; }).filter(Boolean); // Filter out invalid rules - rules = rules.filter(function(rule) { + rules = rules.filter(function (rule) { if (whitelist.indexOf(rule) !== -1) { invalid.push(rule); return false; diff --git a/src/meta/configs.js b/src/meta/configs.js index 3ff42f66ae..0a8435e531 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -1,13 +1,14 @@ 'use strict'; -var winston = require('winston'), - db = require('../database'), - pubsub = require('../pubsub'), - nconf = require('nconf'), - utils = require('../../public/src/utils'); +var winston = require('winston'); +var nconf = require('nconf'); -module.exports = function(Meta) { +var db = require('../database'); +var pubsub = require('../pubsub'); +var utils = require('../../public/src/utils'); + +module.exports = function (Meta) { Meta.config = {}; Meta.configs = {}; @@ -46,12 +47,12 @@ module.exports = function(Meta) { }; Meta.configs.set = function (field, value, callback) { - callback = callback || function() {}; + callback = callback || function () {}; if (!field) { return callback(new Error('invalid config field')); } - db.setObjectField('config', field, value, function(err) { + db.setObjectField('config', field, value, function (err) { if (err) { return callback(err); } @@ -63,12 +64,12 @@ module.exports = function(Meta) { }); }; - Meta.configs.setMultiple = function(data, callback) { - processConfig(data, function(err) { + Meta.configs.setMultiple = function (data, callback) { + processConfig(data, function (err) { if (err) { return callback(err); } - db.setObject('config', data, function(err) { + db.setObject('config', data, function (err) { if (err) { return callback(err); } @@ -91,7 +92,7 @@ module.exports = function(Meta) { var less = require('less'); less.render(data.customCSS, { compress: true - }, function(err, lessObject) { + }, function (err, lessObject) { if (err) { winston.error('[less] Could not convert custom LESS to CSS! Please check your syntax.'); return callback(null, ''); @@ -117,14 +118,20 @@ module.exports = function(Meta) { } }); - Meta.configs.setOnEmpty = function (field, value, callback) { - Meta.configs.get(field, function (err, curValue) { + Meta.configs.setOnEmpty = function (values, callback) { + db.getObject('config', function (err, data) { if (err) { return callback(err); } - - if (!curValue) { - Meta.configs.set(field, value, callback); + data = data || {}; + var empty = {}; + Object.keys(values).forEach(function (key) { + if (!data.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); + if (Object.keys(empty).length) { + db.setObject('config', empty, callback); } else { callback(); } @@ -135,4 +142,4 @@ module.exports = function(Meta) { db.deleteObjectField('config', field); }; -}; \ No newline at end of file +}; diff --git a/src/meta/css.js b/src/meta/css.js index 12c5d49c20..ce31ea1f13 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -1,36 +1,35 @@ 'use strict'; -var winston = require('winston'), - nconf = require('nconf'), - fs = require('fs'), - path = require('path'), - less = require('less'), - crypto = require('crypto'), - async = require('async'), - autoprefixer = require('autoprefixer'), - postcss = require('postcss'), - - plugins = require('../plugins'), - emitter = require('../emitter'), - db = require('../database'), - file = require('../file'), - utils = require('../../public/src/utils'); - -module.exports = function(Meta) { +var winston = require('winston'); +var nconf = require('nconf'); +var fs = require('fs'); +var path = require('path'); +var less = require('less'); +var async = require('async'); +var autoprefixer = require('autoprefixer'); +var postcss = require('postcss'); + +var plugins = require('../plugins'); +var emitter = require('../emitter'); +var db = require('../database'); +var file = require('../file'); +var utils = require('../../public/src/utils'); + +module.exports = function (Meta) { Meta.css = {}; Meta.css.cache = undefined; Meta.css.acpCache = undefined; - Meta.css.minify = function(callback) { - callback = callback || function() {}; + Meta.css.minify = function (callback) { + callback = callback || function () {}; if (nconf.get('isPrimary') !== 'true') { winston.verbose('[meta/css] Cluster worker ' + process.pid + ' skipping LESS/CSS compilation'); return callback(); } winston.verbose('[meta/css] Minifying LESS/CSS'); - db.getObjectFields('config', ['theme:type', 'theme:id'], function(err, themeData) { + db.getObjectFields('config', ['theme:type', 'theme:id'], function (err, themeData) { if (err) { return callback(err); } @@ -40,34 +39,35 @@ module.exports = function(Meta) { paths = [ baseThemePath, path.join(__dirname, '../../node_modules'), - path.join(__dirname, '../../public/vendor/fontawesome/less'), - path.join(__dirname, '../../public/vendor/bootstrap/less') + path.join(__dirname, '../../public/vendor/fontawesome/less') ], - source = '@import "font-awesome";', - acpSource = '@import "font-awesome";'; + source = '@import "font-awesome";'; plugins.lessFiles = filterMissingFiles(plugins.lessFiles); plugins.cssFiles = filterMissingFiles(plugins.cssFiles); async.waterfall([ - function(next) { + function (next) { getStyleSource(plugins.lessFiles, '\n@import ".', '.less', next); }, - function(src, next) { + function (src, next) { source += src; getStyleSource(plugins.cssFiles, '\n@import (inline) ".', '.css', next); }, - function(src, next) { + function (src, next) { source += src; next(); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } - source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";'; + var acpSource = source; + + source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";'; + source += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";'; source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";'; source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; @@ -76,14 +76,15 @@ module.exports = function(Meta) { source = '@import "./theme";\n' + source; acpSource += '\n@import "..' + path.sep + 'public/less/admin/admin";\n'; - acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";'; - acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";'; + acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; + acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; var fromFile = nconf.get('from-file') || ''; async.series([ - function(next) { + function (next) { if (fromFile.match('clientLess')) { winston.info('[minifier] Compiling front-end LESS files skipped'); return Meta.css.getFromFile(path.join(__dirname, '../../public/stylesheet.css'), 'cache', next); @@ -91,7 +92,7 @@ module.exports = function(Meta) { minify(source, paths, 'cache', next); }, - function(next) { + function (next) { if (fromFile.match('acpLess')) { winston.info('[minifier] Compiling ACP LESS files skipped'); return Meta.css.getFromFile(path.join(__dirname, '../../public/admin.css'), 'acpCache', next); @@ -99,7 +100,7 @@ module.exports = function(Meta) { minify(acpSource, paths, 'acpCache', next); } - ], function(err, minified) { + ], function (err, minified) { if (err) { return callback(err); } @@ -125,7 +126,7 @@ module.exports = function(Meta) { var pluginDirectories = [], source = ''; - files.forEach(function(styleFile) { + files.forEach(function (styleFile) { if (styleFile.endsWith(extension)) { source += prefix + path.sep + styleFile + '";'; } else { @@ -133,27 +134,27 @@ module.exports = function(Meta) { } }); - async.each(pluginDirectories, function(directory, next) { - utils.walk(directory, function(err, styleFiles) { + async.each(pluginDirectories, function (directory, next) { + utils.walk(directory, function (err, styleFiles) { if (err) { return next(err); } - styleFiles.forEach(function(styleFile) { + styleFiles.forEach(function (styleFile) { source += prefix + path.sep + styleFile + '";'; }); next(); }); - }, function(err) { + }, function (err) { callback(err, source); }); } - Meta.css.commitToFile = function(filename, callback) { + Meta.css.commitToFile = function (filename, callback) { var file = (filename === 'acpCache' ? 'admin' : 'stylesheet') + '.css'; - fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function(err) { + fs.writeFile(path.join(__dirname, '../../public/' + file), Meta.css[filename], function (err) { if (!err) { winston.verbose('[meta/css] ' + file + ' committed to disk.'); } else { @@ -165,10 +166,14 @@ module.exports = function(Meta) { }); }; - Meta.css.getFromFile = function(filePath, filename, callback) { + Meta.css.getFromFile = function (filePath, filename, callback) { winston.verbose('[meta/css] Reading stylesheet ' + filePath.split('/').pop() + ' from file'); - fs.readFile(filePath, function(err, file) { + fs.readFile(filePath, function (err, file) { + if (err) { + return callback(err); + } + Meta.css[filename] = file; callback(); }); @@ -178,7 +183,7 @@ module.exports = function(Meta) { less.render(source, { paths: paths, compress: true - }, function(err, lessOutput) { + }, function (err, lessOutput) { if (err) { winston.error('[meta/css] Could not minify LESS/CSS: ' + err.message); if (typeof callback === 'function') { @@ -195,8 +200,8 @@ module.exports = function(Meta) { Meta.css[destination] = result.css; // Save the compiled CSS in public/ so things like nginx can serve it - if (nconf.get('isPrimary') === 'true') { - return Meta.css.commitToFile(destination, function() { + if (nconf.get('isPrimary') === 'true' && (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false)) { + return Meta.css.commitToFile(destination, function () { if (typeof callback === 'function') { callback(null, result.css); } @@ -212,7 +217,7 @@ module.exports = function(Meta) { } function filterMissingFiles(files) { - return files.filter(function(filePath) { + return files.filter(function (filePath) { var exists = file.existsSync(path.join(__dirname, '../../node_modules', filePath)); if (!exists) { winston.warn('[meta/css] File not found! ' + filePath); diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index 5482ad3e7f..f115ff6bfc 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -1,24 +1,27 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - async = require('async'), - semver = require('semver'), - winston = require('winston'), +var path = require('path'); +var fs = require('fs'); +var async = require('async'); +var semver = require('semver'); +var winston = require('winston'); - pkg = require('../../package.json'); +var pkg = require('../../package.json'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.dependencies = {}; - Meta.dependencies.check = function(callback) { + Meta.dependencies.check = function (callback) { var modules = Object.keys(pkg.dependencies); + var depsOutdated = false; + var depsMissing = false; + winston.verbose('Checking dependencies for outdated modules'); - async.every(modules, function(module, next) { + async.every(modules, function (module, next) { fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), { encoding: 'utf-8' - }, function(err, pkgData) { + }, function (err, pkgData) { // If a bundled plugin/theme is not present, skip the dep check (#3384) if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) { winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.'); @@ -33,15 +36,23 @@ module.exports = function(Meta) { next(true); } else { process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n'); - next(false); + depsOutdated = true; + next(true); } } catch(e) { - winston.error('[meta/dependencies] Could not read: ' + module); - process.exit(); + process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n'); + depsMissing = true; + next(true); } }); - }, function(ok) { - callback(!ok && global.env !== 'development' ? new Error('dependencies-out-of-date') : null); + }, function (ok) { + if (depsMissing) { + callback(new Error('dependencies-missing')); + } else if (depsOutdated) { + callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null); + } else { + callback(null); + } }); }; }; diff --git a/src/meta/errors.js b/src/meta/errors.js new file mode 100644 index 0000000000..58e381e270 --- /dev/null +++ b/src/meta/errors.js @@ -0,0 +1,37 @@ +'use strict'; + +var validator = require('validator'); + +var db = require('../database'); +var analytics = require('../analytics'); + +module.exports = function (Meta) { + + Meta.errors = {}; + + Meta.errors.log404 = function (route, callback) { + callback = callback || function () {}; + route = route.replace(/\/$/, ''); // remove trailing slashes + analytics.increment('errors:404'); + db.sortedSetIncrBy('errors:404', 1, route, callback); + }; + + Meta.errors.get = function (escape, callback) { + db.getSortedSetRevRangeWithScores('errors:404', 0, -1, function (err, data) { + if (err) { + return callback(err); + } + + data = data.map(function (nfObject) { + nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value; + return nfObject; + }); + + callback(null, data); + }); + }; + + Meta.errors.clear = function (callback) { + db.delete('errors:404', callback); + }; +}; diff --git a/src/meta/js.js b/src/meta/js.js index 740b5e08ee..cfb588125d 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -1,47 +1,47 @@ 'use strict'; -var winston = require('winston'), - fork = require('child_process').fork, - path = require('path'), - async = require('async'), - nconf = require('nconf'), - fs = require('fs'), - file = require('../file'), - plugins = require('../plugins'), - emitter = require('../emitter'), - utils = require('../../public/src/utils'); - -module.exports = function(Meta) { +var winston = require('winston'); +var fork = require('child_process').fork; +var path = require('path'); +var async = require('async'); +var nconf = require('nconf'); +var fs = require('fs'); +var file = require('../file'); +var plugins = require('../plugins'); +var emitter = require('../emitter'); +var utils = require('../../public/src/utils'); + +module.exports = function (Meta) { Meta.js = { target: {}, scripts: { base: [ - 'public/vendor/jquery/js/jquery.js', + './node_modules/jquery/dist/jquery.js', './node_modules/socket.io-client/socket.io.js', 'public/vendor/jquery/timeago/jquery.timeago.js', 'public/vendor/jquery/js/jquery.form.min.js', 'public/vendor/visibility/visibility.min.js', - 'public/vendor/bootstrap/js/bootstrap.min.js', + 'public/vendor/bootstrap/js/bootstrap.js', 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', 'public/vendor/jquery/textcomplete/jquery.textcomplete.js', 'public/vendor/requirejs/require.js', + 'public/src/require-config.js', 'public/vendor/bootbox/bootbox.min.js', 'public/vendor/tinycon/tinycon.js', 'public/vendor/xregexp/xregexp.js', 'public/vendor/xregexp/unicode/unicode-base.js', - 'public/vendor/buzz/buzz.min.js', - 'public/vendor/mousetrap/mousetrap.js', - 'public/vendor/autosize.js', './node_modules/templates.js/lib/templates.js', 'public/src/utils.js', 'public/src/sockets.js', 'public/src/app.js', 'public/src/ajaxify.js', 'public/src/overrides.js', - 'public/src/variables.js', - 'public/src/widgets.js' + 'public/src/widgets.js', + "./node_modules/promise-polyfill/promise.js" ], + + // files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load rjs: [ 'public/src/client/footer.js', 'public/src/client/chats.js', @@ -61,7 +61,6 @@ module.exports = function(Meta) { 'public/src/client/category.js', 'public/src/client/categoryTools.js', - 'public/src/modules/csrf.js', 'public/src/modules/translator.js', 'public/src/modules/notifications.js', 'public/src/modules/chat.js', @@ -76,11 +75,53 @@ module.exports = function(Meta) { 'public/src/modules/helpers.js', 'public/src/modules/sounds.js', 'public/src/modules/string.js' - ] + ], + + // modules listed below are routed through express (/src/modules) so they can be defined anonymously + modules: { + "Chart.js": './node_modules/chart.js/dist/Chart.min.js', + "mousetrap.js": './node_modules/mousetrap/mousetrap.min.js', + "jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js', + "buzz.js": 'public/vendor/buzz/buzz.js' + } } }; - Meta.js.minify = function(target, callback) { + Meta.js.bridgeModules = function (app, callback) { + // Add routes for AMD-type modules to serve those files + var numBridged = 0, + addRoute = function (relPath) { + var relativePath = nconf.get('relative_path'); + + app.get(relativePath + '/src/modules/' + relPath, function (req, res) { + return res.sendFile(path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]), { + maxAge: app.enabled('cache') ? 5184000000 : 0 + }); + }); + }; + + async.series([ + function (next) { + for(var relPath in Meta.js.scripts.modules) { + if (Meta.js.scripts.modules.hasOwnProperty(relPath)) { + addRoute(relPath); + ++numBridged; + } + } + + next(); + } + ], function (err) { + if (err) { + winston.error('[meta/js] Encountered error while bridging modules:' + err.message); + } + + winston.verbose('[meta/js] ' + numBridged + ' of ' + Object.keys(Meta.js.scripts.modules).length + ' modules bridged'); + callback(err); + }); + }; + + Meta.js.minify = function (target, callback) { if (nconf.get('isPrimary') !== 'true') { if (typeof callback === 'function') { callback(); @@ -96,7 +137,7 @@ module.exports = function(Meta) { Meta.js.target[target] = {}; - Meta.js.prepare(target, function() { + Meta.js.prepare(target, function () { minifier.send({ action: 'js', minify: global.env !== 'development', @@ -104,7 +145,7 @@ module.exports = function(Meta) { }); }); - minifier.on('message', function(message) { + minifier.on('message', function (message) { switch(message.type) { case 'end': Meta.js.target[target].cache = message.minified; @@ -119,11 +160,18 @@ module.exports = function(Meta) { }); } - Meta.js.commitToFile(target, function() { + if (nconf.get('local-assets') === undefined || nconf.get('local-assets') !== false) { + return Meta.js.commitToFile(target, function () { + if (typeof callback === 'function') { + callback(); + } + }); + } else { + emitter.emit('meta:js.compiled'); if (typeof callback === 'function') { - callback(); + return callback(); } - }); + } break; case 'error': @@ -140,12 +188,12 @@ module.exports = function(Meta) { }); }; - Meta.js.prepare = function(target, callback) { + Meta.js.prepare = function (target, callback) { var pluginsScripts = []; var pluginDirectories = []; - pluginsScripts = plugins[target === 'nodebb.min.js' ? 'clientScripts' : 'acpScripts'].filter(function(path) { + pluginsScripts = plugins[target === 'nodebb.min.js' ? 'clientScripts' : 'acpScripts'].filter(function (path) { if (path.endsWith('.js')) { return true; } @@ -154,12 +202,12 @@ module.exports = function(Meta) { return false; }); - async.each(pluginDirectories, function(directory, next) { - utils.walk(directory, function(err, scripts) { + async.each(pluginDirectories, function (directory, next) { + utils.walk(directory, function (err, scripts) { pluginsScripts = pluginsScripts.concat(scripts); next(err); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } @@ -172,7 +220,7 @@ module.exports = function(Meta) { Meta.js.target[target].scripts = Meta.js.target[target].scripts.concat(Meta.js.scripts.rjs); } - Meta.js.target[target].scripts = Meta.js.target[target].scripts.map(function(script) { + Meta.js.target[target].scripts = Meta.js.target[target].scripts.map(function (script) { return path.relative(basePath, script).replace(/\\/g, '/'); }); @@ -180,13 +228,13 @@ module.exports = function(Meta) { }); }; - Meta.js.killMinifier = function() { + Meta.js.killMinifier = function () { if (Meta.js.minifierProc) { Meta.js.minifierProc.kill('SIGTERM'); } }; - Meta.js.commitToFile = function(target, callback) { + Meta.js.commitToFile = function (target, callback) { fs.writeFile(path.join(__dirname, '../../public/' + target), Meta.js.target[target].cache, function (err) { if (err) { winston.error('[meta/js] ' + err.message); @@ -198,12 +246,12 @@ module.exports = function(Meta) { }); }; - Meta.js.getFromFile = function(target, callback) { + Meta.js.getFromFile = function (target, callback) { var scriptPath = path.join(__dirname, '../../public/' + target), mapPath = path.join(__dirname, '../../public/' + target + '.map'), paths = [scriptPath]; - file.exists(scriptPath, function(exists) { + file.exists(scriptPath, function (exists) { if (!exists) { winston.warn('[meta/js] ' + target + ' not found on disk, re-minifying'); Meta.js.minify(target, callback); @@ -214,12 +262,16 @@ module.exports = function(Meta) { return callback(); } - file.exists(mapPath, function(exists) { + file.exists(mapPath, function (exists) { if (exists) { paths.push(mapPath); } - async.map(paths, fs.readFile, function(err, files) { + async.map(paths, fs.readFile, function (err, files) { + if (err) { + return callback(err); + } + Meta.js.target[target] = { cache: files[0], map: files[1] || '' diff --git a/src/meta/logs.js b/src/meta/logs.js index 0b38b61a34..b335ff281e 100644 --- a/src/meta/logs.js +++ b/src/meta/logs.js @@ -1,19 +1,19 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - winston = require('winston'); +var path = require('path'); +var fs = require('fs'); +var winston = require('winston'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.logs = { path: path.join('logs', path.sep, 'output.log') }; - Meta.logs.get = function(callback) { + Meta.logs.get = function (callback) { fs.readFile(this.path, { encoding: 'utf-8' - }, function(err, logs) { + }, function (err, logs) { if (err) { winston.error('[meta/logs] Could not retrieve logs: ' + err.message); } @@ -22,7 +22,7 @@ module.exports = function(Meta) { }); }; - Meta.logs.clear = function(callback) { + Meta.logs.clear = function (callback) { fs.truncate(this.path, 0, callback); }; }; \ No newline at end of file diff --git a/src/meta/settings.js b/src/meta/settings.js index ed6702a943..299286abdd 100644 --- a/src/meta/settings.js +++ b/src/meta/settings.js @@ -1,25 +1,25 @@ 'use strict'; -var db = require('../database'), - plugins = require('../plugins'); +var db = require('../database'); +var plugins = require('../plugins'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.settings = {}; - Meta.settings.get = function(hash, callback) { - db.getObject('settings:' + hash, function(err, settings) { + Meta.settings.get = function (hash, callback) { + db.getObject('settings:' + hash, function (err, settings) { callback(err, settings || {}); }); }; - Meta.settings.getOne = function(hash, field, callback) { + Meta.settings.getOne = function (hash, field, callback) { db.getObjectField('settings:' + hash, field, callback); }; - Meta.settings.set = function(hash, values, callback) { + Meta.settings.set = function (hash, values, callback) { var key = 'settings:' + hash; - db.setObject(key, values, function(err) { + db.setObject(key, values, function (err) { if (err) { return callback(err); } @@ -34,18 +34,25 @@ module.exports = function(Meta) { }); }; - Meta.settings.setOne = function(hash, field, value, callback) { + Meta.settings.setOne = function (hash, field, value, callback) { db.setObjectField('settings:' + hash, field, value, callback); }; - Meta.settings.setOnEmpty = function (hash, field, value, callback) { - Meta.settings.getOne(hash, field, function (err, curValue) { + Meta.settings.setOnEmpty = function (hash, values, callback) { + db.getObject('settings:' + hash, function (err, settings) { if (err) { return callback(err); } + settings = settings || {}; + var empty = {}; + Object.keys(values).forEach(function (key) { + if (!settings.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); - if (!curValue) { - Meta.settings.setOne(hash, field, value, callback); + if (Object.keys(empty).length) { + db.setObject('settings:' + hash, empty, callback); } else { callback(); } diff --git a/src/meta/sounds.js b/src/meta/sounds.js index 4b85f681cb..6068f16f5f 100644 --- a/src/meta/sounds.js +++ b/src/meta/sounds.js @@ -1,21 +1,21 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - winston = require('winston'), - rimraf = require('rimraf'), - mkdirp = require('mkdirp'), - async = require('async'), +var path = require('path'); +var fs = require('fs'); +var nconf = require('nconf'); +var winston = require('winston'); +var rimraf = require('rimraf'); +var mkdirp = require('mkdirp'); +var async = require('async'); - plugins = require('../plugins'), - db = require('../database'); +var plugins = require('../plugins'); +var db = require('../database'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.sounds = {}; - Meta.sounds.init = function(callback) { + Meta.sounds.init = function (callback) { if (nconf.get('isPrimary') === 'true') { setupSounds(callback); } else { @@ -25,21 +25,21 @@ module.exports = function(Meta) { } }; - Meta.sounds.getFiles = function(callback) { + Meta.sounds.getFiles = function (callback) { async.waterfall([ - function(next) { + function (next) { fs.readdir(path.join(__dirname, '../../public/sounds'), next); }, - function(sounds, next) { - fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function(err, uploaded) { + function (sounds, next) { + fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), function (err, uploaded) { next(err, sounds.concat(uploaded)); }); } - ], function(err, files) { + ], function (err, files) { var localList = {}; // Filter out hidden files - files = files.filter(function(filename) { + files = files.filter(function (filename) { return !filename.startsWith('.'); }); @@ -50,7 +50,7 @@ module.exports = function(Meta) { } // Return proper paths - files.forEach(function(filename) { + files.forEach(function (filename) { localList[filename] = nconf.get('relative_path') + '/sounds/' + filename; }); @@ -58,20 +58,32 @@ module.exports = function(Meta) { }); }; - Meta.sounds.getMapping = function(callback) { - db.getObject('settings:sounds', function(err, sounds) { - if (err || !sounds) { - // Send default sounds - var defaults = { - 'notification': 'notification.mp3', - 'chat-incoming': 'waterdrop-high.mp3', - 'chat-outgoing': undefined - }; - - return callback(null, defaults); + Meta.sounds.getMapping = function (uid, callback) { + var user = require('../user'); + async.parallel({ + defaultMapping: function (next) { + db.getObject('settings:sounds', next); + }, + userSettings: function (next) { + user.getSettings(uid, next); + } + }, function (err, results) { + if (err) { + return callback(err); } + var userSettings = results.userSettings; + var defaultMapping = results.defaultMapping || {}; + var soundMapping = {}; + soundMapping.notification = (userSettings.notificationSound || userSettings.notificationSound === '') ? + userSettings.notificationSound : defaultMapping.notification || ''; + + soundMapping['chat-incoming'] = (userSettings.incomingChatSound || userSettings.incomingChatSound === '') ? + userSettings.incomingChatSound : defaultMapping['chat-incoming'] || ''; + + soundMapping['chat-outgoing'] = (userSettings.outgoingChatSound || userSettings.outgoingChatSound === '') ? + userSettings.outgoingChatSound : defaultMapping['chat-outgoing'] || ''; - callback(null, sounds); + callback(null, soundMapping); }); }; @@ -79,50 +91,67 @@ module.exports = function(Meta) { var soundsPath = path.join(__dirname, '../../public/sounds'); async.waterfall([ - function(next) { + function (next) { fs.readdir(path.join(__dirname, '../../public/uploads/sounds'), next); }, - function(uploaded, next) { - uploaded = uploaded.map(function(filename) { + function (uploaded, next) { + uploaded = uploaded.filter(function (filename) { + return !filename.startsWith('.'); + }).map(function (filename) { return path.join(__dirname, '../../public/uploads/sounds', filename); }); - plugins.fireHook('filter:sounds.get', uploaded, function(err, filePaths) { + plugins.fireHook('filter:sounds.get', uploaded, function (err, filePaths) { if (err) { winston.error('Could not initialise sound files:' + err.message); return; } + if (nconf.get('local-assets') === false) { + // Don't regenerate the public/sounds/ directory. Instead, create a mapping for the router to use + Meta.sounds._filePathHash = filePaths.reduce(function (hash, filePath) { + hash[path.basename(filePath)] = filePath; + return hash; + }, {}); + + winston.verbose('[sounds] Sounds OK'); + if (typeof next === 'function') { + return next(); + } else { + return; + } + } + // Clear the sounds directory async.series([ - function(next) { + function (next) { rimraf(soundsPath, next); }, - function(next) { + function (next) { mkdirp(soundsPath, next); } - ], function(err) { + ], function (err) { if (err) { winston.error('Could not initialise sound files:' + err.message); return; } // Link paths - async.each(filePaths, function(filePath, next) { + async.each(filePaths, function (filePath, next) { if (process.platform === 'win32') { fs.link(filePath, path.join(soundsPath, path.basename(filePath)), next); } else { fs.symlink(filePath, path.join(soundsPath, path.basename(filePath)), 'file', next); } - }, function(err) { + }, function (err) { if (!err) { winston.verbose('[sounds] Sounds OK'); } else { winston.error('[sounds] Could not initialise sounds: ' + err.message); } - if (typeof callback === 'function') { - callback(); + if (typeof next === 'function') { + next(); } }); }); diff --git a/src/meta/tags.js b/src/meta/tags.js index 3747db61da..1d64a7f93d 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -1,20 +1,20 @@ 'use strict'; -var nconf = require('nconf'), - validator = require('validator'), - async = require('async'), - winston = require('winston'), - plugins = require('../plugins'); +var nconf = require('nconf'); +var validator = require('validator'); +var async = require('async'); +var winston = require('winston'); +var plugins = require('../plugins'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.tags = {}; - Meta.tags.parse = function(meta, link, callback) { + Meta.tags.parse = function (meta, link, callback) { async.parallel({ - tags: function(next) { + tags: function (next) { var defaultTags = [{ name: 'viewport', - content: 'width=device-width, initial-scale=1.0, user-scalable=no' + content: 'width=device-width, initial-scale=1.0' }, { name: 'content-type', content: 'text/html; charset=UTF-8', @@ -42,11 +42,11 @@ module.exports = function(Meta) { }]; plugins.fireHook('filter:meta.getMetaTags', defaultTags, next); }, - links: function(next) { + links: function (next) { var defaultLinks = [{ rel: "icon", type: "image/x-icon", - href: nconf.get('relative_path') + '/favicon.ico?' + Meta.config['cache-buster'] + href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : '') }, { rel: "manifest", href: nconf.get('relative_path') + '/manifest.json' @@ -85,15 +85,19 @@ module.exports = function(Meta) { } plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next); } - }, function(err, results) { - meta = results.tags.concat(meta || []).map(function(tag) { + }, function (err, results) { + if (err) { + return callback(err); + } + + meta = results.tags.concat(meta || []).map(function (tag) { if (!tag || typeof tag.content !== 'string') { winston.warn('Invalid meta tag. ', tag); return tag; } if (!tag.noEscape) { - tag.content = validator.escape(tag.content); + tag.content = validator.escape(String(tag.content)); } return tag; @@ -112,7 +116,7 @@ module.exports = function(Meta) { function addDescription(meta) { var hasDescription = false; - meta.forEach(function(tag) { + meta.forEach(function (tag) { if (tag.name === 'description') { hasDescription = true; } @@ -121,7 +125,7 @@ module.exports = function(Meta) { if (!hasDescription) { meta.push({ name: 'description', - content: validator.escape(Meta.config.description || '') + content: validator.escape(String(Meta.config.description || '')) }); } } diff --git a/src/meta/templates.js b/src/meta/templates.js index 88578b235e..d335709461 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -1,22 +1,22 @@ "use strict"; -var mkdirp = require('mkdirp'), - rimraf = require('rimraf'), - winston = require('winston'), - async = require('async'), - path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - - emitter = require('../emitter'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'), - - Templates = {}, - searchIndex = {}; - -Templates.compile = function(callback) { - callback = callback || function() {}; +var mkdirp = require('mkdirp'); +var rimraf = require('rimraf'); +var winston = require('winston'); +var async = require('async'); +var path = require('path'); +var fs = require('fs'); +var nconf = require('nconf'); + +var emitter = require('../emitter'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); + +var Templates = {}; +var searchIndex = {}; + +Templates.compile = function (callback) { + callback = callback || function () {}; var fromFile = nconf.get('from-file') || ''; if (nconf.get('isPrimary') === 'false' || fromFile.match('tpl')) { @@ -58,13 +58,13 @@ function preparePaths(baseTemplatesPaths, callback) { function (next) { mkdirp(viewsPath, next); }, - function(viewsPath, next) { + function (viewsPath, next) { plugins.fireHook('static:templates.precompile', {}, next); }, - function(next) { + function (next) { plugins.getTemplates(next); } - ], function(err, pluginTemplates) { + ], function (err, pluginTemplates) { if (err) { return callback(err); } @@ -72,13 +72,13 @@ function preparePaths(baseTemplatesPaths, callback) { winston.verbose('[meta/templates] Compiling templates'); async.parallel({ - coreTpls: function(next) { + coreTpls: function (next) { utils.walk(coreTemplatesPath, next); }, - baseThemes: function(next) { - async.map(baseTemplatesPaths, function(baseTemplatePath, next) { - utils.walk(baseTemplatePath, function(err, paths) { - paths = paths.map(function(tpl) { + baseThemes: function (next) { + async.map(baseTemplatesPaths, function (baseTemplatePath, next) { + utils.walk(baseTemplatePath, function (err, paths) { + paths = paths.map(function (tpl) { return { base: baseTemplatePath, path: tpl.replace(baseTemplatePath, '') @@ -89,17 +89,17 @@ function preparePaths(baseTemplatesPaths, callback) { }); }, next); } - }, function(err, data) { + }, function (err, data) { var baseThemes = data.baseThemes, coreTpls = data.coreTpls, paths = {}; - coreTpls.forEach(function(el, i) { + coreTpls.forEach(function (el, i) { paths[coreTpls[i].replace(coreTemplatesPath, '')] = coreTpls[i]; }); - baseThemes.forEach(function(baseTpls) { - baseTpls.forEach(function(el, i) { + baseThemes.forEach(function (baseTpls) { + baseTpls.forEach(function (el, i) { paths[baseTpls[i].path] = path.join(baseTpls[i].base, baseTpls[i].path); }); }); @@ -121,12 +121,12 @@ function compile(callback) { viewsPath = nconf.get('views_dir'); - preparePaths(baseTemplatesPaths, function(err, paths) { + preparePaths(baseTemplatesPaths, function (err, paths) { if (err) { return callback(err); } - async.each(Object.keys(paths), function(relativePath, next) { + async.each(Object.keys(paths), function (relativePath, next) { var file = fs.readFileSync(paths[relativePath]).toString(), matches = null, regex = /[ \t]*<!-- IMPORT ([\s\S]*?)? -->[ \t]*/; @@ -148,13 +148,13 @@ function compile(callback) { mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/'))); fs.writeFile(path.join(viewsPath, relativePath), file, next); - }, function(err) { + }, function (err) { if (err) { winston.error('[meta/templates] ' + err.stack); return callback(err); } - compileIndex(viewsPath, function() { + compileIndex(viewsPath, function () { winston.verbose('[meta/templates] Successfully compiled templates.'); emitter.emit('templates:compiled'); diff --git a/src/meta/themes.js b/src/meta/themes.js index c3d912a222..416b9cb00d 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -1,17 +1,16 @@ 'use strict'; -var nconf = require('nconf'), - winston = require('winston'), - fs = require('fs'), - path = require('path'), - async = require('async'), +var nconf = require('nconf'); +var winston = require('winston'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); - file = require('../file'), - db = require('../database'), - meta = require('../meta'); +var file = require('../file'); +var db = require('../database'); -module.exports = function(Meta) { +module.exports = function (Meta) { Meta.themes = {}; Meta.themes.get = function (callback) { @@ -56,6 +55,10 @@ module.exports = function(Meta) { }); }, function (err, themes) { + if (err) { + return callback(err); + } + themes = themes.filter(function (theme) { return (theme !== undefined); }); @@ -65,7 +68,7 @@ module.exports = function(Meta) { }); }; - Meta.themes.set = function(data, callback) { + Meta.themes.set = function (data, callback) { var themeData = { 'theme:type': data.type, 'theme:id': data.id, @@ -77,17 +80,17 @@ module.exports = function(Meta) { switch(data.type) { case 'local': async.waterfall([ - async.apply(meta.configs.get, 'theme:id'), - function(current, next) { + async.apply(Meta.configs.get, 'theme:id'), + function (current, next) { async.series([ async.apply(db.sortedSetRemove, 'plugins:active', current), async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id) - ], function(err) { + ], function (err) { next(err); }); }, - function(next) { - fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function(err, config) { + function (next) { + fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) { if (!err) { config = JSON.parse(config.toString()); next(null, config); @@ -96,7 +99,7 @@ module.exports = function(Meta) { } }); }, - function(config, next) { + function (config, next) { themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; themeData['theme:templates'] = config.templates ? config.templates : ''; themeData['theme:src'] = ''; @@ -117,20 +120,20 @@ module.exports = function(Meta) { } }; - Meta.themes.setupPaths = function(callback) { + Meta.themes.setupPaths = function (callback) { async.parallel({ themesData: Meta.themes.get, - currentThemeId: function(next) { + currentThemeId: function (next) { db.getObjectField('config', 'theme:id', next); } - }, function(err, data) { + }, function (err, data) { if (err) { return callback(err); } var themeId = data.currentThemeId || 'nodebb-theme-persona'; - var themeObj = data.themesData.filter(function(themeObj) { + var themeObj = data.themesData.filter(function (themeObj) { return themeObj.id === themeId; })[0]; @@ -147,7 +150,7 @@ module.exports = function(Meta) { }); }; - Meta.themes.setPath = function(themeObj) { + Meta.themes.setPath = function (themeObj) { // Theme's templates path var themePath = nconf.get('base_templates_path'), fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates'); @@ -161,6 +164,4 @@ module.exports = function(Meta) { nconf.set('theme_templates_path', themePath); nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json')); }; - - }; \ No newline at end of file diff --git a/src/middleware/admin.js b/src/middleware/admin.js index ceeabdca99..3240eaf0af 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -1,129 +1,122 @@ "use strict"; -var app, - middleware = {}, - nconf = require('nconf'), - async = require('async'), - winston = require('winston'), - user = require('../user'), - meta = require('../meta'), - plugins = require('../plugins'), - - controllers = { - api: require('../controllers/api'), - helpers: require('../controllers/helpers') - }; - -middleware.isAdmin = function(req, res, next) { - winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!'); +var async = require('async'); +var winston = require('winston'); +var user = require('../user'); +var meta = require('../meta'); +var plugins = require('../plugins'); + +var controllers = { + api: require('../controllers/api'), + helpers: require('../controllers/helpers') +}; - if (!req.user) { - return controllers.helpers.notAllowed(req, res); - } +module.exports = function (middleware) { + middleware.admin = {}; + middleware.admin.isAdmin = function (req, res, next) { + winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!'); - user.isAdministrator(req.user.uid, function (err, isAdmin) { - if (err || isAdmin) { - return next(err); + if (!req.user) { + return controllers.helpers.notAllowed(req, res); } - controllers.helpers.notAllowed(req, res); - }); -}; + user.isAdministrator(req.user.uid, function (err, isAdmin) { + if (err || isAdmin) { + return next(err); + } -middleware.buildHeader = function(req, res, next) { - res.locals.renderAdminHeader = true; + controllers.helpers.notAllowed(req, res); + }); + }; - async.parallel({ - config: function(next) { - controllers.api.getConfig(req, res, next); - }, - footer: function(next) { - app.render('admin/footer', {}, next); - } - }, function(err, results) { - if (err) { - return next(err); - } + middleware.admin.buildHeader = function (req, res, next) { + res.locals.renderAdminHeader = true; - res.locals.config = results.config; - res.locals.adminFooter = results.footer; - next(); - }); -}; + controllers.api.getConfig(req, res, function (err, config) { + if (err) { + return next(err); + } -middleware.renderHeader = function(req, res, data, next) { - var custom_header = { - 'plugins': [], - 'authentication': [] + res.locals.config = config; + next(); + }); }; - user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) { - if (err) { - return next(err); - } + middleware.admin.renderHeader = function (req, res, data, next) { + var custom_header = { + 'plugins': [], + 'authentication': [] + }; - userData.uid = req.uid; - userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1; - - async.parallel({ - scripts: function(next) { - plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) { - if (err) { - return next(err); - } - var arr = []; - scripts.forEach(function(script) { - arr.push({src: script}); - }); - - next(null, arr); - }); - }, - custom_header: function(next) { - plugins.fireHook('filter:admin.header.build', custom_header, next); - }, - config: function(next) { - controllers.api.getConfig(req, res, next); - }, - configs: function(next) { - meta.configs.list(next); - } - }, function(err, results) { + user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function (err, userData) { if (err) { return next(err); } - res.locals.config = results.config; - var acpPath = req.path.slice(1).split('/'); - acpPath.forEach(function(path, i) { - acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); + userData.uid = req.uid; + userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1; + + async.parallel({ + scripts: function (next) { + plugins.fireHook('filter:admin.scripts.get', [], function (err, scripts) { + if (err) { + return next(err); + } + var arr = []; + scripts.forEach(function (script) { + arr.push({src: script}); + }); + + next(null, arr); + }); + }, + custom_header: function (next) { + plugins.fireHook('filter:admin.header.build', custom_header, next); + }, + config: function (next) { + controllers.api.getConfig(req, res, next); + }, + configs: function (next) { + meta.configs.list(next); + } + }, function (err, results) { + if (err) { + return next(err); + } + res.locals.config = results.config; + + var acpPath = req.path.slice(1).split('/'); + acpPath.forEach(function (path, i) { + acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); + }); + acpPath = acpPath.join(' > '); + + var templateValues = { + config: results.config, + configJSON: JSON.stringify(results.config), + relative_path: results.config.relative_path, + adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), + user: userData, + userJSON: JSON.stringify(userData).replace(/'/g, "\\'"), + plugins: results.custom_header.plugins, + authentication: results.custom_header.authentication, + scripts: results.scripts, + 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', + env: process.env.NODE_ENV ? true : false, + title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel', + bodyClass: data.bodyClass + }; + + templateValues.template = {name: res.locals.template}; + templateValues.template[res.locals.template] = true; + + req.app.render('admin/header', templateValues, next); }); - acpPath = acpPath.join(' > '); - - var templateValues = { - config: results.config, - configJSON: JSON.stringify(results.config), - relative_path: results.config.relative_path, - adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), - user: userData, - userJSON: JSON.stringify(userData).replace(/'/g, "\\'"), - plugins: results.custom_header.plugins, - authentication: results.custom_header.authentication, - scripts: results.scripts, - 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', - env: process.env.NODE_ENV ? true : false, - title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel' - }; - - templateValues.template = {name: res.locals.template}; - templateValues.template[res.locals.template] = true; - - app.render('admin/header', templateValues, next); }); - }); -}; + }; + -module.exports = function(webserver) { - app = webserver; - return middleware; + middleware.admin.renderFooter = function (req, res, data, next) { + req.app.render('admin/footer', data, next); + }; }; diff --git a/src/middleware/header.js b/src/middleware/header.js index c1ed6d60c2..2741a599f8 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -3,50 +3,44 @@ var async = require('async'); var nconf = require('nconf'); +var db = require('../database'); var user = require('../user'); var meta = require('../meta'); var plugins = require('../plugins'); var navigation = require('../navigation'); -var translator = require('../../public/src/modules/translator'); var controllers = { api: require('../controllers/api'), helpers: require('../controllers/helpers') }; -module.exports = function(app, middleware) { +module.exports = function (middleware) { - middleware.buildHeader = function(req, res, next) { + middleware.buildHeader = function (req, res, next) { res.locals.renderHeader = true; res.locals.isAPI = false; - - middleware.applyCSRF(req, res, function() { - async.parallel({ - config: function(next) { - controllers.api.getConfig(req, res, next); - }, - footer: function(next) { - app.render('footer', {loggedIn: (req.user ? parseInt(req.user.uid, 10) !== 0 : false)}, next); - }, - plugins: function(next) { - plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next); - } - }, function(err, results) { - if (err) { - return next(err); - } - + async.waterfall([ + function (next) { + middleware.applyCSRF(req, res, next); + }, + function (next) { + async.parallel({ + config: function (next) { + controllers.api.getConfig(req, res, next); + }, + plugins: function (next) { + plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next); + } + }, next); + }, + function (results, next) { res.locals.config = results.config; - - translator.translate(results.footer, results.config.defaultLang, function(parsedTemplate) { - res.locals.footer = parsedTemplate; - next(); - }); - }); - }); + next(); + } + ], next); }; - middleware.renderHeader = function(req, res, data, callback) { + middleware.renderHeader = function (req, res, data, callback) { var registrationType = meta.config.registrationType || 'normal'; var templateValues = { bootswatchCSS: meta.config['theme:src'], @@ -56,8 +50,8 @@ module.exports = function(app, middleware) { 'brand:logo': meta.config['brand:logo'] || '', 'brand:logo:url': meta.config['brand:logo:url'] || '', 'brand:logo:alt': meta.config['brand:logo:alt'] || '', - 'brand:logo:display': meta.config['brand:logo']?'':'hide', - allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval', + 'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide', + allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval' || registrationType === 'admin-approval-ip', searchEnabled: plugins.hasListeners('filter:search.query'), config: res.locals.config, relative_path: nconf.get('relative_path'), @@ -67,32 +61,45 @@ module.exports = function(app, middleware) { templateValues.configJSON = JSON.stringify(res.locals.config); async.parallel({ - scripts: function(next) { + scripts: function (next) { plugins.fireHook('filter:scripts.get', [], next); }, - isAdmin: function(next) { + isAdmin: function (next) { user.isAdministrator(req.uid, next); }, - isGlobalMod: function(next) { + isGlobalMod: function (next) { user.isGlobalModerator(req.uid, next); }, - user: function(next) { + isModerator: function (next) { + user.isModeratorOfAnyCategory(req.uid, next); + }, + user: function (next) { + var userData = { + uid: 0, + username: '[[global:guest]]', + userslug: '', + email: '', + picture: meta.config.defaultAvatar, + status: 'offline', + banned: false, + reputation: 0, + 'email:confirmed': false + }; if (req.uid) { - user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'status', 'email:confirmed', 'banned'], next); + user.getUserFields(req.uid, Object.keys(userData), next); } else { - next(null, { - username: '[[global:guest]]', - userslug: '', - picture: meta.config.defaultAvatar, - status: 'offline', - banned: false, - uid: 0 - }); + next(null, userData); } }, + isEmailConfirmSent: function (next) { + if (!meta.config.requireEmailConfirmation || !req.uid) { + return next(null, false); + } + db.get('uid:' + req.uid + ':confirm:email:sent', next); + }, navigation: async.apply(navigation.get), tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -104,8 +111,11 @@ module.exports = function(app, middleware) { results.user.isAdmin = results.isAdmin; results.user.isGlobalMod = results.isGlobalMod; + results.user.isMod = !!results.isModerator; results.user.uid = parseInt(results.user.uid, 10); + results.user.email = String(results.user.email).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; + results.user.isEmailConfirmSent = !!results.isEmailConfirmSent; if (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') { templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css'; @@ -117,6 +127,7 @@ module.exports = function(app, middleware) { templateValues.linkTags = results.tags.link; templateValues.isAdmin = results.user.isAdmin; templateValues.isGlobalMod = results.user.isGlobalMod; + templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; templateValues.user = results.user; templateValues.userJSON = JSON.stringify(results.user); templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; @@ -131,7 +142,7 @@ module.exports = function(app, middleware) { templateValues.template = {name: res.locals.template}; templateValues.template[res.locals.template] = true; - templateValues.scripts = results.scripts.map(function(script) { + templateValues.scripts = results.scripts.map(function (script) { return {src: script}; }); @@ -139,23 +150,31 @@ module.exports = function(app, middleware) { modifyTitle(templateValues); } - plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function(err, data) { + plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function (err, data) { if (err) { return callback(err); } - app.render('header', data.templateValues, callback); + req.app.render('header', data.templateValues, callback); }); }); }; + middleware.renderFooter = function (req, res, data, callback) { + plugins.fireHook('filter:middleware.renderFooter', {templateValues: data, req: req, res: res}, function (err, data) { + if (err) { + return callback(err); + } + req.app.render('footer', data.templateValues, callback); + }); + }; function modifyTitle(obj) { var title = controllers.helpers.buildTitle('[[pages:home]]'); obj.browserTitle = title; if (obj.metaTags) { - obj.metaTags.forEach(function(tag, i) { + obj.metaTags.forEach(function (tag, i) { if (tag.property === 'og:title') { obj.metaTags[i].content = title; } diff --git a/src/middleware/headers.js b/src/middleware/headers.js new file mode 100644 index 0000000000..f6e0a22562 --- /dev/null +++ b/src/middleware/headers.js @@ -0,0 +1,51 @@ +'use strict'; + + +var meta = require('../meta'); +var _ = require('underscore'); + + +module.exports = function (middleware) { + + middleware.addHeaders = function (req, res, next) { + var defaults = { + 'X-Powered-By': 'NodeBB', + 'X-Frame-Options': 'SAMEORIGIN', + 'Access-Control-Allow-Origin': 'null' // yes, string null. + }; + var headers = { + 'X-Powered-By': meta.config['powered-by'], + 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined, + 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'], + 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'], + 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers'] + }; + + _.defaults(headers, defaults); + headers = _.pick(headers, Boolean); // Remove falsy headers + + for(var key in headers) { + if (headers.hasOwnProperty(key)) { + res.setHeader(key, headers[key]); + } + } + + next(); + }; + + middleware.addExpiresHeaders = function (req, res, next) { + if (req.app.enabled('cache')) { + res.setHeader("Cache-Control", "public, max-age=5184000"); + res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString()); + } else { + res.setHeader("Cache-Control", "public, max-age=0"); + res.setHeader("Expires", new Date().toUTCString()); + } + + next(); + }; + +}; + + + diff --git a/src/middleware/index.js b/src/middleware/index.js index 1cbac02323..fdef19db32 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -1,79 +1,202 @@ "use strict"; -var meta = require('../meta'), - db = require('../database'), - file = require('../file'), - auth = require('../routes/authentication'), - - path = require('path'), - nconf = require('nconf'), - flash = require('connect-flash'), - templates = require('templates.js'), - bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - compression = require('compression'), - favicon = require('serve-favicon'), - session = require('express-session'), - useragent = require('express-useragent'); - +var async = require('async'); +var fs = require('fs'); +var path = require('path'); +var csrf = require('csurf'); +var validator = require('validator'); +var nconf = require('nconf'); +var ensureLoggedIn = require('connect-ensure-login'); +var toobusy = require('toobusy-js'); + +var plugins = require('../plugins'); +var languages = require('../languages'); +var meta = require('../meta'); +var user = require('../user'); +var groups = require('../groups'); + +var analytics = require('../analytics'); + +var controllers = { + api: require('./../controllers/api'), + helpers: require('../controllers/helpers') +}; var middleware = {}; -function setupFavicon(app) { - var faviconPath = path.join(__dirname, '../../', 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico'); - if (file.existsSync(faviconPath)) { - app.use(nconf.get('relative_path'), favicon(faviconPath)); +middleware.applyCSRF = csrf(); + +middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login'); + +require('./admin')(middleware); +require('./header')(middleware); +require('./render')(middleware); +require('./maintenance')(middleware); +require('./user')(middleware); +require('./headers')(middleware); + +middleware.authenticate = function (req, res, next) { + if (req.user) { + return next(); + } else if (plugins.hasListeners('action:middleware.authenticate')) { + return plugins.fireHook('action:middleware.authenticate', { + req: req, + res: res, + next: next + }); } -} -module.exports = function(app) { - var relativePath = nconf.get('relative_path'); + controllers.helpers.notAllowed(req, res); +}; - middleware = require('./middleware')(app); +middleware.pageView = function (req, res, next) { + analytics.pageView({ + ip: req.ip, + path: req.path, + uid: req.uid + }); + + plugins.fireHook('action:middleware.pageView', {req: req}); + + if (req.user) { + user.updateLastOnlineTime(req.user.uid); + if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) { + user.updateOnlineUsers(req.user.uid, next); + } else { + user.updateOnlineUsers(req.user.uid); + next(); + } + } else { + next(); + } +}; + + +middleware.pluginHooks = function (req, res, next) { + async.each(plugins.loadedHooks['filter:router.page'] || [], function (hookObj, next) { + hookObj.method(req, res, next); + }, function () { + // If it got here, then none of the subscribed hooks did anything, or there were no hooks + next(); + }); +}; - app.engine('tpl', templates.__express); - app.set('view engine', 'tpl'); - app.set('views', nconf.get('views_dir')); - app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); - app.use(flash()); +middleware.validateFiles = function (req, res, next) { + if (!Array.isArray(req.files.files) || !req.files.files.length) { + return next(new Error(['[[error:invalid-files]]'])); + } - app.enable('view cache'); + next(); +}; - app.use(compression()); +middleware.prepareAPI = function (req, res, next) { + res.locals.isAPI = true; + next(); +}; - setupFavicon(app); +middleware.routeTouchIcon = function (req, res) { + if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) { + return res.redirect(meta.config['brand:touchIcon']); + } else { + return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } +}; - app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon); +middleware.privateTagListing = function (req, res, next) { + if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { + controllers.helpers.notAllowed(req, res); + } else { + next(); + } +}; - app.use(bodyParser.urlencoded({extended: true})); - app.use(bodyParser.json()); - app.use(cookieParser()); - app.use(useragent.express()); +middleware.exposeGroupName = function (req, res, next) { + expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); +}; - var cookie = { - maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14) - }; +middleware.exposeUid = function (req, res, next) { + expose('uid', user.getUidByUserslug, 'userslug', req, res, next); +}; - if (meta.config.cookieDomain) { - cookie.domain = meta.config.cookieDomain; +function expose(exposedField, method, field, req, res, next) { + if (!req.params.hasOwnProperty(field)) { + return next(); } + method(req.params[field], function (err, id) { + if (err) { + return next(err); + } + + res.locals[exposedField] = id; + next(); + }); +} - if (nconf.get('secure')) { - cookie.secure = true; +middleware.privateUploads = function (req, res, next) { + if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { + return next(); + } + if (req.path.startsWith('/uploads/files')) { + return res.status(403).json('not-allowed'); } + next(); +}; - app.use(session({ - store: db.sessionStore, - secret: nconf.get('secret'), - key: 'express.sid', - cookie: cookie, - resave: true, - saveUninitialized: true - })); +middleware.busyCheck = function (req, res, next) { + if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) { + analytics.increment('errors:503'); + res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); + } else { + next(); + } +}; - app.use(middleware.addHeaders); - app.use(middleware.processRender); - auth.initialize(app, middleware); +middleware.applyBlacklist = function (req, res, next) { + meta.blacklist.test(req.ip, function (err) { + next(err); + }); +}; + +middleware.processLanguages = function (req, res, next) { + var code = req.params.code; + var key = req.path.match(/[\w]+\.json/); - return middleware; + if (code && key) { + languages.get(code, key[0], function (err, language) { + if (err) { + return next(err); + } + + res.status(200).json(language); + }); + } else { + res.status(404).json('{}'); + } }; + +middleware.processTimeagoLocales = function (req, res, next) { + var fallback = req.path.indexOf('-short') === -1 ? 'jquery.timeago.en.js' : 'jquery.timeago.en-short.js', + localPath = path.join(__dirname, '../../public/vendor/jquery/timeago/locales', req.path), + exists; + + try { + exists = fs.accessSync(localPath, fs.F_OK | fs.R_OK); + } catch(e) { + exists = false; + } + + if (exists) { + res.status(200).sendFile(localPath, { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } else { + res.status(200).sendFile(path.join(__dirname, '../../public/vendor/jquery/timeago/locales', fallback), { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } +}; + + +module.exports = middleware; diff --git a/src/middleware/maintenance.js b/src/middleware/maintenance.js index f03a804bb9..3abe15f6f8 100644 --- a/src/middleware/maintenance.js +++ b/src/middleware/maintenance.js @@ -3,54 +3,56 @@ var nconf = require('nconf'); var meta = require('../meta'); var user = require('../user'); -var translator = require('../../public/src/modules/translator'); +module.exports = function (middleware) { -module.exports = function(middleware) { - - middleware.maintenanceMode = function(req, res, next) { + middleware.maintenanceMode = function (req, res, next) { if (parseInt(meta.config.maintenanceMode, 10) !== 1) { return next(); } var url = req.url.replace(nconf.get('relative_path'), ''); var allowedRoutes = [ - '^/login', - '^/stylesheet.css', - '^/favicon', - '^/nodebb.min.js', - '^/vendor/fontawesome/fonts/fontawesome-webfont.woff', - '^/src/(modules|client)/[\\w/]+.js', - '^/templates/[\\w/]+.tpl', - '^/api/login', - '^/api/widgets/render', - '^/language/.+', - '^/uploads/system/site-logo.png' - ], - render = function() { - res.status(503); - var data = { - site_title: meta.config.title || 'NodeBB', - message: meta.config.maintenanceModeMessage - }; - if (!isApiRoute.test(url)) { - middleware.buildHeader(req, res, function() { - res.render('503', data); - }); - } else { - res.json(data); - } - }, - isAllowed = function(url) { - for(var x=0,numAllowed=allowedRoutes.length,route;x<numAllowed;x++) { - route = new RegExp(allowedRoutes[x]); - if (route.test(url)) { - return true; - } + '^/ping', + '^/sping', + '^/login', + '^/stylesheet.css', + '^/favicon', + '^/nodebb.min.js', + '^/vendor/fontawesome/fonts/fontawesome-webfont.woff', + '^/src/(modules|client)/[\\w/]+.js', + '^/templates/[\\w/]+.tpl', + '^/api/login', + '^/api/widgets/render', + '^/language/.+', + '^/uploads/system/site-logo.png' + ]; + var render = function () { + res.status(503); + var data = { + site_title: meta.config.title || 'NodeBB', + message: meta.config.maintenanceModeMessage + }; + if (!isApiRoute.test(url)) { + middleware.buildHeader(req, res, function () { + res.render('503', data); + }); + } else { + res.json(data); + } + }; + + var isAllowed = function (url) { + for(var x = 0,numAllowed = allowedRoutes.length,route;x < numAllowed;x++) { + route = new RegExp(allowedRoutes[x]); + if (route.test(url)) { + return true; } - return false; - }, - isApiRoute = /^\/api/; + } + return false; + }; + + var isApiRoute = /^\/api/; if (isAllowed(url)) { return next(); @@ -60,7 +62,7 @@ module.exports = function(middleware) { return render(); } - user.isAdministrator(req.user.uid, function(err, isAdmin) { + user.isAdministrator(req.user.uid, function (err, isAdmin) { if (err) { return next(err); } diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js deleted file mode 100644 index d09270c3c9..0000000000 --- a/src/middleware/middleware.js +++ /dev/null @@ -1,275 +0,0 @@ -"use strict"; - -var app, - middleware = { - admin: {} - }, - async = require('async'), - path = require('path'), - csrf = require('csurf'), - _ = require('underscore'), - - validator = require('validator'), - nconf = require('nconf'), - ensureLoggedIn = require('connect-ensure-login'), - toobusy = require('toobusy-js'), - - plugins = require('../plugins'), - meta = require('../meta'), - user = require('../user'), - groups = require('../groups'), - - analytics = require('../analytics'), - - controllers = { - api: require('./../controllers/api'), - helpers: require('../controllers/helpers') - }; - -toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); -toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); - -middleware.authenticate = function(req, res, next) { - if (req.user) { - return next(); - } else if (plugins.hasListeners('action:middleware.authenticate')) { - return plugins.fireHook('action:middleware.authenticate', { - req: req, - res: res, - next: next - }); - } - - controllers.helpers.notAllowed(req, res); -}; - -middleware.applyCSRF = csrf(); - -middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login'); - -middleware.pageView = function(req, res, next) { - analytics.pageView({ - ip: req.ip, - path: req.path, - uid: req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') ? parseInt(req.user.uid, 10) : 0 - }); - - plugins.fireHook('action:middleware.pageView', {req: req}); - - if (req.user) { - user.updateLastOnlineTime(req.user.uid); - if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) { - user.updateOnlineUsers(req.user.uid, next); - } else { - user.updateOnlineUsers(req.user.uid); - next(); - } - } else { - next(); - } -}; - -middleware.addHeaders = function (req, res, next) { - var defaults = { - 'X-Powered-By': 'NodeBB', - 'X-Frame-Options': 'SAMEORIGIN', - 'Access-Control-Allow-Origin': 'null' // yes, string null. - }; - var headers = { - 'X-Powered-By': meta.config['powered-by'], - 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined, - 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'], - 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'], - 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers'] - }; - - _.defaults(headers, defaults); - headers = _.pick(headers, Boolean); // Remove falsy headers - - for(var key in headers) { - res.setHeader(key, headers[key]); - } - - next(); -}; - -middleware.pluginHooks = function(req, res, next) { - async.each(plugins.loadedHooks['filter:router.page'] || [], function(hookObj, next) { - hookObj.method(req, res, next); - }, function() { - // If it got here, then none of the subscribed hooks did anything, or there were no hooks - next(); - }); -}; - -middleware.redirectToAccountIfLoggedIn = function(req, res, next) { - if (!req.user) { - return next(); - } - user.getUserField(req.user.uid, 'userslug', function (err, userslug) { - if (err) { - return next(err); - } - controllers.helpers.redirect(res, '/user/' + userslug); - }); -}; - -middleware.validateFiles = function(req, res, next) { - if (!Array.isArray(req.files.files) || !req.files.files.length) { - return next(new Error(['[[error:invalid-files]]'])); - } - - next(); -}; - -middleware.prepareAPI = function(req, res, next) { - res.locals.isAPI = true; - next(); -}; - -middleware.checkGlobalPrivacySettings = function(req, res, next) { - if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) { - return controllers.helpers.notAllowed(req, res); - } - - next(); -}; - -middleware.checkAccountPermissions = function(req, res, next) { - // This middleware ensures that only the requested user and admins can pass - async.waterfall([ - function (next) { - middleware.authenticate(req, res, next); - }, - function (next) { - user.getUidByUserslug(req.params.userslug, next); - }, - function (uid, next) { - if (parseInt(uid, 10) === req.uid) { - return next(null, true); - } - - user.isAdminOrGlobalMod(req.uid, next); - } - ], function (err, allowed) { - if (err || allowed) { - return next(err); - } - controllers.helpers.notAllowed(req, res); - }); -}; - -middleware.isAdmin = function(req, res, next) { - if (!req.uid) { - return controllers.helpers.notAllowed(req, res); - } - - user.isAdministrator(req.uid, function (err, isAdmin) { - if (err || isAdmin) { - return next(err); - } - - if (res.locals.isAPI) { - return controllers.helpers.notAllowed(req, res); - } - - middleware.buildHeader(req, res, function() { - controllers.helpers.notAllowed(req, res); - }); - }); -}; - -middleware.routeTouchIcon = function(req, res) { - if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) { - return res.redirect(meta.config['brand:touchIcon']); - } else { - return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - }); - } -}; - -middleware.addExpiresHeaders = function(req, res, next) { - if (app.enabled('cache')) { - res.setHeader("Cache-Control", "public, max-age=5184000"); - res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString()); - } else { - res.setHeader("Cache-Control", "public, max-age=0"); - res.setHeader("Expires", new Date().toUTCString()); - } - - next(); -}; - -middleware.privateTagListing = function(req, res, next) { - if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { - controllers.helpers.notAllowed(req, res); - } else { - next(); - } -}; - -middleware.exposeGroupName = function(req, res, next) { - expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); -}; - -middleware.exposeUid = function(req, res, next) { - expose('uid', user.getUidByUserslug, 'userslug', req, res, next); -}; - -function expose(exposedField, method, field, req, res, next) { - if (!req.params.hasOwnProperty(field)) { - return next(); - } - method(req.params[field], function(err, id) { - if (err) { - return next(err); - } - - res.locals[exposedField] = id; - next(); - }); -} - -middleware.requireUser = function(req, res, next) { - if (req.user) { - return next(); - } - - res.render('403', {title: '[[global:403.title]]'}); -}; - -middleware.privateUploads = function(req, res, next) { - if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { - return next(); - } - if (req.path.startsWith('/uploads/files')) { - return res.status(403).json('not-allowed'); - } - next(); -}; - -middleware.busyCheck = function(req, res, next) { - if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) { - res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); - } else { - next(); - } -}; - -middleware.applyBlacklist = function(req, res, next) { - meta.blacklist.test(req.ip, function(err) { - next(err); - }); -}; - -module.exports = function(webserver) { - app = webserver; - middleware.admin = require('./admin')(webserver); - - require('./header')(app, middleware); - require('./render')(middleware); - require('./maintenance')(middleware); - - return middleware; -}; diff --git a/src/middleware/ratelimit.js b/src/middleware/ratelimit.js index 7c244a6cd9..818fe99c12 100644 --- a/src/middleware/ratelimit.js +++ b/src/middleware/ratelimit.js @@ -8,7 +8,7 @@ var ratelimit = {}; var allowedCalls = 100; var timeframe = 10000; -ratelimit.isFlooding = function(socket) { +ratelimit.isFlooding = function (socket) { socket.callsPerSecond = socket.callsPerSecond || 0; socket.elapsedTime = socket.elapsedTime || 0; socket.lastCallTime = socket.lastCallTime || Date.now(); diff --git a/src/middleware/render.js b/src/middleware/render.js index 79911f71bd..f9124971c0 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -1,96 +1,116 @@ 'use strict'; +var async = require('async'); var nconf = require('nconf'); +var validator = require('validator'); + +var plugins = require('../plugins'); var translator = require('../../public/src/modules/translator'); -module.exports = function(middleware) { +module.exports = function (middleware) { - middleware.processRender = function(req, res, next) { + middleware.processRender = function (req, res, next) { // res.render post-processing, modified from here: https://gist.github.com/mrlannigan/5051687 var render = res.render; - res.render = function(template, options, fn) { - var self = this, - req = this.req, - defaultFn = function(err, str){ - if (err) { - return next(err); - } + res.render = function (template, options, fn) { + var self = this; + var req = this.req; + var defaultFn = function (err, str) { + if (err) { + return next(err); + } + self.send(str); + }; - self.send(str); - }; options = options || {}; - if ('function' === typeof options) { fn = options; options = {}; } - - options.loggedIn = !!req.uid; - options.relative_path = nconf.get('relative_path'); - options.template = {name: template}; - options.template[template] = true; - options.bodyClass = buildBodyClass(req); - - res.locals.template = template; - options._locals = undefined; - - if (res.locals.isAPI) { - if (req.route && req.route.path === '/api/') { - options.title = '[[pages:home]]'; - } - - return res.json(options); - } - if ('function' !== typeof fn) { fn = defaultFn; } - var ajaxifyData = JSON.stringify(options); - ajaxifyData = ajaxifyData.replace(/<\//g, '<\\/'); - - render.call(self, template, options, function(err, str) { - if (err) { - return fn(err); - } - - str = (res.locals.postHeader ? res.locals.postHeader : '') + str + (res.locals.preFooter ? res.locals.preFooter : ''); + var ajaxifyData; + async.waterfall([ + function (next) { + plugins.fireHook('filter:' + template + '.build', {req: req, res: res, templateData: options}, next); + }, + function (data, next) { + options = data.templateData; + + options.loggedIn = !!req.uid; + options.relative_path = nconf.get('relative_path'); + options.template = {name: template}; + options.template[template] = true; + options.url = (req.baseUrl + req.path).replace(/^\/api/, ''); + options.bodyClass = buildBodyClass(req); + + res.locals.template = template; + options._locals = undefined; + + if (res.locals.isAPI) { + if (req.route && req.route.path === '/api/') { + options.title = '[[pages:home]]'; + } - if (res.locals.footer) { - str = str + res.locals.footer; - } else if (res.locals.adminFooter) { - str = str + res.locals.adminFooter; - } + return res.json(options); + } - if (res.locals.renderHeader || res.locals.renderAdminHeader) { - var method = res.locals.renderHeader ? middleware.renderHeader : middleware.admin.renderHeader; - method(req, res, options, function(err, template) { - if (err) { - return fn(err); + ajaxifyData = JSON.stringify(options).replace(/<\//g, '<\\/'); + + async.parallel({ + header: function (next) { + renderHeaderFooter('renderHeader', req, res, options, next); + }, + content: function (next) { + render.call(self, template, options, next); + }, + footer: function (next) { + renderHeaderFooter('renderFooter', req, res, options, next); } - str = template + str; - var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB'; - language = req.query.lang || language; - translator.translate(str, language, function(translated) { - translated = translator.unescape(translated); - translated = translated + '<script id="ajaxify-data" type="application/json">' + ajaxifyData + '</script>'; - fn(err, translated); - }); - }); - } else { - str = str + '<script id="ajaxify-data" type="application/json">' + ajaxifyData + '</script>'; - fn(err, str); + }, next); + }, + function (results, next) { + var str = results.header + + (res.locals.postHeader || '') + + results.content + + (res.locals.preFooter || '') + + results.footer; + + translate(str, req, res, next); + }, + function (translated, next) { + next(null, translated + '<script id="ajaxify-data" type="application/json">' + ajaxifyData + '</script>'); } - }); + ], fn); }; next(); }; + function renderHeaderFooter(method, req, res, options, next) { + if (res.locals.renderHeader) { + middleware[method](req, res, options, next); + } else if (res.locals.renderAdminHeader) { + middleware.admin[method](req, res, options, next); + } else { + next(null, ''); + } + } + + function translate(str, req, res, next) { + var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB'; + language = req.query.lang ? validator.escape(String(req.query.lang)) : language; + translator.translate(str, language, function (translated) { + next(null, translator.unescape(translated)); + }); + } + function buildBodyClass(req) { - var clean = req.path.replace(/^\/api/, '').replace(/^\//, ''); + var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, ''); var parts = clean.split('/').slice(0, 3); - parts.forEach(function(p, index) { + parts.forEach(function (p, index) { parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home'); }); return parts.join(' '); diff --git a/src/middleware/user.js b/src/middleware/user.js new file mode 100644 index 0000000000..a9dc90eb94 --- /dev/null +++ b/src/middleware/user.js @@ -0,0 +1,154 @@ +'use strict'; + +var async = require('async'); +var nconf = require('nconf'); +var meta = require('../meta'); +var user = require('../user'); + +var controllers = { + helpers: require('../controllers/helpers') +}; + +module.exports = function (middleware) { + + middleware.checkGlobalPrivacySettings = function (req, res, next) { + if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) { + return controllers.helpers.notAllowed(req, res); + } + + next(); + }; + + middleware.checkAccountPermissions = function (req, res, next) { + // This middleware ensures that only the requested user and admins can pass + async.waterfall([ + function (next) { + middleware.authenticate(req, res, next); + }, + function (next) { + user.getUidByUserslug(req.params.userslug, next); + }, + function (uid, next) { + if (parseInt(uid, 10) === req.uid) { + return next(null, true); + } + + user.isAdminOrGlobalMod(req.uid, next); + } + ], function (err, allowed) { + if (err || allowed) { + return next(err); + } + controllers.helpers.notAllowed(req, res); + }); + }; + + middleware.redirectToAccountIfLoggedIn = function (req, res, next) { + if (req.session.forceLogin) { + return next(); + } + + if (!req.user) { + return next(); + } + user.getUserField(req.user.uid, 'userslug', function (err, userslug) { + if (err) { + return next(err); + } + controllers.helpers.redirect(res, '/user/' + userslug); + }); + }; + + middleware.redirectUidToUserslug = function (req, res, next) { + var uid = parseInt(req.params.uid, 10); + if (!uid) { + return next(); + } + user.getUserField(uid, 'userslug', function (err, userslug) { + if (err || !userslug) { + return next(err); + } + + var path = req.path.replace(/^\/api/, '') + .replace('uid', 'user') + .replace(uid, function () { return userslug; }); + controllers.helpers.redirect(res, path); + }); + }; + + middleware.isAdmin = function (req, res, next) { + if (!req.uid) { + return controllers.helpers.notAllowed(req, res); + } + + user.isAdministrator(req.uid, function (err, isAdmin) { + if (err) { + return next(err); + } + + if (isAdmin) { + user.hasPassword(req.uid, function (err, hasPassword) { + if (err) { + return next(err); + } + + if (!hasPassword) { + return next(); + } + + var loginTime = req.session.meta ? req.session.meta.datetime : 0; + if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) { + var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000); + if (timeLeft < 300000) { + req.session.meta.datetime += 300000; + } + + return next(); + } + + req.session.returnTo = req.path.replace(/^\/api/, ''); + req.session.forceLogin = 1; + if (res.locals.isAPI) { + res.status(401).json({}); + } else { + res.redirect(nconf.get('relative_path') + '/login'); + } + }); + return; + } + + if (res.locals.isAPI) { + return controllers.helpers.notAllowed(req, res); + } + + middleware.buildHeader(req, res, function () { + controllers.helpers.notAllowed(req, res); + }); + }); + }; + + middleware.requireUser = function (req, res, next) { + if (req.user) { + return next(); + } + + res.status(403).render('403', {title: '[[global:403.title]]'}); + }; + + middleware.registrationComplete = function (req, res, next) { + // If the user's session contains registration data, redirect the user to complete registration + if (!req.session.hasOwnProperty('registration')) { + return next(); + } else { + if (!req.path.endsWith('/register/complete')) { + controllers.helpers.redirect(res, '/register/complete'); + } else { + return next(); + } + } + }; + +}; + + + diff --git a/src/navigation/admin.js b/src/navigation/admin.js index a803b5465b..a7caee97e0 100644 --- a/src/navigation/admin.js +++ b/src/navigation/admin.js @@ -9,9 +9,9 @@ var admin = {}, admin.cache = null; -admin.save = function(data, callback) { +admin.save = function (data, callback) { var order = Object.keys(data), - items = data.map(function(item, idx) { + items = data.map(function (item, idx) { var data = {}; for (var i in item) { @@ -26,29 +26,29 @@ admin.save = function(data, callback) { admin.cache = null; async.waterfall([ - function(next) { + function (next) { db.delete('navigation:enabled', next); }, - function(next) { + function (next) { db.sortedSetAdd('navigation:enabled', order, items, next); } ], callback); }; -admin.getAdmin = function(callback) { +admin.getAdmin = function (callback) { async.parallel({ enabled: admin.get, available: getAvailable }, callback); }; -admin.get = function(callback) { - db.getSortedSetRange('navigation:enabled', 0, -1, function(err, data) { +admin.get = function (callback) { + db.getSortedSetRange('navigation:enabled', 0, -1, function (err, data) { if (err) { return callback(err); } - data = data.map(function(item, idx) { + data = data.map(function (item, idx) { return JSON.parse(item)[idx]; }); @@ -57,17 +57,12 @@ admin.get = function(callback) { }; function getAvailable(callback) { - var core = require('../../install/data/navigation.json').map(function(item) { + var core = require('../../install/data/navigation.json').map(function (item) { item.core = true; return item; }); - // DEPRECATION: backwards compatibility for filter:header.build, will be removed soon. - plugins.fireHook('filter:header.build', {navigation: []}, function(err, data) { - core = core.concat(data.navigation); - - plugins.fireHook('filter:navigation.available', core, callback); - }); + plugins.fireHook('filter:navigation.available', core, callback); } module.exports = admin; \ No newline at end of file diff --git a/src/navigation/index.js b/src/navigation/index.js index ad4627fd5b..5563c44c4b 100644 --- a/src/navigation/index.js +++ b/src/navigation/index.js @@ -6,19 +6,19 @@ var translator = require('../../public/src/modules/translator'); var navigation = {}; -navigation.get = function(callback) { +navigation.get = function (callback) { if (admin.cache) { return callback(null, admin.cache); } - admin.get(function(err, data) { + admin.get(function (err, data) { if (err) { return callback(err); } - data = data.filter(function(item) { + data = data.filter(function (item) { return item && item.enabled; - }).map(function(item) { + }).map(function (item) { if (!item.route.startsWith('http')) { item.route = nconf.get('relative_path') + item.route; } diff --git a/src/notifications.js b/src/notifications.js index 94cb5313b8..c70c0cbb0a 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -1,122 +1,130 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - cron = require('cron').CronJob, - nconf = require('nconf'), - S = require('string'), - _ = require('underscore'), - - db = require('./database'), - User = require('./user'), - groups = require('./groups'), - meta = require('./meta'), - plugins = require('./plugins'); - -(function(Notifications) { - - Notifications.init = function() { +var async = require('async'); +var winston = require('winston'); +var cron = require('cron').CronJob; +var nconf = require('nconf'); +var S = require('string'); +var _ = require('underscore'); + +var db = require('./database'); +var User = require('./user'); +var groups = require('./groups'); +var meta = require('./meta'); +var batch = require('./batch'); +var plugins = require('./plugins'); +var utils = require('../public/src/utils'); + +(function (Notifications) { + + Notifications.init = function () { winston.verbose('[notifications.init] Registering jobs.'); new cron('*/30 * * * *', Notifications.prune, null, true); }; - Notifications.get = function(nid, callback) { - Notifications.getMultiple([nid], function(err, notifications) { + Notifications.get = function (nid, callback) { + Notifications.getMultiple([nid], function (err, notifications) { callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null); }); }; - Notifications.getMultiple = function(nids, callback) { - var keys = nids.map(function(nid) { + Notifications.getMultiple = function (nids, callback) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); - db.getObjects(keys, function(err, notifications) { + db.getObjects(keys, function (err, notifications) { if (err) { return callback(err); } - if (!Array.isArray(notifications) || !notifications.length) { + notifications = notifications.filter(Boolean); + if (!notifications.length) { return callback(null, []); } - async.map(notifications, function(notification, next) { - if (!notification) { - return next(null, null); - } + var userKeys = notifications.map(function (notification) { + return notification.from; + }); - if (notification.bodyLong) { - notification.bodyLong = S(notification.bodyLong).escapeHTML().s; + User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], function (err, usersData) { + if (err) { + return callback(err); } + notifications.forEach(function (notification, index) { + notification.datetimeISO = utils.toISOString(notification.datetime); - if (notification.from && !notification.image) { - User.getUserFields(notification.from, ['username', 'userslug', 'picture'], function(err, userData) { - if (err) { - return next(err); - } - notification.image = userData.picture || null; - notification.user = userData; + if (notification.bodyLong) { + notification.bodyLong = S(notification.bodyLong).escapeHTML().s; + } - if (userData.username === '[[global:guest]]') { + notification.user = usersData[index]; + if (notification.user) { + notification.image = notification.user.picture || null; + if (notification.user.username === '[[global:guest]]') { notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); } - - next(null, notification); - }); - return; - } else if (notification.image) { - switch(notification.image) { - case 'brand:logo': - notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; - break; + } else if (notification.image === 'brand:logo' || !notification.image) { + notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; } + }); - return next(null, notification); - } else { - notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; - return next(null, notification); - } + callback(null, notifications); + }); + }); + }; - }, callback); + Notifications.filterExists = function (nids, callback) { + // Removes nids that have been pruned + db.isSortedSetMembers('notifications', nids, function (err, exists) { + if (err) { + return callback(err); + } + + nids = nids.filter(function (notifId, idx) { + return exists[idx]; + }); + + callback(null, nids); }); }; - Notifications.findRelated = function(mergeIds, set, callback) { + Notifications.findRelated = function (mergeIds, set, callback) { // A related notification is one in a zset that has the same mergeId var _nids; async.waterfall([ async.apply(db.getSortedSetRevRange, set, 0, -1), - function(nids, next) { + function (nids, next) { _nids = nids; - var keys = nids.map(function(nid) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); db.getObjectsFields(keys, ['mergeId'], next); }, - ], function(err, sets) { + ], function (err, sets) { if (err) { return callback(err); } - sets = sets.map(function(set) { + sets = sets.map(function (set) { return set.mergeId; }); - callback(null, _nids.filter(function(nid, idx) { + callback(null, _nids.filter(function (nid, idx) { return mergeIds.indexOf(sets[idx]) !== -1; })); }); }; - Notifications.create = function(data, callback) { + Notifications.create = function (data, callback) { if (!data.nid) { return callback(new Error('no-notification-id')); } data.importance = data.importance || 5; - db.getObject('notifications:' + data.nid, function(err, oldNotification) { + db.getObject('notifications:' + data.nid, function (err, oldNotification) { if (err) { return callback(err); } @@ -130,22 +138,22 @@ var async = require('async'), var now = Date.now(); data.datetime = now; async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('notifications', now, data.nid, next); }, - function(next) { + function (next) { db.setObject('notifications:' + data.nid, data, next); } - ], function(err) { + ], function (err) { callback(err, data); }); }); }; - Notifications.push = function(notification, uids, callback) { - callback = callback || function() {}; + Notifications.push = function (notification, uids, callback) { + callback = callback || function () {}; - if (!notification.nid) { + if (!notification || !notification.nid) { return callback(); } @@ -153,7 +161,7 @@ var async = require('async'), uids = [uids]; } - uids = uids.filter(function(uid, index, array) { + uids = uids.filter(function (uid, index, array) { return parseInt(uid, 10) && array.indexOf(uid) === index; }); @@ -161,85 +169,65 @@ var async = require('async'), return callback(); } - var done = false; - var start = 0; - var batchSize = 50; - - setTimeout(function() { - async.whilst( - function() { - return !done; - }, - function(next) { - var currentUids = uids.slice(start, start + batchSize); - if (!currentUids.length) { - done = true; - return next(); - } - pushToUids(currentUids, notification, function(err) { - if (err) { - return next(err); - } - start = start + batchSize; - - setTimeout(next, 1000); - }); - }, - function(err) { - if (err) { - winston.error(err.stack); - } + setTimeout(function () { + batch.processArray(uids, function (uids, next) { + pushToUids(uids, notification, next); + }, {interval: 1000}, function (err) { + if (err) { + winston.error(err.stack); } - ); + }); }, 1000); callback(); }; function pushToUids(uids, notification, callback) { + var oneWeekAgo = Date.now() - 604800000; var unreadKeys = []; var readKeys = []; - uids.forEach(function(uid) { - unreadKeys.push('uid:' + uid + ':notifications:unread'); - readKeys.push('uid:' + uid + ':notifications:read'); - }); + async.waterfall([ + function (next) { + plugins.fireHook('filter:notification.push', {notification: notification, uids: uids}, next); + }, + function (data, next) { + uids = data.uids; + notification = data.notification; + + uids.forEach(function (uid) { + unreadKeys.push('uid:' + uid + ':notifications:unread'); + readKeys.push('uid:' + uid + ':notifications:read'); + }); - var oneWeekAgo = Date.now() - 604800000; - async.series([ - function(next) { db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next); }, - function(next) { + function (next) { db.sortedSetsRemove(readKeys, notification.nid, next); }, - function(next) { + function (next) { db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next); }, - function(next) { + function (next) { db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next); - } - ], function(err) { - if (err) { - return callback(err); - } - - plugins.fireHook('action:notification.pushed', {notification: notification, uids: uids}); + }, + function (next) { + var websockets = require('./socket.io'); + if (websockets.server) { + uids.forEach(function (uid) { + websockets.in('uid_' + uid).emit('event:new_notification', notification); + }); + } - var websockets = require('./socket.io'); - if (websockets.server) { - uids.forEach(function(uid) { - websockets.in('uid_' + uid).emit('event:new_notification', notification); - }); + plugins.fireHook('action:notification.pushed', {notification: notification, uids: uids}); + next(); } - - callback(); - }); + ], callback); } - Notifications.pushGroup = function(notification, groupName, callback) { - callback = callback || function() {}; - groups.getMembers(groupName, 0, -1, function(err, members) { + Notifications.pushGroup = function (notification, groupName, callback) { + callback = callback || function () {}; + groups.getMembers(groupName, 0, -1, function (err, members) { if (err || !Array.isArray(members) || !members.length) { return callback(err); } @@ -248,21 +236,38 @@ var async = require('async'), }); }; - Notifications.markRead = function(nid, uid, callback) { - callback = callback || function() {}; + Notifications.rescind = function (nid, callback) { + callback = callback || function () {}; + + async.parallel([ + async.apply(db.sortedSetRemove, 'notifications', nid), + async.apply(db.delete, 'notifications:' + nid) + ], function (err) { + if (err) { + winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message); + } else { + winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"'); + } + + callback(err, nid); + }); + }; + + Notifications.markRead = function (nid, uid, callback) { + callback = callback || function () {}; if (!parseInt(uid, 10) || !nid) { return callback(); } Notifications.markReadMultiple([nid], uid, callback); }; - Notifications.markUnread = function(nid, uid, callback) { - callback = callback || function() {}; + Notifications.markUnread = function (nid, uid, callback) { + callback = callback || function () {}; if (!parseInt(uid, 10) || !nid) { return callback(); } - db.getObject('notifications:' + nid, function(err, notification) { + db.getObject('notifications:' + nid, function (err, notification) { if (err || !notification) { return callback(err || new Error('[[error:no-notification]]')); } @@ -275,24 +280,24 @@ var async = require('async'), }); }; - Notifications.markReadMultiple = function(nids, uid, callback) { - callback = callback || function() {}; + Notifications.markReadMultiple = function (nids, uid, callback) { + callback = callback || function () {}; nids = nids.filter(Boolean); if (!Array.isArray(nids) || !nids.length) { return callback(); } - var notificationKeys = nids.map(function(nid) { + var notificationKeys = nids.map(function (nid) { return 'notifications:' + nid; }); async.waterfall([ async.apply(db.getObjectsFields, notificationKeys, ['mergeId']), - function(mergeIds, next) { + function (mergeIds, next) { // Isolate mergeIds and find related notifications - mergeIds = mergeIds.map(function(set) { + mergeIds = mergeIds.map(function (set) { return set.mergeId; - }).reduce(function(memo, mergeId, idx, arr) { + }).reduce(function (memo, mergeId, idx, arr) { if (mergeId && idx === arr.indexOf(mergeId)) { memo.push(mergeId); } @@ -301,45 +306,45 @@ var async = require('async'), Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next); }, - function(relatedNids, next) { - notificationKeys = _.union(nids, relatedNids).map(function(nid) { + function (relatedNids, next) { + notificationKeys = _.union(nids, relatedNids).map(function (nid) { return 'notifications:' + nid; }); db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next); } - ], function(err, notificationData) { + ], function (err, notificationData) { if (err) { return callback(err); } // Filter out notifications that didn't exist - notificationData = notificationData.filter(function(notification) { + notificationData = notificationData.filter(function (notification) { return notification && notification.nid; }); // Extract nid - nids = notificationData.map(function(notification) { + nids = notificationData.map(function (notification) { return notification.nid; }); - var datetimes = notificationData.map(function(notification) { + var datetimes = notificationData.map(function (notification) { return (notification && notification.datetime) || Date.now(); }); async.parallel([ - function(next) { + function (next) { db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); }, - function(next) { + function (next) { db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); } ], callback); }); }; - Notifications.markAllRead = function(uid, callback) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) { + Notifications.markAllRead = function (uid, callback) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { if (err) { return callback(err); } @@ -352,13 +357,13 @@ var async = require('async'), }); }; - Notifications.prune = function() { + Notifications.prune = function () { var week = 604800000, numPruned = 0; var cutoffTime = Date.now() - week; - db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function(err, nids) { + db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) { if (err) { return winston.error(err.message); } @@ -367,20 +372,20 @@ var async = require('async'), return; } - var keys = nids.map(function(nid) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); numPruned = nids.length; async.parallel([ - function(next) { + function (next) { db.sortedSetRemove('notifications', nids, next); }, - function(next) { + function (next) { db.deleteAll(keys, next); } - ], function(err) { + ], function (err) { if (err) { return winston.error('Encountered error pruning notifications: ' + err.message); } @@ -388,10 +393,9 @@ var async = require('async'), }); }; - Notifications.merge = function(notifications, callback) { + Notifications.merge = function (notifications, callback) { // When passed a set of notification objects, merge any that can be merged var mergeIds = [ - 'notifications:favourited_your_post_in', 'notifications:upvoted_your_post_in', 'notifications:user_started_following_you', 'notifications:user_posted_to', @@ -400,8 +404,8 @@ var async = require('async'), ], isolated, differentiators, differentiator, modifyIndex, set; - notifications = mergeIds.reduce(function(notifications, mergeId) { - isolated = notifications.filter(function(notifObj) { + notifications = mergeIds.reduce(function (notifications, mergeId) { + isolated = notifications.filter(function (notifObj) { if (!notifObj || !notifObj.hasOwnProperty('mergeId')) { return false; } @@ -414,7 +418,7 @@ var async = require('async'), } // Each isolated mergeId may have multiple differentiators, so process each separately - differentiators = isolated.reduce(function(cur, next) { + differentiators = isolated.reduce(function (cur, next) { differentiator = next.mergeId.split('|')[1] || 0; if (cur.indexOf(differentiator) === -1) { cur.push(differentiator); @@ -423,11 +427,11 @@ var async = require('async'), return cur; }, []); - differentiators.forEach(function(differentiator) { + differentiators.forEach(function (differentiator) { if (differentiator === 0 && differentiators.length === 1) { set = isolated; } else { - set = isolated.filter(function(notifObj) { + set = isolated.filter(function (notifObj) { return notifObj.mergeId === (mergeId + '|' + differentiator); }); } @@ -438,14 +442,14 @@ var async = require('async'), } switch(mergeId) { - case 'notifications:favourited_your_post_in': // intentional fall-through + // intentional fall-through case 'notifications:upvoted_your_post_in': case 'notifications:user_started_following_you': case 'notifications:user_posted_to': case 'notifications:user_flagged_post_in': - var usernames = set.map(function(notifObj) { + var usernames = set.map(function (notifObj) { return notifObj && notifObj.user && notifObj.user.username; - }).filter(function(username, idx, array) { + }).filter(function (username, idx, array) { return array.indexOf(username) === idx; }); var numUsers = usernames.length; @@ -459,6 +463,8 @@ var async = require('async'), } else if (numUsers > 2) { notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]'; } + + notifications[modifyIndex].path = set[set.length - 1].path; break; case 'new_register': @@ -467,7 +473,7 @@ var async = require('async'), } // Filter out duplicates - notifications = notifications.filter(function(notifObj, idx) { + notifications = notifications.filter(function (notifObj, idx) { if (!notifObj || !notifObj.mergeId) { return true; } @@ -481,7 +487,7 @@ var async = require('async'), plugins.fireHook('filter:notifications.merge', { notifications: notifications - }, function(err, data) { + }, function (err, data) { callback(err, data.notifications); }); }; diff --git a/src/pagination.js b/src/pagination.js index 15b8aa6eb3..347c71ae5c 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -4,7 +4,7 @@ var qs = require('querystring'); var pagination = {}; -pagination.create = function(currentPage, pageCount, queryObj) { +pagination.create = function (currentPage, pageCount, queryObj) { if (pageCount <= 1) { return { prev: {page: 1, active: currentPage > 1}, @@ -23,24 +23,26 @@ pagination.create = function(currentPage, pageCount, queryObj) { var next = Math.min(pageCount, currentPage + 1); var startPage = currentPage - 2; - for(var i=0; i<5; ++i) { + for(var i = 0; i < 5; ++i) { pagesToShow.push(startPage + i); } - pagesToShow = pagesToShow.filter(function(page, index, array) { + pagesToShow = pagesToShow.filter(function (page, index, array) { return page > 0 && page <= pageCount && array.indexOf(page) === index; - }).sort(function(a, b) { + }).sort(function (a, b) { return a - b; }); queryObj = queryObj || {}; - var pages = pagesToShow.map(function(page) { + delete queryObj._; + + var pages = pagesToShow.map(function (page) { queryObj.page = page; return {page: page, active: page === currentPage, qs: qs.stringify(queryObj)}; }); - for (i=pages.length - 1; i>0; --i) { + for (i = pages.length - 1; i > 0; --i) { if (pages[i - 1].page !== pages[i].page - 1) { pages.splice(i, 0, {separator: true}); } diff --git a/src/password.js b/src/password.js index 13f8c11f72..2744cbefba 100644 --- a/src/password.js +++ b/src/password.js @@ -1,22 +1,24 @@ 'use strict'; -(function(module) { +(function (module) { var fork = require('child_process').fork; - module.hash = function(rounds, password, callback) { + module.hash = function (rounds, password, callback) { forkChild({type: 'hash', rounds: rounds, password: password}, callback); }; - module.compare = function(password, hash, callback) { + module.compare = function (password, hash, callback) { forkChild({type: 'compare', password: password, hash: hash}, callback); }; function forkChild(message, callback) { - var child = fork('./bcrypt', { - silent: true - }); + var forkProcessParams = {}; + if(global.v8debug || parseInt(process.execArgv.indexOf('--debug'), 10) !== -1) { + forkProcessParams = {execArgv: ['--debug=' + (5859), '--nolazy']}; + } + var child = fork('./bcrypt', [], forkProcessParams); - child.on('message', function(msg) { + child.on('message', function (msg) { if (msg.err) { return callback(new Error(msg.err)); } @@ -28,4 +30,4 @@ } return module; -})(exports); \ No newline at end of file +}(exports)); \ No newline at end of file diff --git a/src/plugins.js b/src/plugins.js index 853c6f44f2..1274b829ef 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -10,16 +10,14 @@ var nconf = require('nconf'); var db = require('./database'); var emitter = require('./emitter'); -var translator = require('../public/src/modules/translator'); var utils = require('../public/src/utils'); var hotswap = require('./hotswap'); var file = require('./file'); -var controllers = require('./controllers'); var app; var middleware; -(function(Plugins) { +(function (Plugins) { require('./plugins/install')(Plugins); require('./plugins/load')(Plugins); require('./plugins/hooks')(Plugins); @@ -31,20 +29,20 @@ var middleware; Plugins.lessFiles = []; Plugins.clientScripts = []; Plugins.acpScripts = []; - Plugins.customLanguages = []; + Plugins.customLanguages = {}; Plugins.customLanguageFallbacks = {}; Plugins.libraryPaths = []; Plugins.versionWarning = []; Plugins.initialized = false; - Plugins.requireLibrary = function(pluginID, libraryPath) { + Plugins.requireLibrary = function (pluginID, libraryPath) { Plugins.libraries[pluginID] = require(libraryPath); Plugins.libraryPaths.push(libraryPath); }; - Plugins.init = function(nbbApp, nbbMiddleware, callback) { - callback = callback || function() {}; + Plugins.init = function (nbbApp, nbbMiddleware, callback) { + callback = callback || function () {}; if (Plugins.initialized) { return callback(); } @@ -57,7 +55,7 @@ var middleware; winston.verbose('[plugins] Initializing plugins system'); } - Plugins.reload(function(err) { + Plugins.reload(function (err) { if (err) { winston.error('[plugins] NodeBB encountered a problem while loading plugins', err.message); return callback(err); @@ -73,7 +71,7 @@ var middleware; }); }; - Plugins.reload = function(callback) { + Plugins.reload = function (callback) { // Resetting all local plugin data Plugins.libraries = {}; Plugins.loadedHooks = {}; @@ -85,44 +83,58 @@ var middleware; Plugins.acpScripts.length = 0; Plugins.libraryPaths.length = 0; - Plugins.registerHook('core', { - hook: 'static:app.load', - method: addLanguages - }); + // Plugins.registerHook('core', { + // hook: 'static:app.load', + // method: addLanguages + // }); async.waterfall([ - function(next) { + function (next) { + // Build language code list + fs.readdir(path.join(__dirname, '../public/language'), function (err, directories) { + if (err) { + return next(err); + } + + Plugins.languageCodes = directories.filter(function (code) { + return code !== 'TODO'; + }); + + next(); + }); + }, + function (next) { db.getSortedSetRange('plugins:active', 0, -1, next); }, - function(plugins, next) { + function (plugins, next) { if (!Array.isArray(plugins)) { return next(); } - plugins = plugins.filter(function(plugin){ + plugins = plugins.filter(function (plugin){ return plugin && typeof plugin === 'string'; - }).map(function(plugin){ + }).map(function (plugin){ return path.join(__dirname, '../node_modules/', plugin); }); - async.filter(plugins, file.exists, function(plugins) { + async.filter(plugins, file.exists, function (plugins) { async.eachSeries(plugins, Plugins.loadPlugin, next); }); }, - function(next) { + function (next) { // If some plugins are incompatible, throw the warning here if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') { process.stdout.write('\n'); winston.warn('[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.'); - for(var x=0,numPlugins=Plugins.versionWarning.length;x<numPlugins;x++) { + for(var x = 0,numPlugins = Plugins.versionWarning.length;x < numPlugins;x++) { process.stdout.write(' * '.yellow + Plugins.versionWarning[x] + '\n'); } process.stdout.write('\n'); } - Object.keys(Plugins.loadedHooks).forEach(function(hook) { + Object.keys(Plugins.loadedHooks).forEach(function (hook) { var hooks = Plugins.loadedHooks[hook]; - hooks = hooks.sort(function(a, b) { + hooks = hooks.sort(function (a, b) { return a.priority - b.priority; }); }); @@ -132,15 +144,17 @@ var middleware; ], callback); }; - Plugins.reloadRoutes = function(callback) { - callback = callback || function() {}; + Plugins.reloadRoutes = function (callback) { + callback = callback || function () {}; var router = express.Router(); + router.hotswapId = 'plugins'; - router.render = function() { + router.render = function () { app.render.apply(app, arguments); }; - Plugins.fireHook('static:app.load', {app: app, router: router, middleware: middleware, controllers: controllers}, function(err) { + var controllers = require('./controllers'); + Plugins.fireHook('static:app.load', {app: app, router: router, middleware: middleware, controllers: controllers}, function (err) { if (err) { return winston.error('[plugins] Encountered error while executing post-router plugins hooks: ' + err.message); } @@ -151,38 +165,38 @@ var middleware; }); }; - Plugins.getTemplates = function(callback) { + Plugins.getTemplates = function (callback) { var templates = {}, tplName; async.waterfall([ async.apply(db.getSortedSetRange, 'plugins:active', 0, -1), - function(plugins, next) { + function (plugins, next) { var pluginBasePath = path.join(__dirname, '../node_modules'); - var paths = plugins.map(function(plugin) { + var paths = plugins.map(function (plugin) { return path.join(pluginBasePath, plugin); }); // Filter out plugins with invalid paths - async.filter(paths, file.exists, function(paths) { + async.filter(paths, file.exists, function (paths) { next(null, paths); }); }, - function(paths, next) { + function (paths, next) { async.map(paths, Plugins.loadPluginInfo, next); } - ], function(err, plugins) { + ], function (err, plugins) { if (err) { return callback(err); } - async.eachSeries(plugins, function(plugin, next) { + async.eachSeries(plugins, function (plugin, next) { if (plugin.templates || plugin.id.startsWith('nodebb-theme-')) { winston.verbose('[plugins] Loading templates (' + plugin.id + ')'); var templatesPath = path.join(__dirname, '../node_modules', plugin.id, plugin.templates || 'templates'); - utils.walk(templatesPath, function(err, pluginTemplates) { + utils.walk(templatesPath, function (err, pluginTemplates) { if (pluginTemplates) { - pluginTemplates.forEach(function(pluginTemplate) { + pluginTemplates.forEach(function (pluginTemplate) { if (pluginTemplate.endsWith('.tpl')) { tplName = "/" + pluginTemplate.replace(templatesPath, '').substring(1); @@ -196,7 +210,11 @@ var middleware; } }); } else { - winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.'); + if (err) { + winston.error(err); + } else { + winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.'); + } } next(false); @@ -204,24 +222,24 @@ var middleware; } else { next(false); } - }, function(err) { + }, function (err) { callback(err, templates); }); }); }; - Plugins.get = function(id, callback) { + Plugins.get = function (id, callback) { var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id; require('request')(url, { json: true - }, function(err, res, body) { + }, function (err, res, body) { if (res.statusCode === 404 || !body.payload) { return callback(err, {}); } - Plugins.normalise([body.payload], function(err, normalised) { - normalised = normalised.filter(function(plugin) { + Plugins.normalise([body.payload], function (err, normalised) { + normalised = normalised.filter(function (plugin) { return plugin.id === id; }); return callback(err, !err ? normalised[0] : undefined); @@ -229,7 +247,7 @@ var middleware; }); }; - Plugins.list = function(matching, callback) { + Plugins.list = function (matching, callback) { if (arguments.length === 1 && typeof matching === 'function') { callback = matching; matching = true; @@ -239,20 +257,21 @@ var middleware; require('request')(url, { json: true - }, function(err, res, body) { + }, function (err, res, body) { if (err) { winston.error('Error parsing plugins : ' + err.message); + return callback(err); } Plugins.normalise(body, callback); }); }; - Plugins.normalise = function(apiReturn, callback) { + Plugins.normalise = function (apiReturn, callback) { var pluginMap = {}; var dependencies = require.main.require('./package.json').dependencies; apiReturn = apiReturn || []; - for(var i=0; i<apiReturn.length; ++i) { + for(var i = 0; i < apiReturn.length; ++i) { apiReturn[i].id = apiReturn[i].name; apiReturn[i].installed = false; apiReturn[i].active = false; @@ -261,16 +280,16 @@ var middleware; pluginMap[apiReturn[i].name] = apiReturn[i]; } - Plugins.showInstalled(function(err, installedPlugins) { + Plugins.showInstalled(function (err, installedPlugins) { if (err) { return callback(err); } - installedPlugins = installedPlugins.filter(function(plugin) { + installedPlugins = installedPlugins.filter(function (plugin) { return plugin && !plugin.system; }); - async.each(installedPlugins, function(plugin, next) { + async.each(installedPlugins, function (plugin, next) { // If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff if (plugin.error) { pluginMap[plugin.id] = pluginMap[plugin.id] || {}; @@ -298,7 +317,7 @@ var middleware; } pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version); next(); - }, function(err) { + }, function (err) { if (err) { return callback(err); } @@ -311,7 +330,7 @@ var middleware; } } - pluginArray.sort(function(a, b) { + pluginArray.sort(function (a, b) { if (a.name > b.name ) { return 1; } else if (a.name < b.name ){ @@ -326,41 +345,41 @@ var middleware; }); }; - Plugins.showInstalled = function(callback) { + Plugins.showInstalled = function (callback) { var npmPluginPath = path.join(__dirname, '../node_modules'); async.waterfall([ async.apply(fs.readdir, npmPluginPath), - function(dirs, next) { - dirs = dirs.filter(function(dir){ + function (dirs, next) { + dirs = dirs.filter(function (dir){ return dir.startsWith('nodebb-plugin-') || dir.startsWith('nodebb-widget-') || dir.startsWith('nodebb-rewards-') || dir.startsWith('nodebb-theme-'); - }).map(function(dir){ + }).map(function (dir){ return path.join(npmPluginPath, dir); }); - async.filter(dirs, function(dir, callback){ - fs.stat(dir, function(err, stats){ + async.filter(dirs, function (dir, callback){ + fs.stat(dir, function (err, stats){ callback(!err && stats.isDirectory()); }); - }, function(plugins){ + }, function (plugins){ next(null, plugins); }); }, - function(files, next) { + function (files, next) { var plugins = []; - async.each(files, function(file, next) { + async.each(files, function (file, next) { async.waterfall([ - function(next) { + function (next) { Plugins.loadPluginInfo(file, next); }, - function(pluginData, next) { - Plugins.isActive(pluginData.name, function(err, active) { + function (pluginData, next) { + Plugins.isActive(pluginData.name, function (err, active) { if (err) { return next(new Error('no-active-state')); } @@ -373,7 +392,7 @@ var middleware; next(null, pluginData); }); } - ], function(err, pluginData) { + ], function (err, pluginData) { if (err) { return next(); // Silently fail } @@ -381,61 +400,11 @@ var middleware; plugins.push(pluginData); next(); }); - }, function(err) { - next(null, plugins); + }, function (err) { + next(err, plugins); }); } ], callback); }; - Plugins.clearRequireCache = function(next) { - var cached = Object.keys(require.cache); - async.waterfall([ - async.apply(async.map, Plugins.libraryPaths, fs.realpath), - function(paths, next) { - paths = paths.map(function(pluginLib) { - var parent = path.dirname(pluginLib); - return cached.filter(function(libPath) { - return libPath.indexOf(parent) !== -1; - }); - }).reduce(function(prev, cur) { - return prev.concat(cur); - }); - - Plugins.fireHook('filter:plugins.clearRequireCache', {paths: paths}, next); - }, - function(data, next) { - for (var x=0,numPaths=data.paths.length;x<numPaths;x++) { - delete require.cache[data.paths[x]]; - } - winston.verbose('[plugins] Plugin libraries removed from Node.js cache'); - - next(); - } - ], next); - }; - - function addLanguages(params, callback) { - Plugins.customLanguages.forEach(function(lang) { - params.router.get('/language' + lang.route, function(req, res, next) { - res.json(lang.file); - }); - - var components = lang.route.split('/'), - language = components[1], - filename = components[2].replace('.json', ''); - - translator.addTranslation(language, filename, lang.file); - }); - - for(var resource in Plugins.customLanguageFallbacks) { - params.router.get('/language/:lang/' + resource + '.json', function(req, res, next) { - winston.verbose('[translator] No resource file found for ' + req.params.lang + '/' + path.basename(req.path, '.json') + ', using provided fallback language file'); - res.sendFile(Plugins.customLanguageFallbacks[path.basename(req.path, '.json')]); - }); - } - - callback(null); - } - }(exports)); diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index 3330608154..9e08acde78 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -3,38 +3,48 @@ var winston = require('winston'), async = require('async'); -module.exports = function(Plugins) { +module.exports = function (Plugins) { Plugins.deprecatedHooks = { - 'filter:user.delete': 'static:user.delete', - 'filter:user.custom_fields': null, - 'action:user.loggedOut': 'static:user.loggedOut' + 'filter:user.custom_fields': null, // remove in v1.1.0 + 'filter:post.save': 'filter:post.create', + 'filter:user.profileLinks': 'filter:user.profileMenu' }; - /* `data` is an object consisting of (* is required): `data.hook`*, the name of the NodeBB hook `data.method`*, the method called in that plugin `data.priority`, the relative priority of the method when it is eventually called (default: 10) */ - Plugins.registerHook = function(id, data, callback) { + Plugins.registerHook = function (id, data, callback) { + callback = callback || function () {}; function register() { Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; Plugins.loadedHooks[data.hook].push(data); - if (typeof callback === 'function') { - callback(); - } + callback(); + } + + if (!data.hook) { + winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook', data); + return callback(); } var method; if (Object.keys(Plugins.deprecatedHooks).indexOf(data.hook) !== -1) { - winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' + + winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' + (Plugins.deprecatedHooks[data.hook] ? 'please use `' + Plugins.deprecatedHooks[data.hook] + '` instead.' : 'there is no alternative.' ) ); + } else { + // handle hook's startsWith, i.e. action:homepage.get + var parts = data.hook.split(':'); + if (parts.length > 2) { + parts.pop(); + } + var hook = parts.join(':'); } if (data.hook && data.method) { @@ -44,7 +54,7 @@ module.exports = function(Plugins) { } if (typeof data.method === 'string' && data.method.length > 0) { - method = data.method.split('.').reduce(function(memo, prop) { + method = data.method.split('.').reduce(function (memo, prop) { if (memo && memo[prop]) { return memo[prop]; } else { @@ -61,12 +71,13 @@ module.exports = function(Plugins) { register(); } else { winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method); + return callback(); } } }; - Plugins.fireHook = function(hook, params, callback) { - callback = typeof callback === 'function' ? callback : function() {}; + Plugins.fireHook = function (hook, params, callback) { + callback = typeof callback === 'function' ? callback : function () {}; var hookList = Plugins.loadedHooks[hook]; var hookType = hook.split(':')[0]; @@ -92,7 +103,7 @@ module.exports = function(Plugins) { return callback(null, params); } - async.reduce(hookList, params, function(params, hookObj, next) { + async.reduce(hookList, params, function (params, hookObj, next) { if (typeof hookObj.method !== 'function') { if (global.env === 'development') { winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); @@ -102,7 +113,7 @@ module.exports = function(Plugins) { hookObj.method(params, next); - }, function(err, values) { + }, function (err, values) { if (err) { winston.error('[plugins] ' + hook + ', ' + err.message); } @@ -115,7 +126,7 @@ module.exports = function(Plugins) { if (!Array.isArray(hookList) || !hookList.length) { return callback(); } - async.each(hookList, function(hookObj, next) { + async.each(hookList, function (hookObj, next) { if (typeof hookObj.method !== 'function') { if (global.env === 'development') { @@ -133,18 +144,18 @@ module.exports = function(Plugins) { if (!Array.isArray(hookList) || !hookList.length) { return callback(); } - async.each(hookList, function(hookObj, next) { + async.each(hookList, function (hookObj, next) { if (typeof hookObj.method === 'function') { var timedOut = false; - var timeoutId = setTimeout(function() { + var timeoutId = setTimeout(function () { winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); timedOut = true; next(); }, 5000); try { - hookObj.method(params, function() { + hookObj.method(params, function () { clearTimeout(timeoutId); if (!timedOut) { next.apply(null, arguments); @@ -162,7 +173,7 @@ module.exports = function(Plugins) { }, callback); } - Plugins.hasListeners = function(hook) { + Plugins.hasListeners = function (hook) { return !!(Plugins.loadedHooks[hook] && Plugins.loadedHooks[hook].length > 0); }; -}; \ No newline at end of file +}; diff --git a/src/plugins/install.js b/src/plugins/install.js index 1ea826784c..acde7a22e7 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -12,35 +12,35 @@ var winston = require('winston'), pubsub = require('../pubsub'); -module.exports = function(Plugins) { +module.exports = function (Plugins) { if (nconf.get('isPrimary') === 'true') { - pubsub.on('plugins:toggleInstall', function(data) { + pubsub.on('plugins:toggleInstall', function (data) { if (data.hostname !== os.hostname()) { toggleInstall(data.id, data.version); } }); - pubsub.on('plugins:upgrade', function(data) { + pubsub.on('plugins:upgrade', function (data) { if (data.hostname !== os.hostname()) { upgrade(data.id, data.version); } }); } - Plugins.toggleActive = function(id, callback) { - callback = callback || function() {}; + Plugins.toggleActive = function (id, callback) { + callback = callback || function () {}; var isActive; async.waterfall([ - function(next) { + function (next) { Plugins.isActive(id, next); }, - function(_isActive, next) { + function (_isActive, next) { isActive = _isActive; if (isActive) { db.sortedSetRemove('plugins:active', id, next); } else { - db.sortedSetCard('plugins:active', function(err, count) { + db.sortedSetCard('plugins:active', function (err, count) { if (err) { return next(err); } @@ -48,12 +48,12 @@ module.exports = function(Plugins) { }); } }, - function(next) { + function (next) { meta.reloadRequired = true; Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id); next(); } - ], function(err) { + ], function (err) { if (err) { winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\''); return callback(err); @@ -62,40 +62,44 @@ module.exports = function(Plugins) { }); }; - Plugins.toggleInstall = function(id, version, callback) { + Plugins.toggleInstall = function (id, version, callback) { pubsub.publish('plugins:toggleInstall', {hostname: os.hostname(), id: id, version: version}); toggleInstall(id, version, callback); }; function toggleInstall(id, version, callback) { - Plugins.isInstalled(id, function(err, installed) { + Plugins.isInstalled(id, function (err, installed) { if (err) { return callback(err); } var type = installed ? 'uninstall' : 'install'; async.waterfall([ - function(next) { + function (next) { Plugins.isActive(id, next); }, - function(active, next) { + function (active, next) { if (active) { - Plugins.toggleActive(id, function(err, status) { + Plugins.toggleActive(id, function (err, status) { next(err); }); return; } next(); }, - function(next) { + function (next) { var command = installed ? ('npm uninstall ' + id) : ('npm install ' + id + '@' + (version || 'latest')); runNpmCommand(command, next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } - Plugins.get(id, function(err, pluginData) { + Plugins.get(id, function (err, pluginData) { + if (err) { + return callback(err); + } + Plugins.fireHook('action:plugin.' + type, id); callback(null, pluginData); }); @@ -113,39 +117,39 @@ module.exports = function(Plugins) { }); } - Plugins.upgrade = function(id, version, callback) { + Plugins.upgrade = function (id, version, callback) { pubsub.publish('plugins:upgrade', {hostname: os.hostname(), id: id, version: version}); upgrade(id, version, callback); }; function upgrade(id, version, callback) { async.waterfall([ - function(next) { + function (next) { runNpmCommand('npm install ' + id + '@' + (version || 'latest'), next); }, - function(next) { + function (next) { Plugins.isActive(id, next); }, - function(isActive, next) { + function (isActive, next) { meta.reloadRequired = isActive; next(null, isActive); } ], callback); } - Plugins.isInstalled = function(id, callback) { + Plugins.isInstalled = function (id, callback) { var pluginDir = path.join(__dirname, '../../node_modules', id); - fs.stat(pluginDir, function(err, stats) { + fs.stat(pluginDir, function (err, stats) { callback(null, err ? false : stats.isDirectory()); }); }; - Plugins.isActive = function(id, callback) { + Plugins.isActive = function (id, callback) { db.isSortedSetMember('plugins:active', id, callback); }; - Plugins.getActive = function(callback) { + Plugins.getActive = function (callback) { db.getSortedSetRange('plugins:active', 0, -1, callback); }; }; \ No newline at end of file diff --git a/src/plugins/load.js b/src/plugins/load.js index cf1ff27d9a..836e0d2c4a 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -7,14 +7,16 @@ var fs = require('fs'), winston = require('winston'), nconf = require('nconf'), _ = require('underscore'), - file = require('../file'), - utils = require('../../public/src/utils'); + file = require('../file'); +var utils = require('../../public/src/utils'), + meta = require('../meta'); -module.exports = function(Plugins) { - Plugins.loadPlugin = function(pluginPath, callback) { - Plugins.loadPluginInfo(pluginPath, function(err, pluginData) { +module.exports = function (Plugins) { + + Plugins.loadPlugin = function (pluginPath, callback) { + Plugins.loadPluginInfo(pluginPath, function (err, pluginData) { if (err) { if (err.message === '[[error:parse-error]]') { return callback(); @@ -25,25 +27,28 @@ module.exports = function(Plugins) { checkVersion(pluginData); async.parallel([ - function(next) { + function (next) { registerHooks(pluginData, pluginPath, next); }, - function(next) { + function (next) { mapStaticDirectories(pluginData, pluginPath, next); }, - function(next) { + function (next) { mapFiles(pluginData, 'css', 'cssFiles', next); }, - function(next) { + function (next) { mapFiles(pluginData, 'less', 'lessFiles', next); }, - function(next) { + function (next) { mapClientSideScripts(pluginData, next); }, - function(next) { + function (next) { + mapClientModules(pluginData, next); + }, + function (next) { loadLanguages(pluginData, next); } - ], function(err) { + ], function (err) { if (err) { winston.verbose('[plugins] Could not load plugin : ' + pluginData.id); return callback(err); @@ -84,7 +89,7 @@ module.exports = function(Plugins) { } if (Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { - async.each(pluginData.hooks, function(hook, next) { + async.each(pluginData.hooks, function (hook, next) { Plugins.registerHook(pluginData.id, hook, next); }, callback); } else { @@ -109,7 +114,7 @@ module.exports = function(Plugins) { var realPath = pluginData.staticDirs[mappedPath]; var staticDir = path.join(pluginPath, realPath); - file.exists(staticDir, function(exists) { + file.exists(staticDir, function (exists) { if (exists) { Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir; } else { @@ -134,7 +139,7 @@ module.exports = function(Plugins) { winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id); } - Plugins[globalArray] = Plugins[globalArray].concat(pluginData[type].map(function(file) { + Plugins[globalArray] = Plugins[globalArray].concat(pluginData[type].map(function (file) { return path.join(pluginData.id, file); })); } @@ -147,19 +152,61 @@ module.exports = function(Plugins) { winston.verbose('[plugins] Found ' + pluginData.scripts.length + ' js file(s) for plugin ' + pluginData.id); } - Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function(file) { - return path.join(__dirname, '../../node_modules/', pluginData.id, file); - })); + Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function (file) { + return resolveModulePath(path.join(__dirname, '../../node_modules/', pluginData.id, file), file); + })).filter(Boolean); } if (Array.isArray(pluginData.acpScripts)) { if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData.acpScripts.length + ' js file(s) for plugin ' + pluginData.id); + winston.verbose('[plugins] Found ' + pluginData.acpScripts.length + ' ACP js file(s) for plugin ' + pluginData.id); } - Plugins.acpScripts = Plugins.acpScripts.concat(pluginData.acpScripts.map(function(file) { - return path.join(__dirname, '../../node_modules/', pluginData.id, file); - })); + Plugins.acpScripts = Plugins.acpScripts.concat(pluginData.acpScripts.map(function (file) { + return resolveModulePath(path.join(__dirname, '../../node_modules/', pluginData.id, file), file); + })).filter(Boolean); + } + + callback(); + } + + function mapClientModules(pluginData, callback) { + if (!pluginData.hasOwnProperty('modules')) { + return callback(); + } + + var modules = {}; + + if (Array.isArray(pluginData.modules)) { + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + pluginData.modules.length + ' AMD-style module(s) for plugin ' + pluginData.id); + } + + var strip = pluginData.hasOwnProperty('modulesStrip') ? parseInt(pluginData.modulesStrip, 10) : 0; + + pluginData.modules.forEach(function (file) { + if (strip) { + modules[file.replace(new RegExp('\.?(\/[^\/]+){' + strip + '}\/'), '')] = path.join('./node_modules/', pluginData.id, file); + } else { + modules[path.basename(file)] = path.join('./node_modules/', pluginData.id, file); + } + }); + + meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules); + } else { + var keys = Object.keys(pluginData.modules); + + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + keys.length + ' AMD-style module(s) for plugin ' + pluginData.id); + } + + for (var name in pluginData.modules) { + if (pluginData.modules.hasOwnProperty(name)) { + modules[name] = path.join('./node_modules/', pluginData.id, pluginData.modules[name]); + } + } + + meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules); } callback(); @@ -173,56 +220,87 @@ module.exports = function(Plugins) { var pathToFolder = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages), fallbackMap = {}; - utils.walk(pathToFolder, function(err, languages) { - var arr = []; + utils.walk(pathToFolder, function (err, languages) { + if (err) { + return callback(err); + } - async.each(languages, function(pathToLang, next) { - fs.readFile(pathToLang, function(err, file) { + async.each(languages, function (pathToLang, next) { + fs.readFile(pathToLang, function (err, file) { if (err) { return next(err); } - var json; + var data; + var route = pathToLang.replace(pathToFolder + '/', ''); try { - json = JSON.parse(file.toString()); + data = JSON.parse(file.toString()); } catch (err) { winston.error('[plugins] Unable to parse custom language file: ' + pathToLang + '\r\n' + err.stack); return next(err); } - arr.push({ - file: json, - route: pathToLang.replace(pathToFolder, '') - }); + Plugins.customLanguages[route] = Plugins.customLanguages[route] || {}; + _.extendOwn(Plugins.customLanguages[route], data); if (pluginData.defaultLang && pathToLang.endsWith(pluginData.defaultLang + '/' + path.basename(pathToLang))) { - fallbackMap[path.basename(pathToLang, '.json')] = path.join(pathToFolder, pluginData.defaultLang, path.basename(pathToLang)); + Plugins.languageCodes.map(function (code) { + if (pluginData.defaultLang !== code) { + return code + '/' + path.basename(pathToLang); + } else { + return null; + } + }).filter(Boolean).forEach(function (key) { + Plugins.customLanguages[key] = _.defaults(Plugins.customLanguages[key] || {}, data); + }); } next(); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } - Plugins.customLanguages = Plugins.customLanguages.concat(arr); - _.extendOwn(Plugins.customLanguageFallbacks, fallbackMap); - callback(); }); }); } - Plugins.loadPluginInfo = function(pluginPath, callback) { + function resolveModulePath(fullPath, relPath) { + /** + * With npm@3, dependencies can become flattened, and appear at the root level. + * This method resolves these differences if it can. + */ + var matches = fullPath.match(/node_modules/g); + var atRootLevel = !matches || matches.length === 1; + + try { + fs.statSync(fullPath); + winston.verbose('[plugins/load] File found: ' + fullPath); + return fullPath; + } catch (e) { + // File not visible to the calling process, ascend to root level if possible and try again + if (!atRootLevel && relPath) { + winston.verbose('[plugins/load] File not found: ' + fullPath + ' (Ascending)'); + return resolveModulePath(path.join(__dirname, '../..', relPath)); + } else { + // Already at root level, file was simply not found + winston.warn('[plugins/load] File not found: ' + fullPath + ' (Ignoring)'); + return null; + } + } + } + + Plugins.loadPluginInfo = function (pluginPath, callback) { async.parallel({ - package: function(next) { + package: function (next) { fs.readFile(path.join(pluginPath, 'package.json'), next); }, - plugin: function(next) { + plugin: function (next) { fs.readFile(path.join(pluginPath, 'plugin.json'), next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -240,7 +318,7 @@ module.exports = function(Plugins) { callback(null, pluginData); } catch(err) { var pluginDir = pluginPath.split(path.sep); - pluginDir = pluginDir[pluginDir.length -1]; + pluginDir = pluginDir[pluginDir.length - 1]; winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message); @@ -248,4 +326,4 @@ module.exports = function(Plugins) { } }); }; -}; \ No newline at end of file +}; diff --git a/src/posts.js b/src/posts.js index a3d19e7a7f..975846859b 100644 --- a/src/posts.js +++ b/src/posts.js @@ -1,16 +1,16 @@ 'use strict'; -var async = require('async'), - _ = require('underscore'), +var async = require('async'); +var _ = require('underscore'); - db = require('./database'), - utils = require('../public/src/utils'), - user = require('./user'), - topics = require('./topics'), - privileges = require('./privileges'), - plugins = require('./plugins'); +var db = require('./database'); +var utils = require('../public/src/utils'); +var user = require('./user'); +var topics = require('./topics'); +var privileges = require('./privileges'); +var plugins = require('./plugins'); -(function(Posts) { +(function (Posts) { require('./posts/create')(Posts); require('./posts/delete')(Posts); @@ -23,48 +23,52 @@ var async = require('async'), require('./posts/recent')(Posts); require('./posts/flags')(Posts); require('./posts/tools')(Posts); + require('./posts/votes')(Posts); + require('./posts/bookmarks')(Posts); - Posts.exists = function(pid, callback) { + Posts.exists = function (pid, callback) { db.isSortedSetMember('posts:pid', pid, callback); }; - Posts.getPidsFromSet = function(set, start, stop, reverse, callback) { + Posts.getPidsFromSet = function (set, start, stop, reverse, callback) { if (isNaN(start) || isNaN(stop)) { return callback(null, []); } db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback); }; - Posts.getPostsByPids = function(pids, uid, callback) { + Posts.getPostsByPids = function (pids, uid, callback) { if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } var keys = []; - for (var x=0, numPids=pids.length; x<numPids; ++x) { + for (var x = 0, numPids = pids.length; x < numPids; ++x) { keys.push('post:' + pids[x]); } async.waterfall([ - function(next) { + function (next) { db.getObjects(keys, next); }, - function(posts, next) { - async.map(posts, function(post, next) { + function (posts, next) { + async.map(posts, function (post, next) { if (!post) { return next(); } - + post.upvotes = parseInt(post.upvotes, 10) || 0; + post.downvotes = parseInt(post.downvotes, 10) || 0; + post.votes = post.upvotes - post.downvotes; post.timestampISO = utils.toISOString(post.timestamp); post.editedISO = parseInt(post.edited, 10) !== 0 ? utils.toISOString(post.edited) : ''; Posts.parsePost(post, next); }, next); }, - function(posts, next) { + function (posts, next) { plugins.fireHook('filter:post.getPosts', {posts: posts, uid: uid}, next); }, - function(data, next) { + function (data, next) { if (!data || !Array.isArray(data.posts)) { return next(null, []); } @@ -74,25 +78,25 @@ var async = require('async'), ], callback); }; - Posts.getPostSummariesFromSet = function(set, uid, start, stop, callback) { + Posts.getPostSummariesFromSet = function (set, uid, start, stop, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange(set, start, stop, next); }, - function(pids, next) { + function (pids, next) { privileges.posts.filter('read', pids, uid, next); }, - function(pids, next) { + function (pids, next) { Posts.getPostSummaryByPids(pids, uid, {stripTags: false}, next); }, - function(posts, next) { + function (posts, next) { next(null, {posts: posts, nextStart: stop + 1}); } ], callback); }; - Posts.getPostData = function(pid, callback) { - db.getObject('post:' + pid, function(err, data) { + Posts.getPostData = function (pid, callback) { + db.getObject('post:' + pid, function (err, data) { if (err) { return callback(err); } @@ -101,8 +105,8 @@ var async = require('async'), }); }; - Posts.getPostField = function(pid, field, callback) { - Posts.getPostFields(pid, [field], function(err, data) { + Posts.getPostField = function (pid, field, callback) { + Posts.getPostFields(pid, [field], function (err, data) { if (err) { return callback(err); } @@ -111,41 +115,41 @@ var async = require('async'), }); }; - Posts.getPostFields = function(pid, fields, callback) { - db.getObjectFields('post:' + pid, fields, function(err, data) { + Posts.getPostFields = function (pid, fields, callback) { + db.getObjectFields('post:' + pid, fields, function (err, data) { if (err) { return callback(err); } data.pid = pid; - plugins.fireHook('filter:post.getFields', {posts: [data], fields: fields}, function(err, data) { + plugins.fireHook('filter:post.getFields', {posts: [data], fields: fields}, function (err, data) { callback(err, (data && Array.isArray(data.posts) && data.posts.length) ? data.posts[0] : null); }); }); }; - Posts.getPostsFields = function(pids, fields, callback) { + Posts.getPostsFields = function (pids, fields, callback) { if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } - var keys = pids.map(function(pid) { + var keys = pids.map(function (pid) { return 'post:' + pid; }); - db.getObjectsFields(keys, fields, function(err, posts) { + db.getObjectsFields(keys, fields, function (err, posts) { if (err) { return callback(err); } - plugins.fireHook('filter:post.getFields', {posts: posts, fields: fields}, function(err, data) { + plugins.fireHook('filter:post.getFields', {posts: posts, fields: fields}, function (err, data) { callback(err, (data && Array.isArray(data.posts)) ? data.posts : null); }); }); }; - Posts.setPostField = function(pid, field, value, callback) { - db.setObjectField('post:' + pid, field, value, function(err) { + Posts.setPostField = function (pid, field, value, callback) { + db.setObjectField('post:' + pid, field, value, function (err) { if (err) { return callback(err); } @@ -158,8 +162,8 @@ var async = require('async'), }); }; - Posts.setPostFields = function(pid, data, callback) { - db.setObject('post:' + pid, data, function(err) { + Posts.setPostFields = function (pid, data, callback) { + db.setObject('post:' + pid, data, function (err) { if (err) { return callback(err); } @@ -169,9 +173,9 @@ var async = require('async'), }); }; - Posts.getPidIndex = function(pid, tid, topicPostSort, callback) { + Posts.getPidIndex = function (pid, tid, topicPostSort, callback) { var set = topicPostSort === 'most_votes' ? 'tid:' + tid + ':posts:votes' : 'tid:' + tid + ':posts'; - db.sortedSetRank(set, pid, function(err, index) { + db.sortedSetRank(set, pid, function (err, index) { if (!utils.isNumber(index)) { return callback(err, 0); } @@ -179,18 +183,18 @@ var async = require('async'), }); }; - Posts.getPostIndices = function(posts, uid, callback) { + Posts.getPostIndices = function (posts, uid, callback) { if (!Array.isArray(posts) || !posts.length) { return callback(null, []); } - user.getSettings(uid, function(err, settings) { + user.getSettings(uid, function (err, settings) { if (err) { return callback(err); } var byVotes = settings.topicPostSort === 'most_votes'; - var sets = posts.map(function(post) { + var sets = posts.map(function (post) { return byVotes ? 'tid:' + post.tid + ':posts:votes' : 'tid:' + post.tid + ':posts'; }); @@ -201,16 +205,16 @@ var async = require('async'), sets = uniqueSets[0]; } - var pids = posts.map(function(post) { + var pids = posts.map(function (post) { return post.pid; }); - db[method](sets, pids, function(err, indices) { + db[method](sets, pids, function (err, indices) { if (err) { return callback(err); } - for (var i=0; i<indices.length; ++i) { + for (var i = 0; i < indices.length; ++i) { indices[i] = utils.isNumber(indices[i]) ? parseInt(indices[i], 10) + 1 : 0; } @@ -219,14 +223,18 @@ var async = require('async'), }); }; - Posts.updatePostVoteCount = function(postData, voteCount, callback) { + Posts.updatePostVoteCount = function (postData, callback) { if (!postData || !postData.pid || !postData.tid) { return callback(); } async.parallel([ function (next) { if (postData.uid) { - db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', voteCount, postData.pid, next); + if (postData.votes > 0) { + db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next); + } else { + db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next); + } } else { next(); } @@ -240,14 +248,14 @@ var async = require('async'), if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) { return next(); } - db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', voteCount, postData.pid, next); + db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next); } ], next); }, function (next) { - Posts.setPostField(postData.pid, 'votes', voteCount, next); + Posts.setPostFields(postData.pid, {upvotes: postData.upvotes, downvotes: postData.downvotes}, next); } - ], function(err) { + ], function (err) { callback(err); }); }; diff --git a/src/posts/bookmarks.js b/src/posts/bookmarks.js new file mode 100644 index 0000000000..b60da91c77 --- /dev/null +++ b/src/posts/bookmarks.js @@ -0,0 +1,107 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var plugins = require('../plugins'); + +module.exports = function (Posts) { + + Posts.bookmark = function (pid, uid, callback) { + toggleBookmark('bookmark', pid, uid, callback); + }; + + Posts.unbookmark = function (pid, uid, callback) { + toggleBookmark('unbookmark', pid, uid, callback); + }; + + function toggleBookmark(type, pid, uid, callback) { + if (!parseInt(uid, 10)) { + return callback(new Error('[[error:not-logged-in]]')); + } + var isBookmarking = type === 'bookmark'; + + async.parallel({ + owner: function (next) { + Posts.getPostField(pid, 'uid', next); + }, + postData: function (next) { + Posts.getPostFields(pid, ['pid', 'uid'], next); + }, + hasBookmarked: function (next) { + Posts.hasBookmarked(pid, uid, next); + } + }, function (err, results) { + if (err) { + return callback(err); + } + + if (isBookmarking && results.hasBookmarked) { + return callback(new Error('[[error:already-bookmarked]]')); + } + + if (!isBookmarking && !results.hasBookmarked) { + return callback(new Error('[[error:already-unbookmarked]]')); + } + + async.waterfall([ + function (next) { + if (isBookmarking) { + db.sortedSetAdd('uid:' + uid + ':bookmarks', Date.now(), pid, next); + } else { + db.sortedSetRemove('uid:' + uid + ':bookmarks', pid, next); + } + }, + function (next) { + db[isBookmarking ? 'setAdd' : 'setRemove']('pid:' + pid + ':users_bookmarked', uid, next); + }, + function (next) { + db.setCount('pid:' + pid + ':users_bookmarked', next); + }, + function (count, next) { + results.postData.bookmarks = count; + Posts.setPostField(pid, 'bookmarks', count, next); + } + ], function (err) { + if (err) { + return callback(err); + } + + var current = results.hasBookmarked ? 'bookmarked' : 'unbookmarked'; + + plugins.fireHook('action:post.' + type, { + pid: pid, + uid: uid, + owner: results.owner, + current: current + }); + + callback(null, { + post: results.postData, + isBookmarked: isBookmarking + }); + }); + }); + } + + Posts.hasBookmarked = function (pid, uid, callback) { + if (!parseInt(uid, 10)) { + if (Array.isArray(pid)) { + callback(null, pid.map(function () { return false; })); + } else { + callback(null, false); + } + return; + } + + if (Array.isArray(pid)) { + var sets = pid.map(function (pid) { + return 'pid:' + pid + ':users_bookmarked'; + }); + + db.isMemberOfSets(sets, uid, callback); + } else { + db.isSetMember('pid:' + pid + ':users_bookmarked', uid, callback); + } + }; +}; diff --git a/src/posts/cache.js b/src/posts/cache.js index 592d313f48..62277c46e3 100644 --- a/src/posts/cache.js +++ b/src/posts/cache.js @@ -1,11 +1,10 @@ var LRU = require('lru-cache'); +var meta = require('../meta'); var cache = LRU({ - max: 1048576, + max: parseInt(meta.config.postCacheSize, 10) || 1048576, length: function (n) { return n.length; }, maxAge: 1000 * 60 * 60 }); - - module.exports = cache; \ No newline at end of file diff --git a/src/posts/category.js b/src/posts/category.js index 68ae42474b..a1f3e8afbc 100644 --- a/src/posts/category.js +++ b/src/posts/category.js @@ -1,47 +1,50 @@ 'use strict'; -var async = require('async'), - topics = require('../topics'); +var async = require('async'); +var _ = require('underscore'); -module.exports = function(Posts) { +var db = require('../database'); +var topics = require('../topics'); - Posts.getCidByPid = function(pid, callback) { +module.exports = function (Posts) { + + Posts.getCidByPid = function (pid, callback) { async.waterfall([ - function(next) { + function (next) { Posts.getPostField(pid, 'tid', next); }, - function(tid, next) { + function (tid, next) { topics.getTopicField(tid, 'cid', next); - } + } ], callback); }; - Posts.getCidsByPids = function(pids, callback) { - Posts.getPostsFields(pids, ['tid'], function(err, posts) { + Posts.getCidsByPids = function (pids, callback) { + Posts.getPostsFields(pids, ['tid'], function (err, posts) { if (err) { return callback(err); } - var tids = posts.map(function(post) { + var tids = posts.map(function (post) { return post.tid; - }).filter(function(tid, index, array) { + }).filter(function (tid, index, array) { return tid && array.indexOf(tid) === index; }); - topics.getTopicsFields(tids, ['cid'], function(err, topics) { + topics.getTopicsFields(tids, ['cid'], function (err, topics) { if (err) { return callback(err); } var map = {}; - topics.forEach(function(topic, index) { + topics.forEach(function (topic, index) { if (topic) { map[tids[index]] = topic.cid; } }); - var cids = posts.map(function(post) { + var cids = posts.map(function (post) { return map[post.tid]; }); @@ -49,4 +52,34 @@ module.exports = function(Posts) { }); }); }; + + Posts.filterPidsByCid = function (pids, cid, callback) { + if (!cid) { + return callback(null, pids); + } + + if (!Array.isArray(cid) || cid.length === 1) { + // Single cid + db.isSortedSetMembers('cid:' + parseInt(cid, 10) + ':pids', pids, function (err, isMembers) { + if (err) { + return callback(err); + } + pids = pids.filter(function (pid, index) { + return pid && isMembers[index]; + }); + callback(null, pids); + }); + } else { + // Multiple cids + async.map(cid, function (cid, next) { + Posts.filterPidsByCid(pids, cid, next); + }, function (err, pidsArr) { + if (err) { + return callback(err); + } + + callback(null, _.union.apply(_, pidsArr)); + }); + } + }; }; \ No newline at end of file diff --git a/src/posts/create.js b/src/posts/create.js index 041d662915..1054c6d694 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -1,23 +1,24 @@ 'use strict'; -var async = require('async'), - _ = require('underscore'), +var async = require('async'); +var _ = require('underscore'); - meta = require('../meta'), - db = require('../database'), - plugins = require('../plugins'), - user = require('../user'), - topics = require('../topics'), - categories = require('../categories'); +var meta = require('../meta'); +var db = require('../database'); +var plugins = require('../plugins'); +var user = require('../user'); +var topics = require('../topics'); +var categories = require('../categories'); -module.exports = function(Posts) { - Posts.create = function(data, callback) { +module.exports = function (Posts) { + + Posts.create = function (data, callback) { // This is an internal method, consider using Topics.reply instead - var uid = data.uid, - tid = data.tid, - content = data.content.toString(), - timestamp = data.timestamp || Date.now(); + var uid = data.uid; + var tid = data.tid; + var content = data.content.toString(); + var timestamp = data.timestamp || Date.now(); if (!uid && parseInt(uid, 10) !== 0) { return callback(new Error('[[error:invalid-uid]]')); @@ -26,10 +27,10 @@ module.exports = function(Posts) { var postData; async.waterfall([ - function(next) { + function (next) { db.incrObjectField('global', 'nextPid', next); }, - function(pid, next) { + function (pid, next) { postData = { 'pid': pid, @@ -37,10 +38,6 @@ module.exports = function(Posts) { 'tid': tid, 'content': content, 'timestamp': timestamp, - 'reputation': 0, - 'votes': 0, - 'editor': '', - 'edited': 0, 'deleted': 0 }; @@ -52,25 +49,29 @@ module.exports = function(Posts) { postData.ip = data.ip; } - if (parseInt(uid, 10) === 0 && data.handle) { + if (data.handle && !parseInt(uid, 10)) { postData.handle = data.handle; } plugins.fireHook('filter:post.save', postData, next); }, - function(postData, next) { + function (postData, next) { + plugins.fireHook('filter:post.create', {post: postData, data: data}, next); + }, + function (data, next) { + postData = data.post; db.setObject('post:' + postData.pid, postData, next); }, - function(next) { + function (next) { async.parallel([ - function(next) { + function (next) { user.onNewPostMade(postData, next); }, - function(next) { + function (next) { topics.onNewPostMade(postData, next); }, - function(next) { - topics.getTopicFields(tid, ['cid', 'pinned'], function(err, topicData) { + function (next) { + topics.getTopicFields(tid, ['cid', 'pinned'], function (err, topicData) { if (err) { return next(err); } @@ -78,20 +79,20 @@ module.exports = function(Posts) { categories.onNewPostMade(topicData.cid, topicData.pinned, postData, next); }); }, - function(next) { + function (next) { db.sortedSetAdd('posts:pid', timestamp, postData.pid, next); }, - function(next) { + function (next) { db.incrObjectField('global', 'postCount', next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } plugins.fireHook('filter:post.get', postData, next); }); }, - function(postData, next) { + function (postData, next) { plugins.fireHook('action:post.save', _.clone(postData)); next(null, postData); } diff --git a/src/posts/delete.js b/src/posts/delete.js index a77c153cb2..bdaae4a70c 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -8,9 +8,9 @@ var topics = require('../topics'); var user = require('../user'); var plugins = require('../plugins'); -module.exports = function(Posts) { +module.exports = function (Posts) { - Posts.delete = function(pid, uid, callback) { + Posts.delete = function (pid, uid, callback) { var postData; async.waterfall([ function (next) { @@ -24,31 +24,29 @@ module.exports = function(Posts) { }, function (_post, next) { postData = _post; - topics.getTopicField(_post.tid, 'cid', next); + topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next); }, - function (cid, next) { + function (topicData, next) { async.parallel([ - function(next) { - updateTopicTimestamp(postData.tid, next); - }, - function(next) { - db.sortedSetRemove('cid:' + cid + ':pids', pid, next); + function (next) { + updateTopicTimestamp(topicData, next); }, - function(next) { - Posts.dismissFlag(pid, next); + function (next) { + db.sortedSetRemove('cid:' + topicData.cid + ':pids', pid, next); }, - function(next) { + function (next) { topics.updateTeaser(postData.tid, next); } - ], function(err) { - plugins.fireHook('action:post.delete', pid); - next(err, postData); - }); + ], next); + }, + function (results, next) { + plugins.fireHook('action:post.delete', pid); + next(null, postData); } ], callback); }; - Posts.restore = function(pid, uid, callback) { + Posts.restore = function (pid, uid, callback) { var postData; async.waterfall([ function (next) { @@ -62,48 +60,59 @@ module.exports = function(Posts) { }, function (_post, next) { postData = _post; - topics.getTopicField(_post.tid, 'cid', next); + topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next); }, - function (cid, next) { - postData.cid = cid; + function (topicData, next) { + postData.cid = topicData.cid; async.parallel([ - function(next) { - updateTopicTimestamp(postData.tid, next); + function (next) { + updateTopicTimestamp(topicData, next); }, - function(next) { - db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, pid, next); + function (next) { + db.sortedSetAdd('cid:' + topicData.cid + ':pids', postData.timestamp, pid, next); }, - function(next) { + function (next) { topics.updateTeaser(postData.tid, next); } - ], function(err) { - plugins.fireHook('action:post.restore', _.clone(postData)); - next(err, postData); - }); + ], next); + }, + function (results, next) { + plugins.fireHook('action:post.restore', _.clone(postData)); + next(null, postData); } ], callback); }; - function updateTopicTimestamp(tid, callback) { - topics.getLatestUndeletedPid(tid, function(err, pid) { - if(err || !pid) { - return callback(err); - } - - Posts.getPostField(pid, 'timestamp', function(err, timestamp) { - if (err) { - return callback(err); + function updateTopicTimestamp(topicData, callback) { + var timestamp; + async.waterfall([ + function (next) { + topics.getLatestUndeletedPid(topicData.tid, next); + }, + function (pid, next) { + if (!parseInt(pid, 10)) { + return callback(); } - - if (timestamp) { - return topics.updateTimestamp(tid, timestamp, callback); + Posts.getPostField(pid, 'timestamp', next); + }, + function (_timestamp, next) { + timestamp = _timestamp; + if (!parseInt(timestamp, 10)) { + return callback(); } - callback(); - }); - }); + topics.updateTimestamp(topicData.tid, timestamp, next); + }, + function (next) { + if (parseInt(topicData.pinned, 10) !== 1) { + db.sortedSetAdd('cid:' + topicData.cid + ':tids', timestamp, topicData.tid, next); + } else { + next(); + } + } + ], callback); } - Posts.purge = function(pid, uid, callback) { + Posts.purge = function (pid, uid, callback) { async.waterfall([ function (next) { Posts.exists(pid, next); @@ -123,7 +132,7 @@ module.exports = function(Posts) { deletePostFromCategoryRecentPosts(pid, next); }, function (next) { - deletePostFromUsersFavourites(pid, next); + deletePostFromUsersBookmarks(pid, next); }, function (next) { deletePostFromUsersVotes(pid, next); @@ -134,7 +143,7 @@ module.exports = function(Posts) { function (next) { Posts.dismissFlag(pid, next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -146,7 +155,7 @@ module.exports = function(Posts) { }; function deletePostFromTopicAndUser(pid, callback) { - Posts.getPostFields(pid, ['tid', 'uid'], function(err, postData) { + Posts.getPostFields(pid, ['tid', 'uid'], function (err, postData) { if (err) { return callback(err); } @@ -155,12 +164,12 @@ module.exports = function(Posts) { 'tid:' + postData.tid + ':posts', 'tid:' + postData.tid + ':posts:votes', 'uid:' + postData.uid + ':posts' - ], pid, function(err) { + ], pid, function (err) { if (err) { return callback(err); } - topics.getTopicFields(postData.tid, ['cid'], function(err, topicData) { + topics.getTopicFields(postData.tid, ['tid', 'cid', 'pinned'], function (err, topicData) { if (err) { return callback(err); } @@ -175,13 +184,19 @@ module.exports = function(Posts) { function (next) { topics.decreasePostCount(postData.tid, next); }, - function(next) { + function (next) { topics.updateTeaser(postData.tid, next); }, - function(next) { + function (next) { + updateTopicTimestamp(topicData, next); + }, + function (next) { db.sortedSetIncrBy('cid:' + topicData.cid + ':tids:posts', -1, postData.tid, next); }, - function(next) { + function (next) { + db.sortedSetIncrBy('tid:' + postData.tid + ':posters', -1, postData.uid, next); + }, + function (next) { user.incrementUserPostCountBy(postData.uid, -1, next); } ], callback); @@ -191,12 +206,12 @@ module.exports = function(Posts) { } function deletePostFromCategoryRecentPosts(pid, callback) { - db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { if (err) { return callback(err); } - var sets = cids.map(function(cid) { + var sets = cids.map(function (cid) { return 'cid:' + cid + ':pids'; }); @@ -204,55 +219,55 @@ module.exports = function(Posts) { }); } - function deletePostFromUsersFavourites(pid, callback) { - db.getSetMembers('pid:' + pid + ':users_favourited', function(err, uids) { + function deletePostFromUsersBookmarks(pid, callback) { + db.getSetMembers('pid:' + pid + ':users_bookmarked', function (err, uids) { if (err) { return callback(err); } - var sets = uids.map(function(uid) { - return 'uid:' + uid + ':favourites'; + var sets = uids.map(function (uid) { + return 'uid:' + uid + ':bookmarks'; }); - db.sortedSetsRemove(sets, pid, function(err) { + db.sortedSetsRemove(sets, pid, function (err) { if (err) { return callback(err); } - db.delete('pid:' + pid + ':users_favourited', callback); + db.delete('pid:' + pid + ':users_bookmarked', callback); }); }); } function deletePostFromUsersVotes(pid, callback) { async.parallel({ - upvoters: function(next) { + upvoters: function (next) { db.getSetMembers('pid:' + pid + ':upvote', next); }, - downvoters: function(next) { + downvoters: function (next) { db.getSetMembers('pid:' + pid + ':downvote', next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - var upvoterSets = results.upvoters.map(function(uid) { + var upvoterSets = results.upvoters.map(function (uid) { return 'uid:' + uid + ':upvote'; }); - var downvoterSets = results.downvoters.map(function(uid) { + var downvoterSets = results.downvoters.map(function (uid) { return 'uid:' + uid + ':downvote'; }); async.parallel([ - function(next) { + function (next) { db.sortedSetsRemove(upvoterSets, pid, next); }, - function(next) { + function (next) { db.sortedSetsRemove(downvoterSets, pid, next); }, - function(next) { + function (next) { db.deleteAll(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], next); } ], callback); diff --git a/src/posts/edit.js b/src/posts/edit.js index e8af9ae2d4..e8a5ea8395 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -1,25 +1,25 @@ 'use strict'; -var async = require('async'), - validator = require('validator'), - _ = require('underscore'), - db = require('../database'), - topics = require('../topics'), - user = require('../user'), - privileges = require('../privileges'), - plugins = require('../plugins'), - cache = require('./cache'), - pubsub = require('../pubsub'), - utils = require('../../public/src/utils'); - -module.exports = function(Posts) { - - pubsub.on('post:edit', function(pid) { +var async = require('async'); +var validator = require('validator'); +var _ = require('underscore'); + +var db = require('../database'); +var topics = require('../topics'); +var user = require('../user'); +var privileges = require('../privileges'); +var plugins = require('../plugins'); +var cache = require('./cache'); +var pubsub = require('../pubsub'); +var utils = require('../../public/src/utils'); + +module.exports = function (Posts) { + + pubsub.on('post:edit', function (pid) { cache.del(pid); }); - Posts.edit = function(data, callback) { - var now = Date.now(); + Posts.edit = function (data, callback) { var postData; var results; @@ -28,8 +28,8 @@ module.exports = function(Posts) { privileges.posts.canEdit(data.pid, data.uid, next); }, function (canEdit, next) { - if (!canEdit) { - return next(new Error('[[error:no-privileges]]')); + if (!canEdit.flag) { + return next(new Error(canEdit.message)); } Posts.getPostData(data.pid, next); }, @@ -37,30 +37,26 @@ module.exports = function(Posts) { if (!_postData) { return next(new Error('[[error:no-post]]')); } + postData = _postData; postData.content = data.content; - postData.edited = now; + postData.edited = Date.now(); postData.editor = data.uid; - plugins.fireHook('filter:post.edit', {req: data.req, post: postData, uid: data.uid}, next); + if (data.handle) { + postData.handle = data.handle; + } + plugins.fireHook('filter:post.edit', {req: data.req, post: postData, data: data, uid: data.uid}, next); }, function (result, next) { postData = result.post; - var updateData = { - edited: postData.edited, - editor: postData.editor, - content: postData.content - }; - if (data.handle) { - updateData.handle = data.handle; - } - Posts.setPostFields(data.pid, updateData, next); + Posts.setPostFields(data.pid, postData, next); }, function (next) { async.parallel({ - editor: function(next) { + editor: function (next) { user.getUserFields(data.uid, ['username', 'userslug'], next); }, - topic: function(next) { + topic: function (next) { editMainPost(data, postData, next); } }, next); @@ -72,8 +68,8 @@ module.exports = function(Posts) { plugins.fireHook('action:post.edit', _.clone(postData)); - cache.del(postData.pid); - pubsub.publish('post:edit', postData.pid); + cache.del(String(postData.pid)); + pubsub.publish('post:edit', String(postData.pid)); Posts.parsePost(postData, next); }, @@ -89,13 +85,13 @@ module.exports = function(Posts) { var title = data.title ? data.title.trim() : ''; async.parallel({ - topic: function(next) { + topic: function (next) { topics.getTopicFields(tid, ['cid', 'title'], next); }, - isMain: function(next) { + isMain: function (next) { Posts.isMain(data.pid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -121,29 +117,31 @@ module.exports = function(Posts) { topicData.slug = tid + '/' + (utils.slugify(title) || 'topic'); } - topicData.thumb = data.topic_thumb || ''; + topicData.thumb = data.thumb || ''; data.tags = data.tags || []; async.waterfall([ - async.apply(plugins.fireHook, 'filter:topic.edit', {req: data.req, topic: topicData}), - function(results, next) { + function (next) { + plugins.fireHook('filter:topic.edit', {req: data.req, topic: topicData, data: data}, next); + }, + function (results, next) { db.setObject('topic:' + tid, results.topic, next); }, - function(next) { + function (next) { topics.updateTags(tid, data.tags, next); }, - function(next) { + function (next) { topics.getTopicTagsObjects(tid, next); }, - function(tags, next) { + function (tags, next) { topicData.tags = data.tags; plugins.fireHook('action:topic.edit', topicData); next(null, { tid: tid, cid: results.topic.cid, uid: postData.uid, - title: validator.escape(title), + title: validator.escape(String(title)), oldTitle: results.topic.title, slug: topicData.slug, isMainPost: true, diff --git a/src/posts/flags.js b/src/posts/flags.js index 3adb6541f2..84116346af 100644 --- a/src/posts/flags.js +++ b/src/posts/flags.js @@ -2,63 +2,78 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - user = require('../user'); +var async = require('async'); +var winston = require('winston'); +var db = require('../database'); +var user = require('../user'); +var analytics = require('../analytics'); +module.exports = function (Posts) { -module.exports = function(Posts) { - - Posts.flag = function(post, uid, reason, callback) { + Posts.flag = function (post, uid, reason, callback) { if (!parseInt(uid, 10) || !reason) { return 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', 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.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next); - }, - function(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(); + async.waterfall([ + function (next) { + async.parallel({ + hasFlagged: async.apply(hasFlagged, post.pid, uid), + exists: async.apply(Posts.exists, post.pid) + }, next); + }, + function (results, next) { + if (!results.exists) { + return next(new Error('[[error:no-post]]')); + } + + if (results.hasFlagged) { + return next(new Error('[[error:already-flagged]]')); + } + + var now = Date.now(); + async.parallel([ + function (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.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next); + }, + function (next) { + if (parseInt(post.uid, 10)) { + async.parallel([ + async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid), + async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'), + async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid) + ], next); + } else { + next(); + } } + ], next); + }, + function (data, next) { + if (data[1] === 1) { // Only update state on new flag + Posts.updateFlagData(uid, post.pid, { + state: 'open' + }, next); } - ], function(err) { - callback(err); - }); + } + ], function (err) { + if (err) { + return callback(err); + } + analytics.increment('flags'); + callback(); }); }; @@ -66,51 +81,109 @@ module.exports = function(Posts) { db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback); } - Posts.dismissFlag = function(pid, callback) { - async.parallel([ - function(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); + Posts.dismissFlag = function (pid, callback) { + async.waterfall([ + function (next) { + db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next); }, - function(next) { - db.delete('pid:' + pid + ':flag:uids', next); + function (postData, next) { + if (!postData.pid) { + return callback(); + } + async.parallel([ + function (next) { + if (parseInt(postData.uid, 10)) { + if (parseInt(postData.flags, 10) > 0) { + async.parallel([ + async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid), + async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags) + ], next); + } else { + next(); + } + } else { + next(); + } + }, + function (next) { + db.sortedSetsRemove([ + 'posts:flagged', + 'posts:flags:count', + 'uid:' + postData.uid + ':flag:pids' + ], pid, next); + }, + function (next) { + async.series([ + function (next) { + db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) { + if (err) { + return next(err); + } + + async.each(uids, function (uid, next) { + var nid = 'post_flag:' + pid + ':uid:' + uid; + async.parallel([ + async.apply(db.delete, 'notifications:' + nid), + async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid) + ], next); + }, next); + }); + }, + async.apply(db.delete, 'pid:' + pid + ':flag:uids') + ], next); + }, + async.apply(db.deleteObjectField, 'post:' + pid, 'flags'), + async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'), + async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history']) + ], next); }, - function(next) { - db.delete('pid:' + pid + ':flag:uid:reason', next); + function (results, next) { + db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next); } - ], function(err) { - callback(err); + ], callback); + }; + + Posts.dismissAllFlags = function (callback) { + db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) { + if (err) { + return callback(err); + } + async.eachSeries(pids, Posts.dismissFlag, callback); }); }; - Posts.dismissAllFlags = function(callback) { - db.getSortedSetRange('posts:flagged', 0, -1, function(err, pids) { + Posts.dismissUserFlags = function (uid, callback) { + db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) { if (err) { return callback(err); } - async.eachLimit(pids, 50, Posts.dismissFlag, callback); + async.eachSeries(pids, Posts.dismissFlag, callback); }); }; - Posts.getFlags = function(set, uid, start, stop, callback) { + Posts.getFlags = function (set, cid, uid, start, stop, callback) { async.waterfall([ function (next) { - db.getSortedSetRevRange(set, start, stop, next); + if (Array.isArray(set)) { + db.getSortedSetRevIntersect({sets: set, start: start, stop: -1, aggregate: 'MAX'}, next); + } else { + db.getSortedSetRevRange(set, start, -1, next); + } + }, + function (pids, next) { + if (cid) { + Posts.filterPidsByCid(pids, cid, next); + } else { + process.nextTick(next, null, pids); + } }, function (pids, next) { getFlaggedPostsWithReasons(pids, uid, next); + }, + function (posts, next) { + var count = posts.length; + var end = stop - start + 1; + next(null, {posts: posts.slice(0, stop === -1 ? undefined : end), count: count}); } ], callback); }; @@ -119,31 +192,31 @@ module.exports = function(Posts) { async.waterfall([ function (next) { async.parallel({ - uidsReasons: function(next) { - async.map(pids, function(pid, next) { + uidsReasons: function (next) { + async.map(pids, function (pid, next) { db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next); }, next); }, - posts: function(next) { - Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, next); + posts: function (next) { + Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next); } }, next); }, function (results, next) { - async.map(results.uidsReasons, function(uidReasons, next) { - async.map(uidReasons, function(uidReason, next) { + async.map(results.uidsReasons, function (uidReasons, next) { + async.map(uidReasons, function (uidReason, next) { var uid = uidReason.split(':')[0]; var reason = uidReason.substr(uidReason.indexOf(':') + 1); - user.getUserFields(uid, ['username', 'userslug', 'picture'], function(err, userData) { + user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) { next(err, {user: userData, reason: reason}); }); }, next); - }, function(err, reasons) { + }, function (err, reasons) { if (err) { return callback(err); } - results.posts.forEach(function(post, index) { + results.posts.forEach(function (post, index) { if (post) { post.flagReasons = reasons[index]; } @@ -151,33 +224,183 @@ module.exports = function(Posts) { next(null, results.posts); }); + }, + async.apply(Posts.expandFlagHistory), + function (posts, next) { + // Parse out flag data into its own object inside each post hash + async.map(posts, function (postObj, next) { + for(var prop in postObj) { + postObj.flagData = postObj.flagData || {}; + + if (postObj.hasOwnProperty(prop) && prop.startsWith('flag:')) { + postObj.flagData[prop.slice(5)] = postObj[prop]; + + if (prop === 'flag:state') { + switch(postObj[prop]) { + case 'open': + postObj.flagData.labelClass = 'info'; + break; + case 'wip': + postObj.flagData.labelClass = 'warning'; + break; + case 'resolved': + postObj.flagData.labelClass = 'success'; + break; + case 'rejected': + postObj.flagData.labelClass = 'danger'; + break; + } + } + + delete postObj[prop]; + } + } + + if (postObj.flagData.assignee) { + user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) { + if (err) { + return next(err); + } + + postObj.flagData.assigneeUser = userData; + next(null, postObj); + }); + } else { + setImmediate(next.bind(null, null, postObj)); + } + }, next); } ], callback); } - Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, stop, callback) { - async.waterfall([ - function(next) { - user.getUidByUsername(byUsername, next); - }, - function(uid, next) { - if (!uid) { - return next(null, []); + Posts.updateFlagData = function (uid, pid, flagObj, callback) { + // Retrieve existing flag data to compare for history-saving purposes + var changes = []; + var changeset = {}; + var prop; + + Posts.getPostData(pid, function (err, postData) { + if (err) { + return callback(err); + } + + // Track new additions + for(prop in flagObj) { + if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) { + changes.push(prop); } - db.getSortedSetRevRange('uid:' + uid + ':flag:pids', 0, -1, next); - }, - function(pids, next) { - getFlaggedPostsWithReasons(pids, callerUID, next); - }, - function(posts, next) { - if (sortBy === 'count') { - posts.sort(function(a, b) { - return b.flags - a.flags; + } + + // Track changed items + for(prop in postData) { + if ( + postData.hasOwnProperty(prop) && prop.startsWith('flag:') && + flagObj.hasOwnProperty(prop.slice(5)) && + postData[prop] !== flagObj[prop.slice(5)] + ) { + changes.push(prop.slice(5)); + } + } + + changeset = changes.reduce(function (memo, prop) { + memo['flag:' + prop] = flagObj[prop]; + return memo; + }, {}); + + // Append changes to history string + if (changes.length) { + try { + var history = JSON.parse(postData['flag:history'] || '[]'); + + changes.forEach(function (property) { + switch(property) { + case 'assignee': // intentional fall-through + case 'state': + history.unshift({ + uid: uid, + type: property, + value: flagObj[property], + timestamp: Date.now() + }); + break; + + case 'notes': + history.unshift({ + uid: uid, + type: property, + timestamp: Date.now() + }); + } }); + + changeset['flag:history'] = JSON.stringify(history); + } catch (e) { + winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data'); } + } - next(null, posts.slice(start, stop)); + // Save flag data into post hash + if (changes.length) { + Posts.setPostFields(pid, changeset, callback); + } else { + setImmediate(callback); } - ], callback); + }); + }; + + Posts.expandFlagHistory = function (posts, callback) { + // Expand flag history + async.map(posts, function (post, next) { + var history; + try { + history = JSON.parse(post['flag:history'] || '[]'); + } catch (e) { + winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data'); + return callback(e); + } + + async.map(history, function (event, next) { + event.timestampISO = new Date(event.timestamp).toISOString(); + + async.parallel([ + function (next) { + user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) { + if (err) { + return next(err); + } + + event.user = userData; + next(); + }); + }, + function (next) { + if (event.type === 'assignee') { + user.getUserField(parseInt(event.value, 10), 'username', function (err, username) { + if (err) { + return next(err); + } + + event.label = username || 'Unknown user'; + next(null); + }); + } else if (event.type === 'state') { + event.label = '[[topic:flag_manage_state_' + event.value + ']]'; + setImmediate(next); + } else { + setImmediate(next); + } + } + ], function (err) { + next(err, event); + }); + }, function (err, history) { + if (err) { + return next(err); + } + + post['flag:history'] = history; + next(null, post); + }); + }, callback); }; }; diff --git a/src/posts/parse.js b/src/posts/parse.js index 1af374305d..28af97c5e6 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -1,17 +1,22 @@ - 'use strict'; +var nconf = require('nconf'); +var url = require('url'); +var winston = require('winston'); + var cache = require('./cache'); var plugins = require('../plugins'); var translator = require('../../public/src/modules/translator'); -module.exports = function(Posts) { +var urlRegex = /href="([^"]+)"/g; + +module.exports = function (Posts) { - Posts.parsePost = function(postData, callback) { + Posts.parsePost = function (postData, callback) { postData.content = postData.content || ''; - if (postData.pid && cache.has(postData.pid)) { - postData.content = cache.get(postData.pid); + if (postData.pid && cache.has(String(postData.pid))) { + postData.content = cache.get(String(postData.pid)); return callback(null, postData); } @@ -20,7 +25,7 @@ module.exports = function(Posts) { postData.content = postData.content.toString(); } - plugins.fireHook('filter:parse.post', {postData: postData}, function(err, data) { + plugins.fireHook('filter:parse.post', {postData: postData}, function (err, data) { if (err) { return callback(err); } @@ -28,16 +33,44 @@ module.exports = function(Posts) { data.postData.content = translator.escape(data.postData.content); if (global.env === 'production' && data.postData.pid) { - cache.set(data.postData.pid, data.postData.content); + cache.set(String(data.postData.pid), data.postData.content); } callback(null, data.postData); }); }; - Posts.parseSignature = function(userData, uid, callback) { + Posts.parseSignature = function (userData, uid, callback) { userData.signature = userData.signature || ''; plugins.fireHook('filter:parse.signature', {userData: userData, uid: uid}, callback); }; + + Posts.relativeToAbsolute = function (content) { + // Turns relative links in post body to absolute urls + var parsed, current, absolute; + + while ((current = urlRegex.exec(content)) !== null) { + if (current[1]) { + try { + parsed = url.parse(current[1]); + if (!parsed.protocol) { + if (current[1].startsWith('/')) { + // Internal link + absolute = nconf.get('url') + current[1]; + } else { + // External link + absolute = '//' + current[1]; + } + + content = content.slice(0, current.index + 6) + absolute + content.slice(current.index + 6 + current[1].length); + } + } catch(err) { + winston.verbose(err.messsage); + } + } + } + + return content; + }; }; diff --git a/src/posts/recent.js b/src/posts/recent.js index f7d588acff..aec0ea4637 100644 --- a/src/posts/recent.js +++ b/src/posts/recent.js @@ -5,14 +5,14 @@ var async = require('async'), privileges = require('../privileges'); -module.exports = function(Posts) { +module.exports = function (Posts) { var terms = { day: 86400000, week: 604800000, month: 2592000000 }; - Posts.getRecentPosts = function(uid, start, stop, term, callback) { + Posts.getRecentPosts = function (uid, start, stop, term, callback) { var min = 0; if (terms[term]) { min = Date.now() - terms[term]; @@ -21,30 +21,30 @@ module.exports = function(Posts) { var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min, next); }, - function(pids, next) { + function (pids, next) { privileges.posts.filter('read', pids, uid, next); }, - function(pids, next) { + function (pids, next) { Posts.getPostSummaryByPids(pids, uid, {stripTags: true}, next); } ], callback); }; - Posts.getRecentPosterUids = function(start, stop, callback) { + Posts.getRecentPosterUids = function (start, stop, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('posts:pid', start, stop, next); }, - function(pids, next) { + function (pids, next) { Posts.getPostsFields(pids, ['uid'], next); }, - function(postData, next) { - postData = postData.map(function(post) { + function (postData, next) { + postData = postData.map(function (post) { return post && post.uid; - }).filter(function(value, index, array) { + }).filter(function (value, index, array) { return value && array.indexOf(value) === index; }); next(null, postData); diff --git a/src/posts/summary.js b/src/posts/summary.js index 3cb5586bc6..270d9d480b 100644 --- a/src/posts/summary.js +++ b/src/posts/summary.js @@ -1,20 +1,20 @@ 'use strict'; -var async = require('async'), - validator = require('validator'), - S = require('string'), +var async = require('async'); +var validator = require('validator'); +var S = require('string'); - db = require('../database'), - user = require('../user'), - plugins = require('../plugins'), - categories = require('../categories'), - utils = require('../../public/src/utils'); +var db = require('../database'); +var user = require('../user'); +var plugins = require('../plugins'); +var categories = require('../categories'); +var utils = require('../../public/src/utils'); -module.exports = function(Posts) { +module.exports = function (Posts) { - Posts.getPostSummaryByPids = function(pids, uid, options, callback) { + Posts.getPostSummaryByPids = function (pids, uid, options, callback) { if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } @@ -23,109 +23,112 @@ module.exports = function(Posts) { options.parse = options.hasOwnProperty('parse') ? options.parse : true; options.extraFields = options.hasOwnProperty('extraFields') ? options.extraFields : []; - var fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted'].concat(options.extraFields); + var fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes'].concat(options.extraFields); - Posts.getPostsFields(pids, fields, function(err, posts) { - if (err) { - return callback(err); - } - - posts = posts.filter(function(p) { - return !!p && parseInt(p.deleted, 10) !== 1; - }); + var posts; + async.waterfall([ + function (next) { + Posts.getPostsFields(pids, fields, next); + }, + function (_posts, next) { + posts = _posts.filter(Boolean); - var uids = [], topicKeys = []; - for(var i=0; i<posts.length; ++i) { - if (uids.indexOf(posts[i].uid) === -1) { - uids.push(posts[i].uid); - } - if (topicKeys.indexOf('topic:' + posts[i].tid) === -1) { - topicKeys.push('topic:' + posts[i].tid); - } - } - - async.parallel({ - users: function(next) { - user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next); - }, - topicsAndCategories: function(next) { - getTopicAndCategories(topicKeys, next); - }, - indices: function(next) { - Posts.getPostIndices(posts, uid, next); - } - }, function(err, results) { - if (err) { - return callback(err); - } + var uids = []; + var topicKeys = []; + posts.forEach(function (post, i) { + if (uids.indexOf(posts[i].uid) === -1) { + uids.push(posts[i].uid); + } + if (topicKeys.indexOf('topic:' + posts[i].tid) === -1) { + topicKeys.push('topic:' + posts[i].tid); + } + }); + async.parallel({ + users: function (next) { + user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next); + }, + topicsAndCategories: function (next) { + getTopicAndCategories(topicKeys, next); + } + }, next); + }, + function (results, next) { results.users = toObject('uid', results.users); results.topics = toObject('tid', results.topicsAndCategories.topics); results.categories = toObject('cid', results.topicsAndCategories.categories); - for (var i=0; i<posts.length; ++i) { - posts[i].index = utils.isNumber(results.indices[i]) ? parseInt(results.indices[i], 10) + 1 : 1; - posts[i].isMainPost = posts[i].index - 1 === 0; - } - - posts = posts.filter(function(post) { - return results.topics[post.tid] && parseInt(results.topics[post.tid].deleted, 10) !== 1; - }); - - async.map(posts, function(post, next) { + posts.forEach(function (post) { // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. if (!results.users.hasOwnProperty(post.uid)) { post.uid = 0; } - post.user = results.users[post.uid]; post.topic = results.topics[post.tid]; post.category = results.categories[post.topic.cid]; + post.isMainPost = parseInt(post.pid, 10) === parseInt(post.topic.mainPid, 10); + post.deleted = parseInt(post.deleted, 10) === 1; + post.upvotes = parseInt(post.upvotes, 10) || 0; + post.downvotes = parseInt(post.downvotes, 10) || 0; + post.votes = post.upvotes - post.downvotes; post.timestampISO = utils.toISOString(post.timestamp); + }); - if (!post.content || !options.parse) { - if (options.stripTags) { - post.content = stripTags(post.content); - } - post.content = post.content ? validator.escape(post.content) : post.content; - return next(null, post); - } - - Posts.parsePost(post, function(err, post) { - if (err) { - return next(err); - } - if (options.stripTags) { - post.content = stripTags(post.content); - } - - next(null, post); - }); - }, function(err, posts) { - plugins.fireHook('filter:post.getPostSummaryByPids', {posts: posts, uid: uid}, function(err, postData) { - callback(err, postData.posts); - }); + posts = posts.filter(function (post) { + return results.topics[post.tid]; }); - }); - }); + + parsePosts(posts, options, next); + }, + function (posts, next) { + plugins.fireHook('filter:post.getPostSummaryByPids', {posts: posts, uid: uid}, next); + }, + function (data, next) { + next(null, data.posts); + } + ], callback); }; + function parsePosts(posts, options, callback) { + async.map(posts, function (post, next) { + if (!post.content || !options.parse) { + if (options.stripTags) { + post.content = stripTags(post.content); + } + post.content = post.content ? validator.escape(String(post.content)) : post.content; + return next(null, post); + } + + Posts.parsePost(post, function (err, post) { + if (err) { + return next(err); + } + if (options.stripTags) { + post.content = stripTags(post.content); + } + + next(null, post); + }); + }, callback); + } + function getTopicAndCategories(topicKeys, callback) { - db.getObjectsFields(topicKeys, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount'], function(err, topics) { + db.getObjectsFields(topicKeys, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount', 'mainPid'], function (err, topics) { if (err) { return callback(err); } - var cids = topics.map(function(topic) { + var cids = topics.map(function (topic) { if (topic) { - topic.title = validator.escape(topic.title); + topic.title = validator.escape(String(topic.title)); + topic.deleted = parseInt(topic.deleted, 10) === 1; } return topic && topic.cid; - }).filter(function(topic, index, array) { + }).filter(function (topic, index, array) { return topic && array.indexOf(topic) === index; }); - categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color'], function(err, categories) { + categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color'], function (err, categories) { callback(err, {topics: topics, categories: categories}); }); }); @@ -133,7 +136,7 @@ module.exports = function(Posts) { function toObject(key, data) { var obj = {}; - for(var i=0; i<data.length; ++i) { + for(var i = 0; i < data.length; ++i) { obj[data[i][key]] = data[i]; } return obj; @@ -146,4 +149,4 @@ module.exports = function(Posts) { } return content; } -}; \ No newline at end of file +}; diff --git a/src/posts/tools.js b/src/posts/tools.js index 64ddf155d7..82d55b1192 100644 --- a/src/posts/tools.js +++ b/src/posts/tools.js @@ -5,14 +5,14 @@ var async = require('async'); var privileges = require('../privileges'); var cache = require('./cache'); -module.exports = function(Posts) { +module.exports = function (Posts) { Posts.tools = {}; - Posts.tools.delete = function(uid, pid, callback) { + Posts.tools.delete = function (uid, pid, callback) { togglePostDelete(uid, pid, true, callback); }; - Posts.tools.restore = function(uid, pid, callback) { + Posts.tools.restore = function (uid, pid, callback) { togglePostDelete(uid, pid, false, callback); }; @@ -34,18 +34,18 @@ module.exports = function(Posts) { return next(new Error('[[error:post-already-restored]]')); } - privileges.posts.canEdit(pid, uid, next); + privileges.posts.canDelete(pid, uid, next); }, - function (canEdit, next) { - if (!canEdit) { - return next(new Error('[[error:no-privileges]]')); + function (canDelete, next) { + if (!canDelete.flag) { + return next(new Error(canDelete.message)); } if (isDelete) { cache.del(pid); Posts.delete(pid, uid, next); } else { - Posts.restore(pid, uid, function(err, postData) { + Posts.restore(pid, uid, function (err, postData) { if (err) { return next(err); } @@ -56,7 +56,7 @@ module.exports = function(Posts) { ], callback); } - Posts.tools.purge = function(uid, pid, callback) { + Posts.tools.purge = function (uid, pid, callback) { async.waterfall([ function (next) { privileges.posts.canPurge(pid, uid, next); diff --git a/src/posts/topics.js b/src/posts/topics.js index a9946fdace..0653c880e6 100644 --- a/src/posts/topics.js +++ b/src/posts/topics.js @@ -1,45 +1,88 @@ 'use strict'; -var async = require('async'), - topics = require('../topics'); +var async = require('async'); -module.exports = function(Posts) { +var topics = require('../topics'); +var utils = require('../../public/src/utils'); - Posts.getPostsFromSet = function(set, start, stop, uid, reverse, callback) { +module.exports = function (Posts) { + + Posts.getPostsFromSet = function (set, start, stop, uid, reverse, callback) { async.waterfall([ - function(next) { + function (next) { Posts.getPidsFromSet(set, start, stop, reverse, next); }, - function(pids, next) { + function (pids, next) { Posts.getPostsByPids(pids, uid, next); } ], callback); }; - Posts.isMain = function(pid, callback) { + Posts.isMain = function (pid, callback) { async.waterfall([ - function(next) { + function (next) { Posts.getPostField(pid, 'tid', next); }, - function(tid, next) { + function (tid, next) { topics.getTopicField(tid, 'mainPid', next); }, - function(mainPid, next) { + function (mainPid, next) { next(null, parseInt(pid, 10) === parseInt(mainPid, 10)); } ], callback); }; - Posts.getTopicFields = function(pid, fields, callback) { + Posts.getTopicFields = function (pid, fields, callback) { async.waterfall([ - function(next) { + function (next) { Posts.getPostField(pid, 'tid', next); }, - function(tid, next) { + function (tid, next) { topics.getTopicFields(tid, fields, next); } ], callback); }; + Posts.generatePostPath = function (pid, uid, callback) { + Posts.generatePostPaths([pid], uid, function (err, paths) { + callback(err, Array.isArray(paths) && paths.length ? paths[0] : null); + }); + }; + + Posts.generatePostPaths = function (pids, uid, callback) { + async.waterfall([ + function (next) { + Posts.getPostsFields(pids, ['pid', 'tid'], next); + }, + function (postData, next) { + async.parallel({ + indices: function (next) { + Posts.getPostIndices(postData, uid, next); + }, + topics: function (next) { + var tids = postData.map(function (post) { + return post ? post.tid : null; + }); + + topics.getTopicsFields(tids, ['slug'], next); + } + }, next); + }, + function (results, next) { + var paths = pids.map(function (pid, index) { + var slug = results.topics[index] ? results.topics[index].slug : null; + var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null; + + if (slug && postIndex) { + return '/topic/' + slug + '/' + postIndex; + } + return null; + }); + + next(null, paths); + } + ], callback); + }; + }; \ No newline at end of file diff --git a/src/posts/user.js b/src/posts/user.js index cd675cbfa3..9a7fe38893 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -1,66 +1,49 @@ 'use strict'; -var async = require('async'), - validator = require('validator'), +var async = require('async'); +var validator = require('validator'); - db = require('../database'), - user = require('../user'), - groups = require('../groups'), - meta = require('../meta'), - plugins = require('../plugins'); +var user = require('../user'); +var groups = require('../groups'); +var meta = require('../meta'); +var plugins = require('../plugins'); +module.exports = function (Posts) { -module.exports = function(Posts) { - - Posts.getUserInfoForPosts = function(uids, uid, callback) { + Posts.getUserInfoForPosts = function (uids, uid, callback) { var groupsMap = {}; - var userSettings; - async.parallel({ - groupTitles: function(next) { - var keys = uids.map(function(uid) { - return 'user:' + uid + ':settings'; - }); - async.waterfall([ - function (next) { - db.getObjectsFields(keys, ['groupTitle'], next); - }, - function (_userSettings, next) { - userSettings = _userSettings; - var groupKeys = userSettings.filter(function(userSetting) { - return userSetting && userSetting.groupTitle; - }).map(function(userSetting) { - return userSetting.groupTitle; - }).filter(function(groupTitle, index, array) { - return groupTitle && array.indexOf(groupTitle) === index; - }); - groups.getGroupsData(groupKeys, next); - }, - function (groupsData, next) { - groupsData.forEach(function(group) { - if (group && group.userTitleEnabled) { - groupsMap[group.name] = { - name: group.name, - slug: group.slug, - labelColor: group.labelColor, - icon: group.icon, - userTitle: group.userTitle - }; - } - }); - next(null, userSettings); - } - ], next); + var userData; + async.waterfall([ + function (next) { + user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status', 'lastonline', 'groupTitle'], next); }, - userData: function(next) { - user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status', 'lastonline'], next); + function (_userData, next) { + userData = _userData; + var groupTitles = userData.map(function (userData) { + return userData && userData.groupTitle; + }).filter(function (groupTitle, index, array) { + return groupTitle && array.indexOf(groupTitle) === index; + }); + groups.getGroupsData(groupTitles, next); } - }, function(err, results) { + ], function (err, groupsData) { if (err) { return callback(err); } - var userData = results.userData; - userData.forEach(function(userData, i) { + groupsData.forEach(function (group) { + if (group && group.userTitleEnabled) { + groupsMap[group.name] = { + name: group.name, + slug: group.slug, + labelColor: group.labelColor, + icon: group.icon, + userTitle: group.userTitle + }; + } + }); + + userData.forEach(function (userData) { userData.uid = userData.uid || 0; userData.username = userData.username || '[[global:guest]]'; userData.userslug = userData.userslug || ''; @@ -69,30 +52,29 @@ module.exports = function(Posts) { userData.banned = parseInt(userData.banned, 10) === 1; userData.picture = userData.picture || ''; userData.status = user.getStatus(userData); - userData.groupTitle = results.groupTitles[i].groupTitle; - userData.signature = validator.escape(userData.signature || ''); - userData.fullname = validator.escape(userData.fullname || ''); + userData.signature = validator.escape(String(userData.signature || '')); + userData.fullname = validator.escape(String(userData.fullname || '')); }); - async.map(userData, function(userData, next) { + async.map(userData, function (userData, next) { async.parallel({ isMemberOfGroup: function (next) { - if (!userData.groupTitle) { + if (!userData.groupTitle || !groupsMap[userData.groupTitle]) { return next(); } groups.isMember(userData.uid, userData.groupTitle, next); }, - signature: function(next) { + signature: function (next) { if (!userData.signature || parseInt(meta.config.disableSignatures, 10) === 1) { userData.signature = ''; return next(); } Posts.parseSignature(userData, uid, next); }, - customProfileInfo: function(next) { + customProfileInfo: function (next) { plugins.fireHook('filter:posts.custom_profile_info', {profile: [], uid: userData.uid}, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } @@ -110,17 +92,17 @@ module.exports = function(Posts) { }); }; - Posts.isOwner = function(pid, uid, callback) { + Posts.isOwner = function (pid, uid, callback) { uid = parseInt(uid, 10); if (Array.isArray(pid)) { if (!uid) { - return callback(null, pid.map(function() {return false;})); + return callback(null, pid.map(function () {return false;})); } - Posts.getPostsFields(pid, ['uid'], function(err, posts) { + Posts.getPostsFields(pid, ['uid'], function (err, posts) { if (err) { return callback(err); } - posts = posts.map(function(post) { + posts = posts.map(function (post) { return post && parseInt(post.uid, 10) === uid; }); callback(null, posts); @@ -129,17 +111,17 @@ module.exports = function(Posts) { if (!uid) { return callback(null, false); } - Posts.getPostField(pid, 'uid', function(err, author) { + Posts.getPostField(pid, 'uid', function (err, author) { callback(err, parseInt(author, 10) === uid); }); } }; - Posts.isModerator = function(pids, uid, callback) { + Posts.isModerator = function (pids, uid, callback) { if (!parseInt(uid, 10)) { - return callback(null, pids.map(function() {return false;})); + return callback(null, pids.map(function () {return false;})); } - Posts.getCidsByPids(pids, function(err, cids) { + Posts.getCidsByPids(pids, function (err, cids) { if (err) { return callback(err); } diff --git a/src/favourites.js b/src/posts/votes.js similarity index 56% rename from src/favourites.js rename to src/posts/votes.js index e5d8eb3e0a..428685257a 100644 --- a/src/favourites.js +++ b/src/posts/votes.js @@ -1,106 +1,17 @@ -"use strict"; +'use strict'; var async = require('async'); -var db = require('./database'); -var posts = require('./posts'); -var user = require('./user'); -var plugins = require('./plugins'); -var meta = require('./meta'); -(function (Favourites) { +var meta = require('../meta'); +var db = require('../database'); +var user = require('../user'); +var plugins = require('../plugins'); - var votesInProgress = {}; - - function vote(type, unvote, pid, uid, callback) { - uid = parseInt(uid, 10); - - if (uid === 0) { - return callback(new Error('[[error:not-logged-in]]')); - } - - posts.getPostFields(pid, ['pid', 'uid', 'tid'], function (err, postData) { - if (err) { - return callback(err); - } - - var now = Date.now(); - - if (type === 'upvote' && !unvote) { - db.sortedSetAdd('uid:' + uid + ':upvote', now, pid); - } else { - db.sortedSetRemove('uid:' + uid + ':upvote', pid); - } - - if (type === 'upvote' || unvote) { - db.sortedSetRemove('uid:' + uid + ':downvote', pid); - } else { - db.sortedSetAdd('uid:' + uid + ':downvote', now, pid); - } - - user[type === 'upvote' ? 'incrementUserFieldBy' : 'decrementUserFieldBy'](postData.uid, 'reputation', 1, function (err, newreputation) { - if (err) { - return callback(err); - } - - if (parseInt(postData.uid, 10)) { - db.sortedSetAdd('users:reputation', newreputation, postData.uid); - } - - adjustPostVotes(postData, uid, type, unvote, function(err, votes) { - postData.votes = votes; - callback(err, { - user: { - reputation: newreputation - }, - post: postData, - upvote: type === 'upvote' && !unvote, - downvote: type === 'downvote' && !unvote - }); - }); - }); - }); - } - - function adjustPostVotes(postData, uid, type, unvote, callback) { - var notType = (type === 'upvote' ? 'downvote' : 'upvote'); - - async.series([ - function(next) { - if (unvote) { - db.setRemove('pid:' + postData.pid + ':' + type, uid, next); - } else { - db.setAdd('pid:' + postData.pid + ':' + type, uid, next); - } - }, - function(next) { - db.setRemove('pid:' + postData.pid + ':' + notType, uid, next); - } - ], function(err) { - if (err) { - return callback(err); - } +module.exports = function (Posts) { - async.parallel({ - upvotes: function(next) { - db.setCount('pid:' + postData.pid + ':upvote', next); - }, - downvotes: function(next) { - db.setCount('pid:' + postData.pid + ':downvote', next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - var voteCount = parseInt(results.upvotes, 10) - parseInt(results.downvotes, 10); - - posts.updatePostVoteCount(postData, voteCount, function(err) { - callback(err, voteCount); - }); - }); - }); - } + var votesInProgress = {}; - Favourites.upvote = function(pid, uid, callback) { + Posts.upvote = function (pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } @@ -111,13 +22,13 @@ var meta = require('./meta'); putVoteInProgress(pid, uid); - toggleVote('upvote', pid, uid, function(err, data) { + toggleVote('upvote', pid, uid, function (err, data) { clearVoteProgress(pid, uid); callback(err, data); }); }; - Favourites.downvote = function(pid, uid, callback) { + Posts.downvote = function (pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } @@ -132,25 +43,69 @@ var meta = require('./meta'); putVoteInProgress(pid, uid); - toggleVote('downvote', pid, uid, function(err, data) { + toggleVote('downvote', pid, uid, function (err, data) { clearVoteProgress(pid, uid); callback(err, data); }); }; - Favourites.unvote = function(pid, uid, callback) { + Posts.unvote = function (pid, uid, callback) { if (voteInProgress(pid, uid)) { return callback(new Error('[[error:already-voting-for-this-post]]')); } putVoteInProgress(pid, uid); - unvote(pid, uid, 'unvote', function(err, data) { + unvote(pid, uid, 'unvote', function (err, data) { clearVoteProgress(pid, uid); callback(err, data); }); }; + Posts.hasVoted = function (pid, uid, callback) { + if (!parseInt(uid, 10)) { + return callback(null, {upvoted: false, downvoted: false}); + } + + db.isMemberOfSets(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], uid, function (err, hasVoted) { + if (err) { + return callback(err); + } + + callback (null, {upvoted: hasVoted[0], downvoted: hasVoted[1]}); + }); + }; + + Posts.getVoteStatusByPostIDs = function (pids, uid, callback) { + if (!parseInt(uid, 10)) { + var data = pids.map(function () { return false; }); + return callback(null, {upvotes: data, downvotes: data}); + } + var upvoteSets = []; + var downvoteSets = []; + + for (var i = 0; i < pids.length; ++i) { + upvoteSets.push('pid:' + pids[i] + ':upvote'); + downvoteSets.push('pid:' + pids[i] + ':downvote'); + } + + async.parallel({ + upvotes: function (next) { + db.isMemberOfSets(upvoteSets, uid, next); + }, + downvotes: function (next) { + db.isMemberOfSets(downvoteSets, uid, next); + } + }, callback); + }; + + Posts.getUpvotedUidsByPids = function (pids, callback) { + var sets = pids.map(function (pid) { + return 'pid:' + pid + ':upvote'; + }); + db.getSetsMembers(sets, callback); + }; + function voteInProgress(pid, uid) { return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].indexOf(parseInt(pid, 10)) !== -1; } @@ -170,7 +125,7 @@ var meta = require('./meta'); } function toggleVote(type, pid, uid, callback) { - unvote(pid, uid, type, function(err) { + unvote(pid, uid, type, function (err) { if (err) { return callback(err); } @@ -181,16 +136,16 @@ var meta = require('./meta'); function unvote(pid, uid, command, callback) { async.parallel({ - owner: function(next) { - posts.getPostField(pid, 'uid', next); + owner: function (next) { + Posts.getPostField(pid, 'uid', next); }, - voteStatus: function(next) { - Favourites.hasVoted(pid, uid, next); + voteStatus: function (next) { + Posts.hasVoted(pid, uid, next); }, - reputation: function(next) { + reputation: function (next) { user.getUserField(uid, 'reputation', next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -231,146 +186,90 @@ var meta = require('./meta'); }); } - Favourites.hasVoted = function(pid, uid, callback) { - if (!parseInt(uid, 10)) { - return callback(null, {upvoted: false, downvoted: false}); + function vote(type, unvote, pid, uid, callback) { + uid = parseInt(uid, 10); + + if (uid === 0) { + return callback(new Error('[[error:not-logged-in]]')); } - db.isMemberOfSets(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], uid, function(err, hasVoted) { + Posts.getPostFields(pid, ['pid', 'uid', 'tid'], function (err, postData) { if (err) { return callback(err); } - callback (null, {upvoted: hasVoted[0], downvoted: hasVoted[1]}); - }); - }; - - Favourites.getVoteStatusByPostIDs = function(pids, uid, callback) { - if (!parseInt(uid, 10)) { - var data = pids.map(function() {return false;}); - return callback(null, {upvotes: data, downvotes: data}); - } - var upvoteSets = [], - downvoteSets = []; + var now = Date.now(); - for (var i=0; i<pids.length; ++i) { - upvoteSets.push('pid:' + pids[i] + ':upvote'); - downvoteSets.push('pid:' + pids[i] + ':downvote'); - } + if (type === 'upvote' && !unvote) { + db.sortedSetAdd('uid:' + uid + ':upvote', now, pid); + } else { + db.sortedSetRemove('uid:' + uid + ':upvote', pid); + } - async.parallel({ - upvotes: function(next) { - db.isMemberOfSets(upvoteSets, uid, next); - }, - downvotes: function(next) { - db.isMemberOfSets(downvoteSets, uid, next); + if (type === 'upvote' || unvote) { + db.sortedSetRemove('uid:' + uid + ':downvote', pid); + } else { + db.sortedSetAdd('uid:' + uid + ':downvote', now, pid); } - }, callback); - }; - Favourites.favourite = function (pid, uid, callback) { - toggleFavourite('favourite', pid, uid, callback); - }; + user[type === 'upvote' ? 'incrementUserFieldBy' : 'decrementUserFieldBy'](postData.uid, 'reputation', 1, function (err, newreputation) { + if (err) { + return callback(err); + } - Favourites.unfavourite = function(pid, uid, callback) { - toggleFavourite('unfavourite', pid, uid, callback); - }; + if (parseInt(postData.uid, 10)) { + db.sortedSetAdd('users:reputation', newreputation, postData.uid); + } - function toggleFavourite(type, pid, uid, callback) { - if (!parseInt(uid, 10)) { - return callback(new Error('[[error:not-logged-in]]')); - } - var isFavouriting = type === 'favourite'; + adjustPostVotes(postData, uid, type, unvote, function (err) { + callback(err, { + user: { + reputation: newreputation + }, + post: postData, + upvote: type === 'upvote' && !unvote, + downvote: type === 'downvote' && !unvote + }); + }); + }); + }); + } - async.parallel({ - owner: function(next) { - posts.getPostField(pid, 'uid', next); - }, - postData: function(next) { - posts.getPostFields(pid, ['pid', 'uid'], next); + function adjustPostVotes(postData, uid, type, unvote, callback) { + var notType = (type === 'upvote' ? 'downvote' : 'upvote'); + + async.series([ + function (next) { + if (unvote) { + db.setRemove('pid:' + postData.pid + ':' + type, uid, next); + } else { + db.setAdd('pid:' + postData.pid + ':' + type, uid, next); + } }, - hasFavourited: function(next) { - Favourites.hasFavourited(pid, uid, next); + function (next) { + db.setRemove('pid:' + postData.pid + ':' + notType, uid, next); } - }, function(err, results) { + ], function (err) { if (err) { return callback(err); } - if (isFavouriting && results.hasFavourited) { - return callback(new Error('[[error:already-favourited]]')); - } - - if (!isFavouriting && !results.hasFavourited) { - return callback(new Error('[[error:already-unfavourited]]')); - } - - async.waterfall([ - function(next) { - if (isFavouriting) { - db.sortedSetAdd('uid:' + uid + ':favourites', Date.now(), pid, next); - } else { - db.sortedSetRemove('uid:' + uid + ':favourites', pid, next); - } - }, - function(next) { - db[isFavouriting ? 'setAdd' : 'setRemove']('pid:' + pid + ':users_favourited', uid, next); - }, - function(next) { - db.setCount('pid:' + pid + ':users_favourited', next); + async.parallel({ + upvotes: function (next) { + db.setCount('pid:' + postData.pid + ':upvote', next); }, - function(count, next) { - results.postData.reputation = count; - posts.setPostField(pid, 'reputation', count, next); + downvotes: function (next) { + db.setCount('pid:' + postData.pid + ':downvote', next); } - ], function(err) { + }, function (err, results) { if (err) { return callback(err); } - - var current = results.hasFavourited ? 'favourited' : 'unfavourited'; - - plugins.fireHook('action:post.' + type, { - pid: pid, - uid: uid, - owner: results.owner, - current: current - }); - - callback(null, { - post: results.postData, - isFavourited: isFavouriting - }); + postData.upvotes = parseInt(results.upvotes, 10); + postData.downvotes = parseInt(results.downvotes, 10); + postData.votes = postData.upvotes - postData.downvotes; + Posts.updatePostVoteCount(postData, callback); }); }); } - - Favourites.hasFavourited = function(pid, uid, callback) { - if (!parseInt(uid, 10)) { - return callback(null, false); - } - db.isSetMember('pid:' + pid + ':users_favourited', uid, callback); - }; - - Favourites.getFavouritesByPostIDs = function(pids, uid, callback) { - if (!parseInt(uid, 10)) { - return callback(null, pids.map(function() {return false;})); - } - - var sets = []; - for (var i=0; i<pids.length; ++i) { - sets.push('pid:' + pids[i] + ':users_favourited'); - } - - db.isMemberOfSets(sets, uid, callback); - }; - - Favourites.getUpvotedUidsByPids = function(pids, callback) { - var sets = pids.map(function(pid) { - return 'pid:' + pid + ':upvote'; - }); - db.getSetsMembers(sets, callback); - }; - - -}(exports)); +}; diff --git a/src/privileges.js b/src/privileges.js index 5c12365597..d933d72877 100644 --- a/src/privileges.js +++ b/src/privileges.js @@ -1,10 +1,40 @@ "use strict"; -var privileges = {}; +var privileges = module.exports; + +privileges.userPrivilegeList = [ + 'find', + 'read', + 'topics:read', + 'topics:create', + 'topics:reply', + 'posts:edit', + 'posts:delete', + 'topics:delete', + 'upload:post:image', + 'upload:post:file', + 'purge', + 'mods' +]; + +privileges.groupPrivilegeList = [ + 'groups:find', + 'groups:read', + 'groups:topics:read', + 'groups:topics:create', + 'groups:topics:reply', + 'groups:posts:edit', + 'groups:posts:delete', + 'groups:topics:delete', + 'groups:upload:post:image', + 'groups:upload:post:file', + 'groups:purge', + 'groups:moderate' +]; + +privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList); require('./privileges/categories')(privileges); require('./privileges/topics')(privileges); require('./privileges/posts')(privileges); require('./privileges/users')(privileges); - -module.exports = privileges; \ No newline at end of file diff --git a/src/privileges/categories.js b/src/privileges/categories.js index d51edbb9a8..717551e99f 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -1,74 +1,73 @@ 'use strict'; -var async = require('async'), - _ = require('underscore'), +var async = require('async'); +var _ = require('underscore'); - user = require('../user'), - categories = require('../categories'), - groups = require('../groups'), - helpers = require('./helpers'), - plugins = require('../plugins'); +var categories = require('../categories'); +var user = require('../user'); +var groups = require('../groups'); +var helpers = require('./helpers'); +var plugins = require('../plugins'); -module.exports = function(privileges) { +module.exports = function (privileges) { privileges.categories = {}; - privileges.categories.list = function(cid, callback) { + privileges.categories.list = function (cid, callback) { // Method used in admin/category controller to show all users/groups with privs in that given cid var privilegeLabels = [ - {name: 'Find category'}, - {name: 'Access & Read'}, + {name: 'Find Category'}, + {name: 'Access Category'}, + {name: 'Access Topics'}, {name: 'Create Topics'}, {name: 'Reply to Topics'}, + {name: 'Edit Posts'}, + {name: 'Delete Posts'}, + {name: 'Delete Topics'}, + {name: 'Upload Images'}, + {name: 'Upload Files'}, {name: 'Purge'}, {name: 'Moderate'} ]; - var userPrivilegeList = [ - 'find', 'read', 'topics:create', 'topics:reply', 'purge', 'mods' - ]; - var groupPrivilegeList = [ - 'groups:find', 'groups:read', 'groups:topics:create', 'groups:topics:reply', 'groups:purge', 'groups:moderate' - ]; - async.parallel({ - labels: function(next) { + labels: function (next) { async.parallel({ users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privilegeLabels), groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privilegeLabels) }, next); }, - users: function(next) { - var privileges; + users: function (next) { + var userPrivileges; async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.list', userPrivilegeList), - function(privs, next) { - privileges = privs; - groups.getMembersOfGroups(privs.map(function(privilege) { + async.apply(plugins.fireHook, 'filter:privileges.list', privileges.userPrivilegeList), + function (_privs, next) { + userPrivileges = _privs; + groups.getMembersOfGroups(userPrivileges.map(function (privilege) { return 'cid:' + cid + ':privileges:' + privilege; }), next); }, - function(memberSets, next) { + function (memberSets, next) { - memberSets = memberSets.map(function(set) { - return set.map(function(uid) { + memberSets = memberSets.map(function (set) { + return set.map(function (uid) { return parseInt(uid, 10); }); }); var members = _.unique(_.flatten(memberSets)); - user.getUsersFields(members, ['picture', 'username'], function(err, memberData) { + user.getUsersFields(members, ['picture', 'username'], function (err, memberData) { if (err) { return next(err); } - memberData.forEach(function(member) { + memberData.forEach(function (member) { member.privileges = {}; - for(var x=0,numPrivs=privileges.length;x<numPrivs;x++) { - member.privileges[privileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1; + for(var x = 0,numPrivs = userPrivileges.length;x < numPrivs;x++) { + member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1; } }); @@ -77,26 +76,26 @@ module.exports = function(privileges) { } ], next); }, - groups: function(next) { - var privileges; + groups: function (next) { + var groupPrivileges; async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.groups.list', groupPrivilegeList), - function(privs, next) { - privileges = privs; - groups.getMembersOfGroups(privs.map(function(privilege) { + async.apply(plugins.fireHook, 'filter:privileges.groups.list', privileges.groupPrivilegeList), + function (_privs, next) { + groupPrivileges = _privs; + groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { return 'cid:' + cid + ':privileges:' + privilege; }), next); }, - function(memberSets, next) { + function (memberSets, next) { var uniqueGroups = _.unique(_.flatten(memberSets)); - groups.getGroups('groups:createtime', 0, -1, function(err, groupNames) { + groups.getGroups('groups:createtime', 0, -1, function (err, groupNames) { if (err) { return next(err); } - groupNames = groupNames.filter(function(groupName) { + groupNames = groupNames.filter(function (groupName) { return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1; }); @@ -115,11 +114,11 @@ module.exports = function(privileges) { var memberPrivs; - var memberData = groupNames.map(function(member) { + var memberData = groupNames.map(function (member) { memberPrivs = {}; - for(var x=0,numPrivs=privileges.length;x<numPrivs;x++) { - memberPrivs[privileges[x]] = memberSets[x].indexOf(member) !== -1; + for(var x = 0,numPrivs = groupPrivileges.length;x < numPrivs;x++) { + memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1; } return { name: member, @@ -130,10 +129,10 @@ module.exports = function(privileges) { next(null, memberData); }); }, - function(memberData, next) { + function (memberData, next) { // Grab privacy info for the groups as well - async.map(memberData, function(member, next) { - groups.isPrivate(member.name, function(err, isPrivate) { + async.map(memberData, function (member, next) { + groups.isPrivate(member.name, function (err, isPrivate) { if (err) { return next(err); } @@ -145,7 +144,7 @@ module.exports = function(privileges) { } ], next); } - }, function(err, payload) { + }, function (err, payload) { if (err) { return callback(err); } @@ -157,40 +156,39 @@ module.exports = function(privileges) { }); }; - privileges.categories.get = function(cid, uid, callback) { + privileges.categories.get = function (cid, uid, callback) { + var privs = ['topics:create', 'topics:read', 'read']; async.parallel({ - 'topics:create': function(next) { - helpers.isUserAllowedTo('topics:create', uid, [cid], next); + privileges: function (next) { + helpers.isUserAllowedTo(privs, uid, cid, next); }, - read: function(next) { - helpers.isUserAllowedTo('read', uid, [cid], next); - }, - isAdministrator: function(next) { + isAdministrator: function (next) { user.isAdministrator(uid, next); }, - isModerator: function(next) { + isModerator: function (next) { user.isModerator(uid, cid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - + var privData = _.object(privs, results.privileges); var isAdminOrMod = results.isAdministrator || results.isModerator; plugins.fireHook('filter:privileges.categories.get', { + 'topics:create': privData['topics:create'] || isAdminOrMod, + 'topics:read': privData['topics:read'] || isAdminOrMod, + read: privData.read || isAdminOrMod, cid: cid, uid: uid, - 'topics:create': results['topics:create'][0] || isAdminOrMod, editable: isAdminOrMod, view_deleted: isAdminOrMod, - read: results.read[0] || isAdminOrMod, isAdminOrMod: isAdminOrMod }, callback); }); }; - privileges.categories.isAdminOrMod = function(cid, uid, callback) { + privileges.categories.isAdminOrMod = function (cid, uid, callback) { if (!parseInt(uid, 10)) { return callback(null, false); } @@ -204,20 +202,21 @@ module.exports = function(privileges) { ], callback); }; - privileges.categories.isUserAllowedTo = function(privilege, cid, uid, callback) { + privileges.categories.isUserAllowedTo = function (privilege, cid, uid, callback) { if (!cid) { return callback(null, false); } - helpers.isUserAllowedTo(privilege, uid, [cid], function(err, results) { + helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) { callback(err, Array.isArray(results) && results.length ? results[0] : false); }); }; - privileges.categories.can = function(privilege, cid, uid, callback) { + privileges.categories.can = function (privilege, cid, uid, callback) { if (!cid) { return callback(null, false); } - categories.getCategoryField(cid, 'disabled', function(err, disabled) { + + categories.getCategoryField(cid, 'disabled', function (err, disabled) { if (err) { return callback(err); } @@ -227,49 +226,36 @@ module.exports = function(privileges) { } helpers.some([ - function(next) { - helpers.isUserAllowedTo(privilege, uid, [cid], function(err, results) { + function (next) { + helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) { next(err, Array.isArray(results) && results.length ? results[0] : false); }); }, - function(next) { + function (next) { user.isModerator(uid, cid, next); }, - function(next) { + function (next) { user.isAdministrator(uid, next); } ], callback); }); }; - privileges.categories.filterCids = function(privilege, cids, uid, callback) { + privileges.categories.filterCids = function (privilege, cids, uid, callback) { if (!Array.isArray(cids) || !cids.length) { return callback(null, []); } - cids = cids.filter(function(cid, index, array) { + cids = cids.filter(function (cid, index, array) { return array.indexOf(cid) === index; }); - async.parallel({ - categories: function(next) { - categories.getCategoriesFields(cids, ['disabled'], next); - }, - allowedTo: function(next) { - helpers.isUserAllowedTo(privilege, uid, cids, next); - }, - isModerators: function(next) { - user.isModerator(uid, cids, next); - }, - isAdmin: function(next) { - user.isAdministrator(uid, next); - } - }, function(err, results) { + privileges.categories.getBase(privilege, cids, uid, function (err, results) { if (err) { return callback(err); } - cids = cids.filter(function(cid, index) { + cids = cids.filter(function (cid, index) { return !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); }); @@ -278,63 +264,80 @@ module.exports = function(privileges) { }); }; - privileges.categories.filterUids = function(privilege, cid, uids, callback) { + privileges.categories.getBase = function (privilege, cids, uid, callback) { + async.parallel({ + categories: function (next) { + categories.getCategoriesFields(cids, ['disabled'], next); + }, + allowedTo: function (next) { + helpers.isUserAllowedTo(privilege, uid, cids, next); + }, + isModerators: function (next) { + user.isModerator(uid, cids, next); + }, + isAdmin: function (next) { + user.isAdministrator(uid, next); + } + }, callback); + }; + + privileges.categories.filterUids = function (privilege, cid, uids, callback) { if (!uids.length) { return callback(null, []); } - uids = uids.filter(function(uid, index, array) { + uids = uids.filter(function (uid, index, array) { return array.indexOf(uid) === index; }); async.parallel({ - allowedTo: function(next) { + allowedTo: function (next) { helpers.isUsersAllowedTo(privilege, uids, cid, next); }, - isModerators: function(next) { + isModerators: function (next) { user.isModerator(uids, cid, next); }, - isAdmin: function(next) { + isAdmin: function (next) { user.isAdministrator(uids, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - uids = uids.filter(function(uid, index) { + uids = uids.filter(function (uid, index) { return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index]; }); callback(null, uids); }); }; - privileges.categories.give = function(privileges, cid, groupName, callback) { + privileges.categories.give = function (privileges, cid, groupName, callback) { giveOrRescind(groups.join, privileges, cid, groupName, callback); }; - privileges.categories.rescind = function(privileges, cid, groupName, callback) { + privileges.categories.rescind = function (privileges, cid, groupName, callback) { giveOrRescind(groups.leave, privileges, cid, groupName, callback); }; function giveOrRescind(method, privileges, cid, groupName, callback) { - async.each(privileges, function(privilege, next) { + async.each(privileges, function (privilege, next) { method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next); }, callback); } - privileges.categories.canMoveAllTopics = function(currentCid, targetCid, uid, callback) { + privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) { async.parallel({ - isAdministrator: function(next) { + isAdministrator: function (next) { user.isAdministrator(uid, next); }, - moderatorOfCurrent: function(next) { + moderatorOfCurrent: function (next) { user.isModerator(uid, currentCid, next); }, - moderatorOfTarget: function(next) { + moderatorOfTarget: function (next) { user.isModerator(uid, targetCid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -343,37 +346,61 @@ module.exports = function(privileges) { }); }; - privileges.categories.userPrivileges = function(cid, uid, callback) { + privileges.categories.userPrivileges = function (cid, uid, callback) { async.parallel({ find: async.apply(groups.isMember, uid, 'cid:' + cid + ':privileges:find'), - read: function(next) { + read: function (next) { groups.isMember(uid, 'cid:' + cid + ':privileges:read', next); }, - 'topics:create': function(next) { + 'topics:create': function (next) { groups.isMember(uid, 'cid:' + cid + ':privileges:topics:create', next); }, - 'topics:reply': function(next) { + 'topics:read': function (next) { + groups.isMember(uid, 'cid:' + cid + ':privileges:topics:read', next); + }, + 'topics:reply': function (next) { groups.isMember(uid, 'cid:' + cid + ':privileges:topics:reply', next); }, - mods: function(next) { + 'posts:edit': function (next) { + groups.isMember(uid, 'cid:' + cid + ':privileges:posts:edit', next); + }, + 'posts:delete': function (next) { + groups.isMember(uid, 'cid:' + cid + ':privileges:posts:delete', next); + }, + 'topics:delete': function (next) { + groups.isMember(uid, 'cid:' + cid + ':privileges:topics:delete', next); + }, + mods: function (next) { user.isModerator(uid, cid, next); } }, callback); }; - privileges.categories.groupPrivileges = function(cid, groupName, callback) { + privileges.categories.groupPrivileges = function (cid, groupName, callback) { async.parallel({ 'groups:find': async.apply(groups.isMember, groupName, 'cid:' + cid + ':privileges:groups:find'), - 'groups:read': function(next) { + 'groups:read': function (next) { groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:read', next); }, - 'groups:topics:create': function(next) { + 'groups:topics:create': function (next) { groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:topics:create', next); }, - 'groups:topics:reply': function(next) { + 'groups:topics:reply': function (next) { groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:topics:reply', next); + }, + 'groups:posts:edit': function (next) { + groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:posts:edit', next); + }, + 'groups:posts:delete': function (next) { + groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:posts:delete', next); + }, + 'groups:topics:delete': function (next) { + groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:topics:delete', next); + }, + 'groups:topics:read': function (next) { + groups.isMember(groupName, 'cid:' + cid + ':privileges:groups:topics:read', next); } }, callback); }; -}; +}; \ No newline at end of file diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 7b5cb0c561..9df9d8cd7b 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -6,63 +6,106 @@ var groups = require('../groups'); var helpers = {}; -helpers.some = function(tasks, callback) { - async.some(tasks, function(task, next) { - task(function(err, result) { +helpers.some = function (tasks, callback) { + async.some(tasks, function (task, next) { + task(function (err, result) { next(!err && result); }); - }, function(result) { + }, function (result) { callback(null, result); }); }; -helpers.isUserAllowedTo = function(privilege, uid, cids, callback) { +helpers.isUserAllowedTo = function (privilege, uid, cid, callback) { + if (Array.isArray(privilege) && !Array.isArray(cid)) { + isUserAllowedToPrivileges(privilege, uid, cid, callback); + } else if (Array.isArray(cid) && !Array.isArray(privilege)) { + isUserAllowedToCids(privilege, uid, cid, callback); + } else { + return callback(new Error('[[error:invalid-data]]')); + } +}; + +function isUserAllowedToCids(privilege, uid, cids, callback) { if (parseInt(uid, 10) === 0) { - return isGuestAllowedTo(privilege, cids, callback); + return isGuestAllowedToCids(privilege, cids, callback); } var userKeys = [], groupKeys = []; - for (var i=0; i<cids.length; ++i) { + for (var i = 0; i < cids.length; ++i) { userKeys.push('cid:' + cids[i] + ':privileges:' + privilege); groupKeys.push('cid:' + cids[i] + ':privileges:groups:' + privilege); } async.parallel({ - hasUserPrivilege: function(next) { + hasUserPrivilege: function (next) { groups.isMemberOfGroups(uid, userKeys, next); }, - hasGroupPrivilege: function(next) { + hasGroupPrivilege: function (next) { groups.isMemberOfGroupsList(uid, groupKeys, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var result = []; - for (var i=0; i<cids.length; ++i) { + for (var i = 0; i < cids.length; ++i) { result.push(results.hasUserPrivilege[i] || results.hasGroupPrivilege[i]); } callback(null, result); }); -}; +} + +function isUserAllowedToPrivileges(privileges, uid, cid, callback) { + if (parseInt(uid, 10) === 0) { + return isGuestAllowedToPrivileges(privileges, cid, callback); + } + + var userKeys = [], groupKeys = []; + for (var i = 0; i < privileges.length; ++i) { + userKeys.push('cid:' + cid + ':privileges:' + privileges[i]); + groupKeys.push('cid:' + cid + ':privileges:groups:' + privileges[i]); + } + + async.parallel({ + hasUserPrivilege: function (next) { + groups.isMemberOfGroups(uid, userKeys, next); + }, + hasGroupPrivilege: function (next) { + groups.isMemberOfGroupsList(uid, groupKeys, next); + } + }, function (err, results) { + if (err) { + return callback(err); + } -helpers.isUsersAllowedTo = function(privilege, uids, cid, callback) { + var result = []; + for (var i = 0; i < privileges.length; ++i) { + result.push(results.hasUserPrivilege[i] || results.hasGroupPrivilege[i]); + } + + callback(null, result); + }); +} + + +helpers.isUsersAllowedTo = function (privilege, uids, cid, callback) { async.parallel({ - hasUserPrivilege: function(next) { + hasUserPrivilege: function (next) { groups.isMembers(uids, 'cid:' + cid + ':privileges:' + privilege, next); }, - hasGroupPrivilege: function(next) { + hasGroupPrivilege: function (next) { groups.isMembersOfGroupList(uids, 'cid:' + cid + ':privileges:groups:' + privilege, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var result = []; - for(var i=0; i<uids.length; ++i) { + for(var i = 0; i < uids.length; ++i) { result.push(results.hasUserPrivilege[i] || results.hasGroupPrivilege[i]); } @@ -70,14 +113,22 @@ helpers.isUsersAllowedTo = function(privilege, uids, cid, callback) { }); }; -function isGuestAllowedTo(privilege, cids, callback) { +function isGuestAllowedToCids(privilege, cids, callback) { var groupKeys = []; - for (var i=0; i<cids.length; ++i) { + for (var i = 0; i < cids.length; ++i) { groupKeys.push('cid:' + cids[i] + ':privileges:groups:' + privilege); } groups.isMemberOfGroups('guests', groupKeys, callback); } +function isGuestAllowedToPrivileges(privileges, cid, callback) { + var groupKeys = []; + for (var i = 0; i < privileges.length; ++i) { + groupKeys.push('cid:' + cid + ':privileges:groups:' + privileges[i]); + } + + groups.isMemberOfGroups('guests', groupKeys, callback); +} module.exports = helpers; \ No newline at end of file diff --git a/src/privileges/posts.js b/src/privileges/posts.js index 8291fcd4e4..99a193e01d 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -10,38 +10,47 @@ var user = require('../user'); var helpers = require('./helpers'); var plugins = require('../plugins'); -module.exports = function(privileges) { +module.exports = function (privileges) { privileges.posts = {}; - privileges.posts.get = function(pids, uid, callback) { + privileges.posts.get = function (pids, uid, callback) { if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } - async.parallel({ - isAdmin: function(next){ - user.isAdministrator(uid, next); - }, - isModerator: function(next) { - posts.isModerator(pids, uid, next); + async.waterfall([ + function (next) { + posts.getCidsByPids(pids, next); }, - isOwner: function(next) { - posts.isOwner(pids, uid, next); + function (cids, next) { + async.parallel({ + isAdmin: async.apply(user.isAdministrator, uid), + isModerator: async.apply(posts.isModerator, pids, uid), + isOwner: async.apply(posts.isOwner, pids, uid), + 'topics:read': async.apply(helpers.isUserAllowedTo, 'topics:read', uid, cids), + read: async.apply(helpers.isUserAllowedTo, 'read', uid, cids), + 'posts:edit': async.apply(helpers.isUserAllowedTo, 'posts:edit', uid, cids), + }, next); } - }, function(err, results) { + ], function (err, results) { if (err) { return callback(err); } var privileges = []; - for (var i=0; i<pids.length; ++i) { - var editable = results.isAdmin || results.isModerator[i] || results.isOwner[i]; + for (var i = 0; i < pids.length; ++i) { + var isAdminOrMod = results.isAdmin || results.isModerator[i]; + var editable = isAdminOrMod || (results.isOwner[i] && results['posts:edit'][i]); + privileges.push({ editable: editable, view_deleted: editable, - move: results.isAdmin || results.isModerator[i] + move: isAdminOrMod, + isAdminOrMod: isAdminOrMod, + 'topics:read': results['topics:read'][i] || isAdminOrMod, + read: results.read[i] || isAdminOrMod }); } @@ -49,8 +58,8 @@ module.exports = function(privileges) { }); }; - privileges.posts.can = function(privilege, pid, uid, callback) { - posts.getCidByPid(pid, function(err, cid) { + privileges.posts.can = function (privilege, pid, uid, callback) { + posts.getCidByPid(pid, function (err, cid) { if (err) { return callback(err); } @@ -59,27 +68,62 @@ module.exports = function(privileges) { }); }; - privileges.posts.filter = function(privilege, pids, uid, callback) { + privileges.posts.filter = function (privilege, pids, uid, callback) { if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } - posts.getCidsByPids(pids, function(err, cids) { - if (err) { - return callback(err); - } + var cids; + var postData; + var tids; + var tidToTopic = {}; + + async.waterfall([ + function (next) { + posts.getPostsFields(pids, ['uid', 'tid', 'deleted'], next); + }, + function (_posts, next) { + postData = _posts; + tids = _posts.map(function (post) { + return post && post.tid; + }).filter(function (tid, index, array) { + return tid && array.indexOf(tid) === index; + }); + topics.getTopicsFields(tids, ['deleted', 'cid'], next); + }, + function (topicData, next) { - pids = pids.map(function(pid, index) { - return {pid: pid, cid: cids[index]}; - }); + topicData.forEach(function (topic, index) { + if (topic) { + tidToTopic[tids[index]] = topic; + } + }); - privileges.categories.filterCids(privilege, cids, uid, function(err, cids) { - if (err) { - return callback(err); - } + cids = postData.map(function (post, index) { + if (post) { + post.pid = pids[index]; + post.topic = tidToTopic[post.tid]; + } + return tidToTopic[post.tid] && tidToTopic[post.tid].cid; + }).filter(function (cid, index, array) { + return cid && array.indexOf(cid) === index; + }); - pids = pids.filter(function(post) { - return cids.indexOf(post.cid) !== -1; - }).map(function(post) { + privileges.categories.getBase(privilege, cids, uid, next); + }, + function (results, next) { + + var isModOf = {}; + cids = cids.filter(function (cid, index) { + isModOf[cid] = results.isModerators[index]; + return !results.categories[index].disabled && + (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); + }); + + + pids = postData.filter(function (post) { + return post.topic && cids.indexOf(post.topic.cid) !== -1 && + ((parseInt(post.topic.deleted, 10) !== 1 && parseInt(post.deleted, 10) !== 1) || results.isAdmin || isModOf[post.cid]); + }).map(function (post) { return post.pid; }); @@ -87,36 +131,72 @@ module.exports = function(privileges) { privilege: privilege, uid: uid, pids: pids - }, function(err, data) { - callback(err, data ? data.pids : null); + }, function (err, data) { + next(err, data ? data.pids : null); }); - }); - }); + } + ], callback); }; - privileges.posts.canEdit = function(pid, uid, callback) { + privileges.posts.canEdit = function (pid, uid, callback) { async.parallel({ isEditable: async.apply(isPostEditable, pid, uid), isAdminOrMod: async.apply(isAdminOrMod, pid, uid) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } if (results.isAdminOrMod) { - return callback(null, true); + return callback(null, {flag: true}); } - if (results.isEditable.isLocked) { - return callback(new Error('[[error:topic-locked]]')); + + callback(null, results.isEditable); + }); + }; + + privileges.posts.canDelete = function (pid, uid, callback) { + var postData; + async.waterfall([ + function (next) { + posts.getPostFields(pid, ['tid', 'timestamp'], next); + }, + function (_postData, next) { + postData = _postData; + async.parallel({ + isAdminOrMod: async.apply(isAdminOrMod, pid, uid), + isLocked: async.apply(topics.isLocked, postData.tid), + isOwner: async.apply(posts.isOwner, pid, uid), + 'posts:delete': async.apply(privileges.posts.can, 'posts:delete', pid, uid) + }, next); } - if (results.isEditable.isEditExpired) { - return callback(new Error('[[error:post-edit-duration-expired, ' + meta.config.postEditDuration + ']]')); + ], function (err, results) { + if (err) { + return callback(err); } - callback(null, results.isEditable.editable); + + if (results.isAdminOrMod) { + return callback(null, {flag: true}); + } + + if (results.isLocked) { + return callback(null, {flag: false, message: '[[error:topic-locked]]'}); + } + + if (!results['posts:delete']) { + return callback(null, {flag: false, message: '[[error:no-privileges]]'}); + } + + var postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10); + if (postDeleteDuration && (Date.now() - parseInt(postData.timestamp, 10) > postDeleteDuration * 1000)) { + return callback(null, {flag: false, message: '[[error:post-delete-duration-expired, ' + meta.config.postDeleteDuration + ']]'}); + } + + callback(null, {flag: results.isOwner, message: '[[error:no-privileges]]'}); }); }; - privileges.posts.canMove = function(pid, uid, callback) { - posts.isMain(pid, function(err, isMain) { + privileges.posts.canMove = function (pid, uid, callback) { + posts.isMain(pid, function (err, isMain) { if (err || isMain) { return callback(err || new Error('[[error:cant-move-mainpost]]')); } @@ -124,7 +204,7 @@ module.exports = function(privileges) { }); }; - privileges.posts.canPurge = function(pid, uid, callback) { + privileges.posts.canPurge = function (pid, uid, callback) { async.waterfall([ function (next) { posts.getCidByPid(pid, next); @@ -143,34 +223,39 @@ module.exports = function(privileges) { }; function isPostEditable(pid, uid, callback) { + var tid; async.waterfall([ - function(next) { + function (next) { posts.getPostFields(pid, ['tid', 'timestamp'], next); }, - function(postData, next) { + function (postData, next) { + tid = postData.tid; var postEditDuration = parseInt(meta.config.postEditDuration, 10); if (postEditDuration && Date.now() - parseInt(postData.timestamp, 10) > postEditDuration * 1000) { - return callback(null, {isEditExpired: true}); + return callback(null, {flag: false, message: '[[error:post-edit-duration-expired, ' + meta.config.postEditDuration + ']]'}); } topics.isLocked(postData.tid, next); }, - function(isLocked, next) { + function (isLocked, next) { if (isLocked) { - return callback(null, {isLocked: true}); + return callback(null, {flag: false, message: '[[error:topic-locked]]'}); } - posts.isOwner(pid, uid, next); + async.parallel({ + owner: async.apply(posts.isOwner, pid, uid), + edit: async.apply(privileges.posts.can, 'posts:edit', pid, uid) + }, next); }, - function(isOwner, next) { - next(null, {editable: isOwner}); + function (result, next) { + next(null, {flag: result.owner && result.edit, message: '[[error:no-privileges]]'}); } ], callback); } function isAdminOrMod(pid, uid, callback) { helpers.some([ - function(next) { - posts.getCidByPid(pid, function(err, cid) { + function (next) { + posts.getCidByPid(pid, function (err, cid) { if (err || !cid) { return next(err, false); } @@ -178,9 +263,9 @@ module.exports = function(privileges) { user.isModerator(uid, cid, next); }); }, - function(next) { + function (next) { user.isAdministrator(uid, next); } ], callback); } -}; +}; \ No newline at end of file diff --git a/src/privileges/topics.js b/src/privileges/topics.js index 83e635fde9..39ad054462 100644 --- a/src/privileges/topics.js +++ b/src/privileges/topics.js @@ -2,52 +2,58 @@ 'use strict'; var async = require('async'); +var _ = require('underscore'); +var meta = require('../meta'); var topics = require('../topics'); var user = require('../user'); var helpers = require('./helpers'); var categories = require('../categories'); var plugins = require('../plugins'); -module.exports = function(privileges) { +module.exports = function (privileges) { privileges.topics = {}; - privileges.topics.get = function(tid, uid, callback) { + privileges.topics.get = function (tid, uid, callback) { var topic; + var privs = ['topics:reply', 'topics:read', 'topics:delete', 'posts:edit', 'posts:delete', 'read']; async.waterfall([ - async.apply(topics.getTopicFields, tid, ['cid', 'uid', 'locked']), - function(_topic, next) { + async.apply(topics.getTopicFields, tid, ['cid', 'uid', 'locked', 'deleted']), + function (_topic, next) { topic = _topic; async.parallel({ - 'topics:reply': async.apply(helpers.isUserAllowedTo, 'topics:reply', uid, [topic.cid]), - read: async.apply(helpers.isUserAllowedTo, 'read', uid, [topic.cid]), - isOwner: function(next) { - next(null, parseInt(uid, 10) === parseInt(topic.uid, 10)); - }, + privileges: async.apply(helpers.isUserAllowedTo, privs, uid, topic.cid), isAdministrator: async.apply(user.isAdministrator, uid), isModerator: async.apply(user.isModerator, uid, topic.cid), disabled: async.apply(categories.getCategoryField, topic.cid, 'disabled') }, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return callback(err); } + var privData = _.object(privs, results.privileges); var disabled = parseInt(results.disabled, 10) === 1; var locked = parseInt(topic.locked, 10) === 1; + var deleted = parseInt(topic.deleted, 10) === 1; + var isOwner = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(topic.uid, 10); var isAdminOrMod = results.isAdministrator || results.isModerator; var editable = isAdminOrMod; - var deletable = isAdminOrMod || results.isOwner; + var deletable = isAdminOrMod || (isOwner && privData['topics:delete']); plugins.fireHook('filter:privileges.topics.get', { - 'topics:reply': (results['topics:reply'][0] && !locked) || isAdminOrMod, - read: results.read[0] || isAdminOrMod, + 'topics:reply': (privData['topics:reply'] && !locked && !deleted) || isAdminOrMod, + 'topics:read': privData['topics:read'] || isAdminOrMod, + 'topics:delete': (isOwner && privData['topics:delete']) || isAdminOrMod, + 'posts:edit': (privData['posts:edit'] && !locked) || isAdminOrMod, + 'posts:delete': (privData['posts:delete'] && !locked) || isAdminOrMod, + read: privData.read || isAdminOrMod, view_thread_tools: editable || deletable, editable: editable, deletable: deletable, - view_deleted: isAdminOrMod || results.isOwner, + view_deleted: isAdminOrMod || isOwner, isAdminOrMod: isAdminOrMod, disabled: disabled, tid: tid, @@ -56,8 +62,8 @@ module.exports = function(privileges) { }); }; - privileges.topics.can = function(privilege, tid, uid, callback) { - topics.getTopicField(tid, 'cid', function(err, cid) { + privileges.topics.can = function (privilege, tid, uid, callback) { + topics.getTopicField(tid, 'cid', function (err, cid) { if (err) { return callback(err); } @@ -66,98 +72,86 @@ module.exports = function(privileges) { }); }; - privileges.topics.filterTids = function(privilege, tids, uid, callback) { + privileges.topics.filterTids = function (privilege, tids, uid, callback) { if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } - + var cids; + var topicsData; async.waterfall([ - function(next) { + function (next) { topics.getTopicsFields(tids, ['tid', 'cid', 'deleted'], next); }, - function(topicsData, next) { - var cids = topicsData.map(function(topic) { + function (_topicsData, next) { + topicsData = _topicsData; + cids = topicsData.map(function (topic) { return topic.cid; - }).filter(function(cid, index, array) { + }).filter(function (cid, index, array) { return cid && array.indexOf(cid) === index; }); - async.parallel({ - categories: function(next) { - categories.getCategoriesFields(cids, ['disabled'], next); - }, - allowedTo: function(next) { - helpers.isUserAllowedTo(privilege, uid, cids, next); - }, - isModerators: function(next) { - user.isModerator(uid, cids, next); - }, - isAdmin: function(next) { - user.isAdministrator(uid, next); - } - }, function(err, results) { - if (err) { - return next(err); - } - var isModOf = {}; - cids = cids.filter(function(cid, index) { - isModOf[cid] = results.isModerators[index]; - return !results.categories[index].disabled && - (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); - }); + privileges.categories.getBase(privilege, cids, uid, next); + }, + function (results, next) { - tids = topicsData.filter(function(topic) { - return cids.indexOf(topic.cid) !== -1 && - (parseInt(topic.deleted, 10) !== 1 || results.isAdmin || isModOf[topic.cid]); - }).map(function(topic) { - return topic.tid; - }); + var isModOf = {}; + cids = cids.filter(function (cid, index) { + isModOf[cid] = results.isModerators[index]; + return !results.categories[index].disabled && + (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); + }); - plugins.fireHook('filter:privileges.topics.filter', { - privilege: privilege, - uid: uid, - tids: tids - }, function(err, data) { - next(err, data ? data.tids : null); - }); + tids = topicsData.filter(function (topic) { + return cids.indexOf(topic.cid) !== -1 && + (parseInt(topic.deleted, 10) !== 1 || results.isAdmin || isModOf[topic.cid]); + }).map(function (topic) { + return topic.tid; + }); + + plugins.fireHook('filter:privileges.topics.filter', { + privilege: privilege, + uid: uid, + tids: tids + }, function (err, data) { + next(err, data ? data.tids : null); }); } ], callback); }; - privileges.topics.filterUids = function(privilege, tid, uids, callback) { + privileges.topics.filterUids = function (privilege, tid, uids, callback) { if (!Array.isArray(uids) || !uids.length) { return callback(null, []); } - uids = uids.filter(function(uid, index, array) { + uids = uids.filter(function (uid, index, array) { return array.indexOf(uid) === index; }); async.waterfall([ - function(next) { + function (next) { topics.getTopicFields(tid, ['tid', 'cid', 'deleted'], next); }, - function(topicData, next) { + function (topicData, next) { async.parallel({ - disabled: function(next) { + disabled: function (next) { categories.getCategoryField(topicData.cid, 'disabled', next); }, - allowedTo: function(next) { + allowedTo: function (next) { helpers.isUsersAllowedTo(privilege, uids, topicData.cid, next); }, - isModerators: function(next) { + isModerators: function (next) { user.isModerator(uids, topicData.cid, next); }, - isAdmins: function(next) { + isAdmins: function (next) { user.isAdministrator(uids, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } - uids = uids.filter(function(uid, index) { + uids = uids.filter(function (uid, index) { return parseInt(results.disabled, 10) !== 1 && ((results.allowedTo[index] && parseInt(topicData.deleted, 10) !== 1) || results.isAdmins[index] || results.isModerators[index]); }); @@ -168,7 +162,7 @@ module.exports = function(privileges) { ], callback); }; - privileges.topics.canPurge = function(tid, uid, callback) { + privileges.topics.canPurge = function (tid, uid, callback) { async.waterfall([ function (next) { topics.getTopicField(tid, 'cid', next); @@ -186,35 +180,75 @@ module.exports = function(privileges) { ], callback); }; - privileges.topics.canEdit = function(tid, uid, callback) { + privileges.topics.canDelete = function (tid, uid, callback) { + var topicData; + async.waterfall([ + function (next) { + topics.getTopicFields(tid, ['cid', 'postcount'], next); + }, + function (_topicData, next) { + topicData = _topicData; + async.parallel({ + isModerator: async.apply(user.isModerator, uid, topicData.cid), + isAdministrator: async.apply(user.isAdministrator, uid), + isOwner: async.apply(topics.isOwner, tid, uid), + 'topics:delete': async.apply(helpers.isUserAllowedTo, 'topics:delete', uid, [topicData.cid]) + }, next); + } + ], function (err, results) { + if (err) { + return callback(err); + } + + if (results.isModerator || results.isAdministrator) { + return callback(null, true); + } + + var preventTopicDeleteAfterReplies = parseInt(meta.config.preventTopicDeleteAfterReplies, 10) || 0; + if (preventTopicDeleteAfterReplies && (topicData.postcount - 1) >= preventTopicDeleteAfterReplies) { + var langKey = preventTopicDeleteAfterReplies > 1 ? + '[[error:cant-delete-topic-has-replies, ' + meta.config.preventTopicDeleteAfterReplies + ']]' : + '[[error:cant-delete-topic-has-reply]]'; + return callback(new Error(langKey)); + } + + if (!results['topics:delete'][0]) { + return callback(null, false); + } + + callback(null, results.isOwner); + }); + }; + + privileges.topics.canEdit = function (tid, uid, callback) { privileges.topics.isOwnerOrAdminOrMod(tid, uid, callback); }; - privileges.topics.isOwnerOrAdminOrMod = function(tid, uid, callback) { + privileges.topics.isOwnerOrAdminOrMod = function (tid, uid, callback) { helpers.some([ - function(next) { + function (next) { topics.isOwner(tid, uid, next); }, - function(next) { + function (next) { privileges.topics.isAdminOrMod(tid, uid, next); } ], callback); }; - privileges.topics.isAdminOrMod = function(tid, uid, callback) { + privileges.topics.isAdminOrMod = function (tid, uid, callback) { helpers.some([ - function(next) { - topics.getTopicField(tid, 'cid', function(err, cid) { + function (next) { + topics.getTopicField(tid, 'cid', function (err, cid) { if (err) { return next(err); } user.isModerator(uid, cid, next); }); }, - function(next) { + function (next) { user.isAdministrator(uid, next); } ], callback); }; -}; +}; \ No newline at end of file diff --git a/src/privileges/users.js b/src/privileges/users.js index 4ee2d69461..4f6341be9f 100644 --- a/src/privileges/users.js +++ b/src/privileges/users.js @@ -6,11 +6,11 @@ var async = require('async'); var groups = require('../groups'); var plugins = require('../plugins'); -module.exports = function(privileges) { +module.exports = function (privileges) { privileges.users = {}; - privileges.users.isAdministrator = function(uid, callback) { + privileges.users.isAdministrator = function (uid, callback) { if (Array.isArray(uid)) { groups.isMembers(uid, 'administrators', callback); } else { @@ -18,7 +18,7 @@ module.exports = function(privileges) { } }; - privileges.users.isGlobalModerator = function(uid, callback) { + privileges.users.isGlobalModerator = function (uid, callback) { if (Array.isArray(uid)) { groups.isMembers(uid, 'Global Moderators', callback); } else { @@ -26,7 +26,7 @@ module.exports = function(privileges) { } }; - privileges.users.isModerator = function(uid, cid, callback) { + privileges.users.isModerator = function (uid, cid, callback) { if (Array.isArray(cid)) { isModeratorOfCategories(cid, uid, callback); } else { @@ -40,48 +40,48 @@ module.exports = function(privileges) { function isModeratorOfCategories(cids, uid, callback) { if (!parseInt(uid, 10)) { - return filterIsModerator(cids, uid, cids.map(function() {return false;}), callback); + return filterIsModerator(cids, uid, cids.map(function () {return false;}), callback); } - privileges.users.isGlobalModerator(uid, function(err, isGlobalModerator) { + privileges.users.isGlobalModerator(uid, function (err, isGlobalModerator) { if (err) { return callback(err); } if (isGlobalModerator) { - return filterIsModerator(cids, uid, cids.map(function() {return true;}), callback); + return filterIsModerator(cids, uid, cids.map(function () {return true;}), callback); } - var uniqueCids = cids.filter(function(cid, index, array) { + var uniqueCids = cids.filter(function (cid, index, array) { return array.indexOf(cid) === index; }); - var groupNames = uniqueCids.map(function(cid) { + var groupNames = uniqueCids.map(function (cid) { return 'cid:' + cid + ':privileges:mods'; // At some point we should *probably* change this to "moderate" as well }); - var groupListNames = uniqueCids.map(function(cid) { + var groupListNames = uniqueCids.map(function (cid) { return 'cid:' + cid + ':privileges:groups:moderate'; }); async.parallel({ user: async.apply(groups.isMemberOfGroups, uid, groupNames), group: async.apply(groups.isMemberOfGroupsList, uid, groupListNames) - }, function(err, checks) { + }, function (err, checks) { if (err) { return callback(err); } - var isMembers = checks.user.map(function(isMember, idx) { + var isMembers = checks.user.map(function (isMember, idx) { return isMember || checks.group[idx]; }), map = {}; - uniqueCids.forEach(function(cid, index) { + uniqueCids.forEach(function (cid, index) { map[cid] = isMembers[index]; }); - var isModerator = cids.map(function(cid) { + var isModerator = cids.map(function (cid) { return map[cid]; }); @@ -95,12 +95,12 @@ module.exports = function(privileges) { async.apply(privileges.users.isGlobalModerator, uids), async.apply(groups.isMembers, uids, 'cid:' + cid + ':privileges:mods'), async.apply(groups.isMembersOfGroupList, uids, 'cid:' + cid + ':privileges:groups:moderate') - ], function(err, checks) { + ], function (err, checks) { if (err) { return callback(err); } - var isModerator = checks[0].map(function(isMember, idx) { + var isModerator = checks[0].map(function (isMember, idx) { return isMember || checks[1][idx] || checks[2][idx]; }); @@ -113,7 +113,7 @@ module.exports = function(privileges) { async.apply(privileges.users.isGlobalModerator, uid), async.apply(groups.isMember, uid, 'cid:' + cid + ':privileges:mods'), async.apply(groups.isMemberOfGroupList, uid, 'cid:' + cid + ':privileges:groups:moderate') - ], function(err, checks) { + ], function (err, checks) { if (err) { return callback(err); } @@ -124,7 +124,7 @@ module.exports = function(privileges) { } function filterIsModerator(cid, uid, isModerator, callback) { - plugins.fireHook('filter:user.isModerator', {uid: uid, cid: cid, isModerator: isModerator}, function(err, data) { + plugins.fireHook('filter:user.isModerator', {uid: uid, cid: cid, isModerator: isModerator}, function (err, data) { if (err) { return callback(err); } @@ -136,4 +136,4 @@ module.exports = function(privileges) { }); } -}; +}; \ No newline at end of file diff --git a/src/pubsub.js b/src/pubsub.js index 82414a5c31..a2e11746e1 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -8,7 +8,7 @@ var nconf = require('nconf'), var channelName; -var PubSub = function() { +var PubSub = function () { var self = this; if (nconf.get('redis')) { var redis = require('./database/redis'); @@ -18,7 +18,7 @@ var PubSub = function() { channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel'; subClient.subscribe(channelName); - subClient.on('message', function(channel, message) { + subClient.on('message', function (channel, message) { if (channel !== channelName) { return; } @@ -35,7 +35,7 @@ var PubSub = function() { util.inherits(PubSub, EventEmitter); -PubSub.prototype.publish = function(event, data) { +PubSub.prototype.publish = function (event, data) { if (this.pubClient) { this.pubClient.publish(channelName, JSON.stringify({event: event, data: data})); } else { diff --git a/src/reset.js b/src/reset.js index f1e27738e8..f38a2be4fd 100644 --- a/src/reset.js +++ b/src/reset.js @@ -8,8 +8,8 @@ var db = require('./database'); var Reset = {}; -Reset.reset = function() { - db.init(function(err) { +Reset.reset = function () { + db.init(function (err) { if (err) { winston.error(err.message); process.exit(); @@ -32,7 +32,7 @@ Reset.reset = function() { } else if (nconf.get('s')) { resetSettings(); } else if (nconf.get('a')) { - require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function(err) { + require('async').series([resetWidgets, resetThemes, resetPlugins, resetSettings], function (err) { if (!err) { winston.info('[reset] Reset complete.'); } else { @@ -59,7 +59,7 @@ Reset.reset = function() { function resetSettings(callback) { var meta = require('./meta'); - meta.configs.set('allowLocalLogin', 1, function(err) { + meta.configs.set('allowLocalLogin', 1, function (err) { winston.info('[reset] Settings reset to default'); if (typeof callback === 'function') { callback(err); @@ -73,7 +73,7 @@ function resetTheme(themeId) { var meta = require('./meta'); var fs = require('fs'); - fs.access('node_modules/' + themeId + '/package.json', function(err, fd) { + fs.access('node_modules/' + themeId + '/package.json', function (err, fd) { if (err) { winston.warn('[reset] Theme `%s` is not installed on this forum', themeId); process.exit(); @@ -81,8 +81,13 @@ function resetTheme(themeId) { meta.themes.set({ type: 'local', id: themeId - }, function(err) { - winston.info('[reset] Theme reset to ' + themeId); + }, function (err) { + if (err) { + winston.warn('[reset] Failed to reset theme to ' + themeId); + } else { + winston.info('[reset] Theme reset to ' + themeId); + } + process.exit(); }); } @@ -95,7 +100,7 @@ function resetThemes(callback) { meta.themes.set({ type: 'local', id: 'nodebb-theme-persona' - }, function(err) { + }, function (err) { winston.info('[reset] Theme reset to Persona'); if (typeof callback === 'function') { callback(err); @@ -110,7 +115,7 @@ function resetPlugin(pluginId) { async.waterfall([ async.apply(db.isSortedSetMember, 'plugins:active', pluginId), - function(isMember, next) { + function (isMember, next) { active = isMember; if (isMember) { @@ -119,7 +124,7 @@ function resetPlugin(pluginId) { next(); } } - ], function(err) { + ], function (err) { if (err) { winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message); } else { @@ -136,7 +141,7 @@ function resetPlugin(pluginId) { } function resetPlugins(callback) { - db.delete('plugins:active', function(err) { + db.delete('plugins:active', function (err) { winston.info('[reset] All Plugins De-activated'); if (typeof callback === 'function') { callback(err); @@ -147,7 +152,7 @@ function resetPlugins(callback) { } function resetWidgets(callback) { - require('./widgets').reset(function(err) { + require('./widgets').reset(function (err) { winston.info('[reset] All Widgets moved to Draft Zone'); if (typeof callback === 'function') { callback(err); diff --git a/src/rewards/admin.js b/src/rewards/admin.js index e6c32bc34b..fcfacf5877 100644 --- a/src/rewards/admin.js +++ b/src/rewards/admin.js @@ -6,7 +6,7 @@ var rewards = {}, db = require('../database'); -rewards.save = function(data, callback) { +rewards.save = function (data, callback) { function save(data, next) { function commit(err, id) { if (err) { @@ -16,16 +16,16 @@ rewards.save = function(data, callback) { data.id = id; async.series([ - function(next) { + function (next) { rewards.delete(data, next); }, - function(next) { + function (next) { db.setAdd('rewards:list', data.id, next); }, - function(next) { + function (next) { db.setObject('rewards:id:' + data.id, data, next); }, - function(next) { + function (next) { db.setObject('rewards:id:' + data.id + ':rewards', rewardsData, next); } ], next); @@ -45,42 +45,46 @@ rewards.save = function(data, callback) { } } - async.each(data, save, function(err) { + async.each(data, save, function (err) { + if (err) { + return callback(err); + } + saveConditions(data, callback); }); }; -rewards.delete = function(data, callback) { +rewards.delete = function (data, callback) { async.parallel([ - function(next) { + function (next) { db.setRemove('rewards:list', data.id, next); }, - function(next) { + function (next) { db.delete('rewards:id:' + data.id, next); }, - function(next) { + function (next) { db.delete('rewards:id:' + data.id + ':rewards', next); } ], callback); }; -rewards.get = function(callback) { +rewards.get = function (callback) { async.parallel({ active: getActiveRewards, - conditions: function(next) { + conditions: function (next) { plugins.fireHook('filter:rewards.conditions', [], next); }, - conditionals: function(next) { + conditionals: function (next) { plugins.fireHook('filter:rewards.conditionals', [], next); }, - rewards: function(next) { + rewards: function (next) { plugins.fireHook('filter:rewards.rewards', [], next); } }, callback); }; function saveConditions(data, callback) { - db.delete('conditions:active', function(err) { + db.delete('conditions:active', function (err) { if (err) { return callback(err); } @@ -88,7 +92,7 @@ function saveConditions(data, callback) { var conditions = [], rewardsPerCondition = {}; - data.forEach(function(reward) { + data.forEach(function (reward) { conditions.push(reward.condition); rewardsPerCondition[reward.condition] = rewardsPerCondition[reward.condition] || []; rewardsPerCondition[reward.condition].push(reward.id); @@ -96,7 +100,7 @@ function saveConditions(data, callback) { db.setAdd('conditions:active', conditions, callback); - async.each(Object.keys(rewardsPerCondition), function(condition, next) { + async.each(Object.keys(rewardsPerCondition), function (condition, next) { db.setAdd('condition:' + condition + ':rewards', rewardsPerCondition[condition], next); }, callback); }); @@ -107,13 +111,13 @@ function getActiveRewards(callback) { function load(id, next) { async.parallel({ - main: function(next) { + main: function (next) { db.getObject('rewards:id:' + id, next); }, - rewards: function(next) { + rewards: function (next) { db.getObject('rewards:id:' + id + ':rewards', next); } - }, function(err, data) { + }, function (err, data) { if (data.main) { data.main.disabled = data.main.disabled === 'true'; data.main.rewards = data.rewards; @@ -124,8 +128,12 @@ function getActiveRewards(callback) { }); } - db.getSetMembers('rewards:list', function(err, rewards) { - async.eachSeries(rewards, load, function(err) { + db.getSetMembers('rewards:list', function (err, rewards) { + if (err) { + return callback(err); + } + + async.eachSeries(rewards, load, function (err) { callback(err, activeRewards); }); }); diff --git a/src/rewards/index.js b/src/rewards/index.js index 1b46b190d9..4ba403fce8 100644 --- a/src/rewards/index.js +++ b/src/rewards/index.js @@ -6,10 +6,10 @@ var rewards = {}, async = require('async'); -rewards.checkConditionAndRewardUser = function(uid, condition, method, callback) { +rewards.checkConditionAndRewardUser = function (uid, condition, method, callback) { async.waterfall([ - function(next) { - isConditionActive(condition, function(err, isActive) { + function (next) { + isConditionActive(condition, function (err, isActive) { if (!isActive) { return back(err); } @@ -17,16 +17,16 @@ rewards.checkConditionAndRewardUser = function(uid, condition, method, callback) next(err); }); }, - function(next) { - getIDsByCondition(condition, function(err, ids) { + function (next) { + getIDsByCondition(condition, function (err, ids) { next(err, ids); }); }, - function(ids, next) { + function (ids, next) { getRewardDataByIDs(ids, next); }, - function(rewards, next) { - filterCompletedRewards(uid, rewards, function(err, filtered) { + function (rewards, next) { + filterCompletedRewards(uid, rewards, function (err, filtered) { if (!filtered || !filtered.length) { return back(err); } @@ -34,14 +34,14 @@ rewards.checkConditionAndRewardUser = function(uid, condition, method, callback) next(err, filtered); }); }, - function(rewards, next) { - async.filter(rewards, function(reward, next) { + function (rewards, next) { + async.filter(rewards, function (reward, next) { if (!reward) { return next(false); } checkCondition(reward, method, next); - }, function(eligible) { + }, function (eligible) { if (!eligible) { return next(false); } @@ -68,18 +68,18 @@ function getIDsByCondition(condition, callback) { } function filterCompletedRewards(uid, rewards, callback) { - db.getSortedSetRangeByScoreWithScores('uid:' + uid + ':rewards', 0, -1, 1, '+inf', function(err, data) { + db.getSortedSetRangeByScoreWithScores('uid:' + uid + ':rewards', 0, -1, 1, '+inf', function (err, data) { if (err) { return callback(err); } var userRewards = {}; - data.forEach(function(obj) { + data.forEach(function (obj) { userRewards[obj.value] = parseInt(obj.score, 10); }); - rewards = rewards.filter(function(reward) { + rewards = rewards.filter(function (reward) { if (!reward) { return false; } @@ -90,7 +90,7 @@ function filterCompletedRewards(uid, rewards, callback) { return true; } - return (userRewards[reward.id] > reward.claimable) ? false : true; + return (userRewards[reward.id] >= reward.claimable) ? false : true; }); callback(false, rewards); @@ -98,28 +98,36 @@ function filterCompletedRewards(uid, rewards, callback) { } function getRewardDataByIDs(ids, callback) { - db.getObjects(ids.map(function(id) { + db.getObjects(ids.map(function (id) { return 'rewards:id:' + id; }), callback); } function getRewardsByRewardData(rewards, callback) { - db.getObjects(rewards.map(function(reward) { + 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); + method(function (err, value) { + if (err) { + return callback(err); + } + + plugins.fireHook('filter:rewards.checkConditional:' + reward.conditional, {left: value, right: reward.value}, function (err, bool) { + callback(err || bool); }); }); } function giveRewards(uid, rewards, callback) { - getRewardsByRewardData(rewards, function(err, rewardData) { - async.each(rewards, function(reward, next) { + getRewardsByRewardData(rewards, function (err, rewardData) { + if (err) { + return callback(err); + } + + 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); diff --git a/src/routes/accounts.js b/src/routes/accounts.js index 8c7a505d0b..7cda2228b0 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -7,6 +7,8 @@ module.exports = function (app, middleware, controllers) { var middlewares = [middleware.checkGlobalPrivacySettings]; var accountMiddlewares = [middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions]; + setupPageRoute(app, '/uid/:uid/:section?', middleware, [], middleware.redirectUidToUserslug); + setupPageRoute(app, '/user/:userslug', middleware, middlewares, controllers.accounts.profile.get); setupPageRoute(app, '/user/:userslug/following', middleware, middlewares, controllers.accounts.follow.getFollowing); setupPageRoute(app, '/user/:userslug/followers', middleware, middlewares, controllers.accounts.follow.getFollowers); @@ -15,7 +17,7 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/best', middleware, middlewares, controllers.accounts.posts.getBestPosts); setupPageRoute(app, '/user/:userslug/groups', middleware, middlewares, controllers.accounts.groups.get); - setupPageRoute(app, '/user/:userslug/favourites', middleware, accountMiddlewares, controllers.accounts.posts.getFavourites); + setupPageRoute(app, '/user/:userslug/bookmarks', middleware, accountMiddlewares, controllers.accounts.posts.getBookmarks); setupPageRoute(app, '/user/:userslug/watched', middleware, accountMiddlewares, controllers.accounts.posts.getWatchedTopics); setupPageRoute(app, '/user/:userslug/upvoted', middleware, accountMiddlewares, controllers.accounts.posts.getUpVotedPosts); setupPageRoute(app, '/user/:userslug/downvoted', middleware, accountMiddlewares, controllers.accounts.posts.getDownVotedPosts); @@ -23,10 +25,12 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/edit/username', middleware, accountMiddlewares, controllers.accounts.edit.username); setupPageRoute(app, '/user/:userslug/edit/email', middleware, accountMiddlewares, controllers.accounts.edit.email); setupPageRoute(app, '/user/:userslug/edit/password', middleware, accountMiddlewares, controllers.accounts.edit.password); + setupPageRoute(app, '/user/:userslug/info', middleware, accountMiddlewares, controllers.accounts.info.get); setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get); - app.delete('/user/:userslug/session/:uuid', accountMiddlewares, controllers.accounts.session.revoke); + app.delete('/api/user/:userslug/session/:uuid', [middleware.requireUser], controllers.accounts.session.revoke); setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get); - setupPageRoute(app, '/chats/:roomid?', middleware, [middleware.authenticate], controllers.accounts.chats.get); + setupPageRoute(app, '/user/:userslug/chats/:roomid?', middleware, middlewares, controllers.accounts.chats.get); + setupPageRoute(app, '/chats/:roomid?', middleware, [], controllers.accounts.chats.redirectToChat); }; diff --git a/src/routes/admin.js b/src/routes/admin.js index 6ce4fb5f48..7d7029482c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -15,6 +15,7 @@ function apiRoutes(router, middleware, controllers) { router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon); router.post('/uploadTouchIcon', middlewares, controllers.admin.uploads.uploadTouchIcon); router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo); + router.post('/uploadOgImage', middlewares, controllers.admin.uploads.uploadOgImage); router.post('/upload/sound', middlewares, controllers.admin.uploads.uploadSound); router.post('/uploadDefaultAvatar', middlewares, controllers.admin.uploads.uploadDefaultAvatar); } @@ -64,6 +65,7 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/users/not-validated', middlewares, controllers.admin.users.notValidated); router.get('/manage/users/no-posts', middlewares, controllers.admin.users.noPosts); router.get('/manage/users/inactive', middlewares, controllers.admin.users.inactive); + router.get('/manage/users/flagged', middlewares, controllers.admin.users.flagged); router.get('/manage/users/banned', middlewares, controllers.admin.users.banned); router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue); @@ -81,13 +83,15 @@ function addRoutes(router, middleware, controllers) { router.get('/advanced/database', middlewares, controllers.admin.database.get); router.get('/advanced/events', middlewares, controllers.admin.events.get); router.get('/advanced/logs', middlewares, controllers.admin.logs.get); - router.get('/advanced/post-cache', middlewares, controllers.admin.postCache.get); + router.get('/advanced/errors', middlewares, controllers.admin.errors.get); + router.get('/advanced/errors/export', middlewares, controllers.admin.errors.export); + router.get('/advanced/cache', middlewares, controllers.admin.cache.get); router.get('/development/logger', middlewares, controllers.admin.logger.get); router.get('/development/info', middlewares, controllers.admin.info.get); } -module.exports = function(app, middleware, controllers) { +module.exports = function (app, middleware, controllers) { app.use('/admin/', adminRouter(middleware, controllers)); app.use('/api/admin/', apiRouter(middleware, controllers)); }; diff --git a/src/routes/api.js b/src/routes/api.js index 34cf142b37..be76336c77 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -1,10 +1,10 @@ "use strict"; -var express = require('express'), +var express = require('express'); - uploadsController = require('../controllers/uploads'); +var uploadsController = require('../controllers/uploads'); -module.exports = function(app, middleware, controllers) { +module.exports = function (app, middleware, controllers) { var router = express.Router(); app.use('/api', router); @@ -12,6 +12,7 @@ module.exports = function(app, middleware, controllers) { router.get('/config', middleware.applyCSRF, controllers.api.getConfig); router.get('/widgets/render', controllers.api.renderWidgets); + router.get('/me', middleware.checkGlobalPrivacySettings, controllers.api.getCurrentUser); router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUID); router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUsername); router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.api.getUserByEmail); @@ -22,8 +23,9 @@ module.exports = function(app, middleware, controllers) { router.get('/categories/:cid/moderators', controllers.api.getModerators); router.get('/recent/posts/:term?', controllers.api.getRecentPosts); - router.get('/unread/total', middleware.authenticate, controllers.unread.unreadTotal); + router.get('/unread/:filter?/total', middleware.authenticate, controllers.unread.unreadTotal); router.get('/topic/teaser/:topic_id', controllers.topics.teaser); + router.get('/topic/pagination/:topic_id', controllers.topics.pagination); var multipart = require('connect-multiparty'); var multipartMiddleware = multipart(); diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 8e1824cad6..c6f1359fc2 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -1,4 +1,4 @@ -(function(Auth) { +(function (Auth) { "use strict"; var passport = require('passport'), @@ -13,11 +13,11 @@ loginStrategies = []; - Auth.initialize = function(app, middleware) { + Auth.initialize = function (app, middleware) { app.use(passport.initialize()); app.use(passport.session()); - app.use(function(req, res, next) { + app.use(function (req, res, next) { req.uid = req.user ? parseInt(req.user.uid, 10) : 0; next(); }); @@ -26,11 +26,11 @@ Auth.middleware = middleware; }; - Auth.getLoginStrategies = function() { + Auth.getLoginStrategies = function () { return loginStrategies; }; - Auth.reloadRoutes = function(callback) { + Auth.reloadRoutes = function (callback) { var router = express.Router(); router.hotswapId = 'auth'; @@ -43,16 +43,17 @@ passport.use(new passportLocal({passReqToCallback: true}, controllers.authentication.localLogin)); } - plugins.fireHook('filter:auth.init', loginStrategies, function(err) { + plugins.fireHook('filter:auth.init', loginStrategies, function (err) { if (err) { winston.error('filter:auth.init - plugin failure'); return callback(err); } - loginStrategies.forEach(function(strategy) { + loginStrategies.forEach(function (strategy) { if (strategy.url) { router.get(strategy.url, passport.authenticate(strategy.name, { - scope: strategy.scope + scope: strategy.scope, + prompt: strategy.prompt || undefined })); } @@ -63,6 +64,8 @@ }); router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register); + router.post('/register/complete', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.registerComplete); + router.get('/register/abort', controllers.authentication.registerAbort); router.post('/login', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.login); router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout); @@ -73,11 +76,11 @@ }); }; - passport.serializeUser(function(user, done) { + passport.serializeUser(function (user, done) { done(null, user.uid); }); - passport.deserializeUser(function(uid, done) { + passport.deserializeUser(function (uid, done) { done(null, { uid: uid }); diff --git a/src/routes/debug.js b/src/routes/debug.js index b81938ccc9..9c5efe6c93 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -1,13 +1,15 @@ "use strict"; -var express = require('express'), - nconf = require('nconf'), - user = require('./../user'), - categories = require('./../categories'), - topics = require('./../topics'), - posts = require('./../posts'); +var express = require('express'); +var nconf = require('nconf'); +var winston = require('winston'); +var user = require('../user'); +var categories = require('../categories'); +var topics = require('../topics'); +var posts = require('../posts'); +var db = require('../database'); -module.exports = function(app, middleware, controllers) { +module.exports = function (app, middleware, controllers) { var router = express.Router(); router.get('/uid/:uid', function (req, res) { @@ -16,6 +18,10 @@ module.exports = function(app, middleware, controllers) { } user.getUserData(req.params.uid, function (err, data) { + if (err) { + winston.error(err); + } + if (data) { res.send(data); } else { @@ -28,6 +34,10 @@ module.exports = function(app, middleware, controllers) { router.get('/cid/:cid', function (req, res) { categories.getCategoryData(req.params.cid, function (err, data) { + if (err) { + winston.error(err); + } + if (data) { res.send(data); } else { @@ -38,6 +48,10 @@ module.exports = function(app, middleware, controllers) { router.get('/tid/:tid', function (req, res) { topics.getTopicData(req.params.tid, function (err, data) { + if (err) { + winston.error(err); + } + if (data) { res.send(data); } else { @@ -48,6 +62,10 @@ module.exports = function(app, middleware, controllers) { router.get('/pid/:pid', function (req, res) { posts.getPostData(req.params.pid, function (err, data) { + if (err) { + winston.error(err); + } + if (data) { res.send(data); } else { @@ -56,7 +74,7 @@ module.exports = function(app, middleware, controllers) { }); }); - router.get('/test', function(req, res) { + router.get('/test', function (req, res) { res.redirect(404); }); diff --git a/src/routes/feeds.js b/src/routes/feeds.js index bfa2945fb5..7a178ab9f0 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -23,10 +23,10 @@ function generateForTopic(req, res, callback) { async.waterfall([ function (next) { async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.topics.get(tid, req.uid, next); }, - topic: function(next) { + topic: function (next) { topics.getTopicData(tid, next); } }, next); @@ -38,13 +38,13 @@ function generateForTopic(req, res, callback) { if (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted) { return callback(); } - if (!results.privileges.read) { + if (!results.privileges.read || !results.privileges['topics:read']) { return helpers.notAllowed(req, res); } userPrivileges = results.privileges; topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next); } - ], function(err, topicData) { + ], function (err, topicData) { if (err) { return callback(err); } @@ -70,14 +70,14 @@ function generateForTopic(req, res, callback) { feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString(); } - topicData.posts.forEach(function(postData) { + topicData.posts.forEach(function (postData) { if (!postData.deleted) { dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString(); feed.item({ title: 'Reply to ' + topicData.title + ' on ' + dateStamp, description: postData.content, - url: nconf.get('url') + '/topic/' + topicData.slug + (postData.index ? '/' + (postData.index + 1) : ''), + url: nconf.get('url') + '/post/' + postData.pid, author: postData.user ? postData.user.username : '', date: dateStamp }); @@ -105,9 +105,9 @@ function generateForUserTopics(req, res, callback) { } user.getUserFields(uid, ['uid', 'username'], next); } - ], function(err, userData) { + ], function (err, userData) { if (err) { - return next(err); + return callback(err); } generateForTopics({ @@ -116,7 +116,7 @@ function generateForUserTopics(req, res, callback) { description: 'A list of topics that are posted by ' + userData.username, feed_url: '/user/' + userslug + '/topics.rss', site_url: '/user/' + userslug + '/topics' - }, 'uid:' + userData.uid + ':topics', req, res, next); + }, 'uid:' + userData.uid + ':topics', req, res, callback); }); } @@ -129,10 +129,10 @@ function generateForCategory(req, res, next) { async.waterfall([ function (next) { async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.categories.get(cid, req.uid, next); }, - category: function(next) { + category: function (next) { categories.getCategoryById({ cid: cid, set: 'cid:' + cid + ':tids', @@ -156,7 +156,7 @@ function generateForCategory(req, res, next) { site_url: '/category/' + results.category.cid, }, results.category.topics, next); } - ], function(err, feed) { + ], function (err, feed) { if (err) { return next(err); } @@ -189,7 +189,7 @@ function generateForPopular(req, res, next) { }; var term = terms[req.params.term] || 'day'; - topics.getPopular(term, req.uid, 19, function(err, topics) { + topics.getPopular(term, req.uid, 19, function (err, topics) { if (err) { return next(err); } @@ -200,7 +200,7 @@ function generateForPopular(req, res, next) { description: 'A list of topics that are sorted by post count', feed_url: '/popular/' + (req.params.term || 'daily') + '.rss', site_url: '/popular/' + (req.params.term || 'daily') - }, topics, function(err, feed) { + }, topics, function (err, feed) { if (err) { return next(err); } @@ -215,7 +215,7 @@ function generateForTopics(options, set, req, res, next) { return next(err); } - generateTopicsFeed(options, data.topics, function(err, feed) { + generateTopicsFeed(options, data.topics, function (err, feed) { if (err) { return next(err); } @@ -238,7 +238,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) { feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString(); } - async.map(feedTopics, function(topicData, next) { + async.map(feedTopics, function (topicData, next) { var feedItem = { title: topicData.title, url: nconf.get('url') + '/topic/' + topicData.slug, @@ -251,7 +251,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) { return next(null, feedItem); } - topics.getMainPost(topicData.tid, feedOptions.uid, function(err, mainPost) { + topics.getMainPost(topicData.tid, feedOptions.uid, function (err, mainPost) { if (err) { return next(err); } @@ -262,11 +262,11 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) { feedItem.author = mainPost.user.username; next(null, feedItem); }); - }, function(err, feedItems) { + }, function (err, feedItems) { if (err) { return callback(err); } - feedItems.forEach(function(feedItem) { + feedItems.forEach(function (feedItem) { if (feedItem) { feed.item(feedItem); } @@ -280,7 +280,7 @@ function generateForRecentPosts(req, res, next) { return next(); } - posts.getRecentPosts(req.uid, 0, 19, 'month', function(err, posts) { + posts.getRecentPosts(req.uid, 0, 19, 'month', function (err, posts) { if (err) { return next(err); } @@ -303,16 +303,16 @@ function generateForCategoryRecentPosts(req, res, next) { var cid = req.params.category_id; async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.categories.get(cid, req.uid, next); }, - category: function(next) { + category: function (next) { categories.getCategoryData(cid, next); }, - posts: function(next) { + posts: function (next) { categories.getRecentReplies(cid, req.uid, 20, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return next(err); } @@ -346,11 +346,11 @@ function generateForPostsFeed(feedOptions, posts) { feed.pubDate = new Date(parseInt(posts[0].timestamp, 10)).toUTCString(); } - posts.forEach(function(postData) { + posts.forEach(function (postData) { feed.item({ title: postData.topic ? postData.topic.title : '', description: postData.content, - url: nconf.get('url') + '/topic/' + (postData.topic ? postData.topic.slug : '#') + '/'+postData.index, + url: nconf.get('url') + '/post/' + postData.pid, author: postData.user ? postData.user.username : '', date: new Date(parseInt(postData.timestamp, 10)).toUTCString() }); @@ -364,7 +364,7 @@ function sendFeed(feed, res) { res.type('xml').set('Content-Length', Buffer.byteLength(xml)).send(xml); } -module.exports = function(app, middleware, controllers){ +module.exports = function (app, middleware, controllers){ app.get('/topic/:topic_id.rss', generateForTopic); app.get('/category/:category_id.rss', generateForCategory); app.get('/recent.rss', generateForRecent); diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 3dcca80bc4..052d99292c 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -2,8 +2,8 @@ var helpers = {}; -helpers.setupPageRoute = function(router, name, middleware, middlewares, controller) { - middlewares = middlewares.concat([middleware.pageView, middleware.pluginHooks]); +helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) { + middlewares = middlewares.concat([middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]); router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller); router.get('/api' + name, middlewares, controller); diff --git a/src/routes/index.js b/src/routes/index.js index a80eb2e46a..e1e5c79d80 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,23 +1,21 @@ "use strict"; -var nconf = require('nconf'), - path = require('path'), - async = require('async'), - winston = require('winston'), - controllers = require('../controllers'), - plugins = require('../plugins'), - express = require('express'), - validator = require('validator'), - - accountRoutes = require('./accounts'), - - metaRoutes = require('./meta'), - apiRoutes = require('./api'), - adminRoutes = require('./admin'), - feedRoutes = require('./feeds'), - pluginRoutes = require('./plugins'), - authRoutes = require('./authentication'), - helpers = require('./helpers'); +var nconf = require('nconf'); +var path = require('path'); +var async = require('async'); +var controllers = require('../controllers'); +var plugins = require('../plugins'); +var user = require('../user'); +var express = require('express'); + +var accountRoutes = require('./accounts'); +var metaRoutes = require('./meta'); +var apiRoutes = require('./api'); +var adminRoutes = require('./admin'); +var feedRoutes = require('./feeds'); +var pluginRoutes = require('./plugins'); +var authRoutes = require('./authentication'); +var helpers = require('./helpers'); var setupPageRoute = helpers.setupPageRoute; @@ -28,17 +26,24 @@ function mainRoutes(app, middleware, controllers) { setupPageRoute(app, '/login', middleware, loginRegisterMiddleware, controllers.login); setupPageRoute(app, '/register', middleware, loginRegisterMiddleware, controllers.register); + setupPageRoute(app, '/register/complete', middleware, [], controllers.registerInterstitial); setupPageRoute(app, '/compose', middleware, [], controllers.compose); setupPageRoute(app, '/confirm/:code', middleware, [], controllers.confirmEmail); setupPageRoute(app, '/outgoing', middleware, [], controllers.outgoing); - setupPageRoute(app, '/search/:term?', middleware, [], controllers.search.search); + setupPageRoute(app, '/search', middleware, [], controllers.search.search); setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset); setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse); + + app.get('/ping', controllers.ping); + app.get('/sping', controllers.ping); +} + +function modRoutes(app, middleware, controllers) { + setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged); } function globalModRoutes(app, middleware, controllers) { setupPageRoute(app, '/ip-blacklist', middleware, [], controllers.globalMods.ipBlacklist); - setupPageRoute(app, '/posts/flags', middleware, [], controllers.globalMods.flagged); } function topicRoutes(app, middleware, controllers) { @@ -46,6 +51,10 @@ function topicRoutes(app, middleware, controllers) { setupPageRoute(app, '/topic/:topic_id/:slug?', middleware, [], controllers.topics.get); } +function postRoutes(app, middleware, controllers) { + setupPageRoute(app, '/post/:pid', middleware, [], controllers.posts.redirectToPost); +} + function tagRoutes(app, middleware, controllers) { setupPageRoute(app, '/tags/:tag', middleware, [middleware.privateTagListing], controllers.tags.getTag); setupPageRoute(app, '/tags', middleware, [middleware.privateTagListing], controllers.tags.getTags); @@ -55,7 +64,7 @@ function categoryRoutes(app, middleware, controllers) { setupPageRoute(app, '/categories', middleware, [], controllers.categories.list); setupPageRoute(app, '/popular/:term?', middleware, [], controllers.popular.get); setupPageRoute(app, '/recent', middleware, [], controllers.recent.get); - setupPageRoute(app, '/unread', middleware, [middleware.authenticate], controllers.unread.get); + setupPageRoute(app, '/unread/:filter?', middleware, [middleware.authenticate], controllers.unread.get); setupPageRoute(app, '/category/:category_id/:slug/:topic_index', middleware, [], controllers.category.get); setupPageRoute(app, '/category/:category_id/:slug?', middleware, [], controllers.category.get); @@ -64,33 +73,37 @@ function categoryRoutes(app, middleware, controllers) { function userRoutes(app, middleware, controllers) { var middlewares = [middleware.checkGlobalPrivacySettings]; - setupPageRoute(app, '/users', middleware, middlewares, controllers.users.getUsersSortedByJoinDate); - setupPageRoute(app, '/users/online', middleware, middlewares, controllers.users.getOnlineUsers); - setupPageRoute(app, '/users/sort-posts', middleware, middlewares, controllers.users.getUsersSortedByPosts); - setupPageRoute(app, '/users/sort-reputation', middleware, middlewares, controllers.users.getUsersSortedByReputation); - setupPageRoute(app, '/users/banned', middleware, middlewares, controllers.users.getBannedUsers); + setupPageRoute(app, '/users', middleware, middlewares, controllers.users.index); } - function groupRoutes(app, middleware, controllers) { - var middlewares = [middleware.checkGlobalPrivacySettings, middleware.exposeGroupName]; + var middlewares = [middleware.checkGlobalPrivacySettings]; setupPageRoute(app, '/groups', middleware, middlewares, controllers.groups.list); setupPageRoute(app, '/groups/:slug', middleware, middlewares, controllers.groups.details); setupPageRoute(app, '/groups/:slug/members', middleware, middlewares, controllers.groups.members); } -module.exports = function(app, middleware) { - var router = express.Router(), - pluginRouter = express.Router(), - authRouter = express.Router(), - relativePath = nconf.get('relative_path'), - ensureLoggedIn = require('connect-ensure-login'); +module.exports = function (app, middleware, hotswapIds) { + var routers = [ + express.Router(), // plugin router + express.Router(), // main app router + express.Router() // auth router + ]; + var router = routers[1]; + var pluginRouter = routers[0]; + var authRouter = routers[2]; + var relativePath = nconf.get('relative_path'); + var ensureLoggedIn = require('connect-ensure-login'); - pluginRouter.render = function() { - app.render.apply(app, arguments); - }; - controllers.render = function() { + if (Array.isArray(hotswapIds) && hotswapIds.length) { + for(var idx,x = 0;x < hotswapIds.length;x++) { + idx = routers.push(express.Router()) - 1; + routers[idx].hotswapId = hotswapIds[x]; + } + } + + pluginRouter.render = function () { app.render.apply(app, arguments); }; @@ -112,6 +125,8 @@ module.exports = function(app, middleware) { mainRoutes(router, middleware, controllers); topicRoutes(router, middleware, controllers); + postRoutes(router, middleware, controllers); + modRoutes(router, middleware, controllers); globalModRoutes(router, middleware, controllers); tagRoutes(router, middleware, controllers); categoryRoutes(router, middleware, controllers); @@ -119,98 +134,28 @@ module.exports = function(app, middleware) { userRoutes(router, middleware, controllers); groupRoutes(router, middleware, controllers); - app.use(relativePath, pluginRouter); - app.use(relativePath, router); - app.use(relativePath, authRouter); + for(var x = 0;x < routers.length;x++) { + app.use(relativePath, routers[x]); + } if (process.env.NODE_ENV === 'development') { require('./debug')(app, middleware, controllers); } app.use(middleware.privateUploads); - + app.use(relativePath + '/language/:code', middleware.processLanguages); app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { maxAge: app.enabled('cache') ? 5184000000 : 0 })); - - handle404(app, middleware); - handleErrors(app, middleware); - + app.use('/vendor/jquery/timeago/locales', middleware.processTimeagoLocales); + app.use(controllers.handle404); + app.use(controllers.handleURIErrors); + app.use(controllers.handleErrors); // Add plugin routes async.series([ async.apply(plugins.reloadRoutes), - async.apply(authRoutes.reloadRoutes) + async.apply(authRoutes.reloadRoutes), + async.apply(user.addInterstitials) ]); }; - -function handle404(app, middleware) { - var relativePath = nconf.get('relative_path'); - var isLanguage = new RegExp('^' + relativePath + '/language/.*/.*.json'); - var isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); - - app.use(function(req, res) { - if (plugins.hasListeners('action:meta.override404')) { - return plugins.fireHook('action:meta.override404', { - req: req, - res: res, - error: {} - }); - } - - if (isClientScript.test(req.url)) { - res.type('text/javascript').status(200).send(''); - } else if (isLanguage.test(req.url)) { - res.status(200).json({}); - } else if (req.path.startsWith(relativePath + '/uploads')) { - res.status(404).send(''); - } else if (req.path === '/favicon.ico') { - res.status(404).send(''); - } else if (req.accepts('html')) { - if (process.env.NODE_ENV === 'development') { - winston.warn('Route requested but not found: ' + req.url); - } - - res.status(404); - - if (res.locals.isAPI) { - return res.json({path: validator.escape(req.path.replace(/^\/api/, '') || ''), title: '[[global:404.title]]'}); - } - - middleware.buildHeader(req, res, function() { - res.render('404', {path: validator.escape(req.path || ''), title: '[[global:404.title]]'}); - }); - } else { - res.status(404).type('txt').send('Not found'); - } - }); -} - -function handleErrors(app, middleware) { - app.use(function(err, req, res, next) { - switch (err.code) { - case 'EBADCSRFTOKEN': - winston.error(req.path + '\n', err.message); - return res.sendStatus(403); - case 'blacklisted-ip': - return res.status(403).type('text/plain').send(err.message); - } - - if (parseInt(err.status, 10) === 302 && err.path) { - return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path); - } - - winston.error(req.path + '\n', err.stack); - - res.status(err.status || 500); - - if (res.locals.isAPI) { - res.json({path: validator.escape(req.path || ''), error: err.message}); - } else { - middleware.buildHeader(req, res, function() { - res.render('500', {path: validator.escape(req.path || ''), error: validator.escape(err.message)}); - }); - } - }); -} - diff --git a/src/routes/meta.js b/src/routes/meta.js index 65757566de..c62465c8d0 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -1,11 +1,14 @@ "use strict"; -var meta = require('../meta'), - middleware = require('../middleware'); +var path = require('path'); +var nconf = require('nconf'); +var meta = require('../meta'); -function sendMinifiedJS(req, res, next) { - var cache = meta.js.target['nodebb.min.js'] ? meta.js.target['nodebb.min.js'].cache : ''; + +function sendMinifiedJS(req, res) { + var target = path.basename(req.path); + var cache = meta.js.target[target] ? meta.js.target[target].cache : ''; res.type('text/javascript').send(cache); } @@ -19,18 +22,29 @@ function sendMinifiedJS(req, res, next) { // } // }; -function sendStylesheet(req, res, next) { +function sendStylesheet(req, res) { res.type('text/css').status(200).send(meta.css.cache); } -function sendACPStylesheet(req, res, next) { +function sendACPStylesheet(req, res) { res.type('text/css').status(200).send(meta.css.acpCache); } -module.exports = function(app, middleware, controllers) { +function sendSoundFile(req, res, next) { + var resolved = meta.sounds._filePathHash[path.basename(req.path)]; + + if (resolved) { + res.status(200).sendFile(resolved); + } else { + next(); + } +} + +module.exports = function (app, middleware, controllers) { app.get('/stylesheet.css', middleware.addExpiresHeaders, sendStylesheet); app.get('/admin.css', middleware.addExpiresHeaders, sendACPStylesheet); app.get('/nodebb.min.js', middleware.addExpiresHeaders, sendMinifiedJS); + app.get('/acp.min.js', middleware.addExpiresHeaders, sendMinifiedJS); // app.get('/nodebb.min.js.map', middleware.addExpiresHeaders, sendJSSourceMap); app.get('/sitemap.xml', controllers.sitemap.render); app.get('/sitemap/pages.xml', controllers.sitemap.getPages); @@ -39,4 +53,8 @@ module.exports = function(app, middleware, controllers) { app.get('/robots.txt', controllers.robots); app.get('/manifest.json', controllers.manifest); app.get('/css/previews/:theme', controllers.admin.themes.get); + + if (nconf.get('local-assets') === false) { + app.get('/sounds/*', middleware.addExpiresHeaders, sendSoundFile); + } }; diff --git a/src/routes/plugins.js b/src/routes/plugins.js index 72af49fbed..37e23c5ee6 100644 --- a/src/routes/plugins.js +++ b/src/routes/plugins.js @@ -1,41 +1,40 @@ "use strict"; -var _ = require('underscore'), - nconf = require('nconf'), - path = require('path'), - fs = require('fs'), - validator = require('validator'), - async = require('async'), - winston = require('winston'), +var _ = require('underscore'); +var path = require('path'); - plugins = require('../plugins'), - helpers = require('../controllers/helpers'); +var plugins = require('../plugins'); - -module.exports = function(app, middleware, controllers) { +module.exports = function (app, middleware, controllers) { // Static Assets - app.get('/plugins/:id/*', middleware.addExpiresHeaders, function(req, res, next) { - var relPath = req._parsedUrl.pathname.replace('/plugins/', ''), - matches = _.map(plugins.staticDirs, function(realPath, mappedPath) { - if (relPath.match(mappedPath)) { - return mappedPath; - } else { - return null; + app.get('/plugins/:id/*', middleware.addExpiresHeaders, function (req, res, next) { + + var relPath = req._parsedUrl.pathname.replace('/plugins/', ''); + + var matches = _.map(plugins.staticDirs, function (realPath, mappedPath) { + if (relPath.match(mappedPath)) { + var pathToFile = path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length))); + if (pathToFile.startsWith(plugins.staticDirs[mappedPath])) { + return pathToFile; } - }).filter(Boolean); + } + + return null; + }).filter(Boolean); - if (!matches) { + if (!matches || !matches.length) { return next(); } - matches = matches.map(function(mappedPath) { - return path.join(plugins.staticDirs[mappedPath], decodeURIComponent(relPath.slice(mappedPath.length))); + res.sendFile(matches[0], {}, function (err) { + if (err) { + if (err.code === 'ENOENT') { + // File doesn't exist, this isn't an error, to send to 404 handler + return next(); + } else { + return next(err); + } + } }); - - if (matches.length) { - res.sendFile(matches[0]); - } else { - next(); - } }); -}; +}; \ No newline at end of file diff --git a/src/search.js b/src/search.js index 9b7306c7fa..7f03634625 100644 --- a/src/search.js +++ b/src/search.js @@ -16,7 +16,7 @@ var search = {}; module.exports = search; -search.search = function(data, callback) { +search.search = function (data, callback) { var start = process.hrtime(); var searchIn = data.searchIn || 'titlesposts'; @@ -34,7 +34,7 @@ search.search = function(data, callback) { } }, function (result, next) { - result.search_query = validator.escape(data.query || ''); + result.search_query = validator.escape(String(data.query || '')); result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); next(null, result); } @@ -44,26 +44,26 @@ search.search = function(data, callback) { function searchInContent(data, callback) { data.uid = data.uid || 0; async.parallel({ - searchCids: function(next) { + searchCids: function (next) { getSearchCids(data, next); }, - searchUids: function(next) { + searchUids: function (next) { getSearchUids(data, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } async.parallel({ - pids: function(next) { + pids: function (next) { if (data.searchIn === 'posts' || data.searchIn === 'titlesposts') { search.searchQuery('post', data.query, results.searchCids, results.searchUids, next); } else { next(null, []); } }, - tids: function(next) { + tids: function (next) { if (data.searchIn === 'titles' || data.searchIn === 'titlesposts') { search.searchQuery('topic', data.query, results.searchCids, results.searchUids, next); } else { @@ -81,22 +81,22 @@ function searchInContent(data, callback) { } async.waterfall([ - function(next) { + function (next) { topics.getMainPids(results.tids, next); }, - function(mainPids, next) { - results.pids = mainPids.concat(results.pids).map(function(pid) { + function (mainPids, next) { + results.pids = mainPids.concat(results.pids).map(function (pid) { return pid && pid.toString(); - }).filter(function(pid, index, array) { + }).filter(function (pid, index, array) { return pid && array.indexOf(pid) === index; }); privileges.posts.filter('read', results.pids, data.uid, next); }, - function(pids, next) { + function (pids, next) { filterAndSort(pids, data, next); }, - function(pids, next) { + function (pids, next) { matchCount = pids.length; if (data.page) { var start = Math.max(0, (data.page - 1)) * 10; @@ -105,7 +105,7 @@ function searchInContent(data, callback) { posts.getPostSummaryByPids(pids, data.uid, {}, next); }, - function(posts, next) { + function (posts, next) { next(null, {posts: posts, matchCount: matchCount, pageCount: Math.max(1, Math.ceil(parseInt(matchCount, 10) / 10))}); } ], callback); @@ -114,7 +114,7 @@ function searchInContent(data, callback) { } function filterAndSort(pids, data, callback) { - getMatchedPosts(pids, data, function(err, posts) { + getMatchedPosts(pids, data, function (err, posts) { if (err) { return callback(err); } @@ -129,7 +129,7 @@ function filterAndSort(pids, data, callback) { sortPosts(posts, data); - pids = posts.map(function(post) { + pids = posts.map(function (post) { return post && post.pid; }); @@ -162,21 +162,21 @@ function getMatchedPosts(pids, data, callback) { var posts; async.waterfall([ - function(next) { - var keys = pids.map(function(pid) { + function (next) { + var keys = pids.map(function (pid) { return 'post:' + pid; }); db.getObjectsFields(keys, postFields, next); }, - function(_posts, next) { - posts = _posts.filter(function(post) { + function (_posts, next) { + posts = _posts.filter(function (post) { return post && parseInt(post.deleted, 10) !== 1; }); async.parallel({ - users: function(next) { + users: function (next) { if (data.sortBy && data.sortBy.startsWith('user')) { - var uids = posts.map(function(post) { + var uids = posts.map(function (post) { return post.uid; }); user.getUsersFields(uids, ['username'], next); @@ -184,22 +184,22 @@ function getMatchedPosts(pids, data, callback) { next(); } }, - topics: function(next) { + topics: function (next) { var topics; async.waterfall([ - function(next) { - var topicKeys = posts.map(function(post) { + function (next) { + var topicKeys = posts.map(function (post) { return 'topic:' + post.tid; }); db.getObjectsFields(topicKeys, topicFields, next); }, - function(_topics, next) { + function (_topics, next) { topics = _topics; async.parallel({ - teasers: function(next) { + teasers: function (next) { if (topicFields.indexOf('teaserPid') !== -1) { - var teaserKeys = topics.map(function(topic) { + var teaserKeys = topics.map(function (topic) { return 'post:' + topic.teaserPid; }); db.getObjectsFields(teaserKeys, ['timestamp'], next); @@ -207,23 +207,23 @@ function getMatchedPosts(pids, data, callback) { next(); } }, - categories: function(next) { + categories: function (next) { if (!categoryFields.length) { return next(); } - var cids = topics.map(function(topic) { + var cids = topics.map(function (topic) { return 'category:' + topic.cid; }); db.getObjectsFields(cids, categoryFields, next); } }, next); } - ], function(err, results) { + ], function (err, results) { if (err) { return next(err); } - topics.forEach(function(topic, index) { + topics.forEach(function (topic, index) { if (topic && results.categories && results.categories[index]) { topic.category = results.categories[index]; } @@ -237,9 +237,9 @@ function getMatchedPosts(pids, data, callback) { } }, next); }, - function(results, next) { + function (results, next) { - posts.forEach(function(post, index) { + posts.forEach(function (post, index) { if (results.topics && results.topics[index]) { post.topic = results.topics[index]; if (results.topics[index].category) { @@ -255,7 +255,7 @@ function getMatchedPosts(pids, data, callback) { } }); - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return post && post.topic && parseInt(post.topic.deleted, 10) !== 1; }); @@ -268,11 +268,11 @@ function filterByPostcount(posts, postCount, repliesFilter) { postCount = parseInt(postCount, 10); if (postCount) { if (repliesFilter === 'atleast') { - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return post.topic && post.topic.postcount >= postCount; }); } else { - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return post.topic && post.topic.postcount <= postCount; }); } @@ -285,11 +285,11 @@ function filterByTimerange(posts, timeRange, timeFilter) { if (timeRange) { var time = Date.now() - timeRange; if (timeFilter === 'newer') { - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return post.timestamp >= time; }); } else { - posts = posts.filter(function(post) { + posts = posts.filter(function (post) { return post.timestamp <= time; }); } @@ -298,15 +298,15 @@ function filterByTimerange(posts, timeRange, timeFilter) { } function sortPosts(posts, data) { - if (!posts.length) { + if (!posts.length || !data.sortBy) { return; } - data.sortBy = data.sortBy || 'timestamp'; + data.sortDirection = data.sortDirection || 'desc'; var direction = data.sortDirection === 'desc' ? 1 : -1; if (data.sortBy === 'timestamp') { - posts.sort(function(p1, p2) { + posts.sort(function (p1, p2) { return direction * (p2.timestamp - p1.timestamp); }); @@ -323,11 +323,11 @@ function sortPosts(posts, data) { var isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); if (isNumeric) { - posts.sort(function(p1, p2) { + posts.sort(function (p1, p2) { return direction * (p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]); }); } else { - posts.sort(function(p1, p2) { + posts.sort(function (p1, p2) { if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) { return direction; } else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) { @@ -345,10 +345,10 @@ function getSearchCids(data, callback) { if (data.categories.indexOf('all') !== -1) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRange('categories:cid', 0, -1, next); }, - function(cids, next) { + function (cids, next) { privileges.categories.filterCids('read', cids, data.uid, next); } ], callback); @@ -356,26 +356,26 @@ function getSearchCids(data, callback) { } async.parallel({ - watchedCids: function(next) { + watchedCids: function (next) { if (data.categories.indexOf('watched') !== -1) { user.getWatchedCategories(data.uid, next); } else { next(null, []); } }, - childrenCids: function(next) { + childrenCids: function (next) { if (data.searchChildren) { getChildrenCids(data.categories, data.uid, next); } else { next(null, []); } } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - var cids = results.watchedCids.concat(results.childrenCids).concat(data.categories).filter(function(cid, index, array) { + var cids = results.watchedCids.concat(results.childrenCids).concat(data.categories).filter(function (cid, index, array) { return cid && array.indexOf(cid) === index; }); @@ -384,7 +384,7 @@ function getSearchCids(data, callback) { } function getChildrenCids(cids, uid, callback) { - categories.getChildren(cids, uid, function(err, childrenCategories) { + categories.getChildren(cids, uid, function (err, childrenCategories) { if (err) { return callback(err); } @@ -392,9 +392,9 @@ function getChildrenCids(cids, uid, callback) { var childrenCids = []; var allCategories = []; - childrenCategories.forEach(function(childrens) { + childrenCategories.forEach(function (childrens) { categories.flattenCategories(allCategories, childrens); - childrenCids = childrenCids.concat(allCategories.map(function(category) { + childrenCids = childrenCids.concat(allCategories.map(function (category) { return category && category.cid; })); }); @@ -411,7 +411,7 @@ function getSearchUids(data, callback) { } } -search.searchQuery = function(index, content, cids, uids, callback) { +search.searchQuery = function (index, content, cids, uids, callback) { plugins.fireHook('filter:search.query', { index: index, content: content, diff --git a/src/sitemap.js b/src/sitemap.js index 4c7efb73b5..d21bb96241 100644 --- a/src/sitemap.js +++ b/src/sitemap.js @@ -1,16 +1,15 @@ 'use strict'; -var path = require('path'), - async = require('async'), - sm = require('sitemap'), - url = require('url'), - nconf = require('nconf'), - db = require('./database'), - categories = require('./categories'), - topics = require('./topics'), - privileges = require('./privileges'), - meta = require('./meta'), - utils = require('../public/src/utils'); +var async = require('async'); +var sm = require('sitemap'); +var nconf = require('nconf'); + +var db = require('./database'); +var categories = require('./categories'); +var topics = require('./topics'); +var privileges = require('./privileges'); +var meta = require('./meta'); +var utils = require('../public/src/utils'); var sitemap = { maps: { @@ -18,7 +17,7 @@ var sitemap = { } }; -sitemap.render = function(callback) { +sitemap.render = function (callback) { var numTopics = parseInt(meta.config.sitemapTopics, 10) || 500; var returnData = { url: nconf.get('url'), @@ -28,17 +27,17 @@ sitemap.render = function(callback) { async.waterfall([ async.apply(db.getSortedSetRange, 'topics:recent', 0, -1), - function(tids, next) { + function (tids, next) { privileges.topics.filterTids('read', tids, 0, next); } - ], function(err, tids) { + ], function (err, tids) { if (err) { numPages = 1; } else { numPages = Math.ceil(tids.length / numTopics); } - for(var x=1;x<=numPages;x++) { + for(var x = 1;x <= numPages;x++) { returnData.topics.push(x); } @@ -46,49 +45,32 @@ sitemap.render = function(callback) { }); }; -sitemap.getStaticUrls = function(callback) { - callback(null, [{ - url: '', - changefreq: 'weekly', - priority: '0.6' - }, { - url: '/recent', - changefreq: 'daily', - priority: '0.4' - }, { - url: '/users', - changefreq: 'daily', - priority: '0.4' - }, { - url: '/groups', - changefreq: 'daily', - priority: '0.4' - }]); -}; - -sitemap.getPages = function(callback) { - if (sitemap.maps.pages && sitemap.maps.pages.cache.length) { +sitemap.getPages = function (callback) { + if ( + sitemap.maps.pages && + Date.now() < parseInt(sitemap.maps.pages.cacheSetTimestamp, 10) + parseInt(sitemap.maps.pages.cacheResetPeriod, 10) + ) { return sitemap.maps.pages.toXML(callback); } var urls = [{ url: '', changefreq: 'weekly', - priority: '0.6' + priority: 0.6 }, { url: '/recent', changefreq: 'daily', - priority: '0.4' + priority: 0.4 }, { url: '/users', changefreq: 'daily', - priority: '0.4' + priority: 0.4 }, { url: '/groups', changefreq: 'daily', - priority: '0.4' + priority: 0.4 }]; - + sitemap.maps.pages = sm.createSitemap({ hostname: nconf.get('url'), cacheTime: 1000 * 60 * 60 * 24, // Cached for 24 hours @@ -98,23 +80,26 @@ sitemap.getPages = function(callback) { sitemap.maps.pages.toXML(callback); }; -sitemap.getCategories = function(callback) { - if (sitemap.maps.categories && sitemap.maps.categories.cache.length) { +sitemap.getCategories = function (callback) { + if ( + sitemap.maps.categories && + Date.now() < parseInt(sitemap.maps.categories.cacheSetTimestamp, 10) + parseInt(sitemap.maps.categories.cacheResetPeriod, 10) + ) { return sitemap.maps.categories.toXML(callback); } var categoryUrls = []; - categories.getCategoriesByPrivilege('categories:cid', 0, 'find', function(err, categoriesData) { + categories.getCategoriesByPrivilege('categories:cid', 0, 'find', function (err, categoriesData) { if (err) { return callback(err); } - categoriesData.forEach(function(category) { + categoriesData.forEach(function (category) { if (category) { categoryUrls.push({ url: '/category/' + category.slug, changefreq: 'weekly', - priority: '0.4' + priority: 0.4 }); } }); @@ -129,7 +114,7 @@ sitemap.getCategories = function(callback) { }); }; -sitemap.getTopicPage = function(page, callback) { +sitemap.getTopicPage = function (page, callback) { if (parseInt(page, 10) <= 0) { return callback(); } @@ -138,49 +123,52 @@ sitemap.getTopicPage = function(page, callback) { var min = (parseInt(page, 10) - 1) * numTopics; var max = min + numTopics; - if (sitemap.maps.topics[page-1] && sitemap.maps.topics[page-1].cache.length) { - return sitemap.maps.topics[page-1].toXML(callback); + if ( + sitemap.maps.topics[page - 1] && + Date.now() < parseInt(sitemap.maps.topics[page - 1].cacheSetTimestamp, 10) + parseInt(sitemap.maps.topics[page - 1].cacheResetPeriod, 10) + ) { + return sitemap.maps.topics[page - 1].toXML(callback); } var topicUrls = []; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('topics:recent', min, max, next); }, - function(tids, next) { + function (tids, next) { privileges.topics.filterTids('read', tids, 0, next); }, - function(tids, next) { + function (tids, next) { topics.getTopicsFields(tids, ['tid', 'title', 'slug', 'lastposttime'], next); } - ], function(err, topics) { + ], function (err, topics) { if (err) { return callback(err); } - topics.forEach(function(topic) { + topics.forEach(function (topic) { if (topic) { topicUrls.push({ url: '/topic/' + topic.slug, lastmodISO: utils.toISOString(topic.lastposttime), changefreq: 'daily', - priority: '0.6' + priority: 0.6 }); } }); - sitemap.maps.topics[page-1] = sm.createSitemap({ + sitemap.maps.topics[page - 1] = sm.createSitemap({ hostname: nconf.get('url'), cacheTime: 1000 * 60 * 60, // Cached for 1 hour urls: topicUrls }); - sitemap.maps.topics[page-1].toXML(callback); + sitemap.maps.topics[page - 1].toXML(callback); }); }; -sitemap.clearCache = function() { +sitemap.clearCache = function () { if (sitemap.obj) { sitemap.obj.clearCache(); } diff --git a/src/social.js b/src/social.js index fec8fb036c..0c71daadba 100644 --- a/src/social.js +++ b/src/social.js @@ -8,7 +8,7 @@ var social = {}; social.postSharing = null; -social.getPostSharing = function(callback) { +social.getPostSharing = function (callback) { if (social.postSharing) { return callback(null, social.postSharing); } @@ -32,16 +32,16 @@ social.getPostSharing = function(callback) { ]; async.waterfall([ - function(next) { + function (next) { plugins.fireHook('filter:social.posts', networks, next); }, - function(networks, next) { - db.getSetMembers('social:posts.activated', function(err, activated) { + function (networks, next) { + db.getSetMembers('social:posts.activated', function (err, activated) { if (err) { return next(err); } - networks.forEach(function(network, i) { + networks.forEach(function (network, i) { networks[i].activated = (activated.indexOf(network.id) !== -1); }); @@ -52,19 +52,19 @@ social.getPostSharing = function(callback) { ], callback); }; -social.getActivePostSharing = function(callback) { - social.getPostSharing(function(err, networks) { +social.getActivePostSharing = function (callback) { + social.getPostSharing(function (err, networks) { if (err) { return callback(err); } - networks = networks.filter(function(network) { + networks = networks.filter(function (network) { return network && network.activated; }); callback(null, networks); }); }; -social.setActivePostSharingNetworks = function(networkIDs, callback) { +social.setActivePostSharingNetworks = function (networkIDs, callback) { async.waterfall([ function (next) { db.delete('social:posts.activated', next); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 6dd22134ba..e59e15d55c 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -1,47 +1,46 @@ "use strict"; -var async = require('async'), - winston = require('winston'), - - - meta = require('../meta'), - plugins = require('../plugins'), - widgets = require('../widgets'), - user = require('../user'), - - logger = require('../logger'), - events = require('../events'), - emailer = require('../emailer'), - db = require('../database'), - analytics = require('../analytics'), - index = require('./index'), - - - SocketAdmin = { - user: require('./admin/user'), - categories: require('./admin/categories'), - groups: require('./admin/groups'), - tags: require('./admin/tags'), - rewards: require('./admin/rewards'), - navigation: require('./admin/navigation'), - rooms: require('./admin/rooms'), - social: require('./admin/social'), - themes: {}, - plugins: {}, - widgets: {}, - config: {}, - settings: {}, - email: {}, - analytics: {}, - logs: {} - }; +var async = require('async'); +var winston = require('winston'); +var nconf = require('nconf'); + +var meta = require('../meta'); +var plugins = require('../plugins'); +var widgets = require('../widgets'); +var user = require('../user'); +var logger = require('../logger'); +var events = require('../events'); +var emailer = require('../emailer'); +var db = require('../database'); +var analytics = require('../analytics'); +var index = require('./index'); + +var SocketAdmin = { + user: require('./admin/user'), + categories: require('./admin/categories'), + groups: require('./admin/groups'), + tags: require('./admin/tags'), + rewards: require('./admin/rewards'), + navigation: require('./admin/navigation'), + rooms: require('./admin/rooms'), + social: require('./admin/social'), + themes: {}, + plugins: {}, + widgets: {}, + config: {}, + settings: {}, + email: {}, + analytics: {}, + logs: {}, + errors: {} +}; -SocketAdmin.before = function(socket, method, data, next) { +SocketAdmin.before = function (socket, method, data, next) { if (!socket.uid) { return; } - user.isAdministrator(socket.uid, function(err, isAdmin) { + user.isAdministrator(socket.uid, function (err, isAdmin) { if (err || isAdmin) { return next(err); } @@ -50,23 +49,7 @@ SocketAdmin.before = function(socket, method, data, next) { }); }; -SocketAdmin.reload = function(socket, data, callback) { - events.log({ - type: 'reload', - uid: socket.uid, - ip: socket.ip - }); - if (process.send) { - process.send({ - action: 'reload' - }); - callback(); - } else { - meta.reload(callback); - } -}; - -SocketAdmin.restart = function(socket, data, callback) { +SocketAdmin.restart = function (socket, data, callback) { events.log({ type: 'restart', uid: socket.uid, @@ -76,21 +59,29 @@ SocketAdmin.restart = function(socket, data, callback) { callback(); }; -SocketAdmin.fireEvent = function(socket, data, callback) { +/** + * Reload deprecated as of v1.1.2+, remove in v2.x + */ +SocketAdmin.reload = SocketAdmin.restart; + +SocketAdmin.fireEvent = function (socket, data, callback) { index.server.emit(data.name, data.payload || {}); callback(); }; -SocketAdmin.themes.getInstalled = function(socket, data, callback) { +SocketAdmin.themes.getInstalled = function (socket, data, callback) { meta.themes.get(callback); }; -SocketAdmin.themes.set = function(socket, data, callback) { +SocketAdmin.themes.set = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } - var wrappedCallback = function(err) { + var wrappedCallback = function (err) { + if (err) { + return callback(err); + } meta.themes.set(data, callback); }; if (data.type === 'bootswatch') { @@ -100,22 +91,22 @@ SocketAdmin.themes.set = function(socket, data, callback) { } }; -SocketAdmin.plugins.toggleActive = function(socket, plugin_id, callback) { +SocketAdmin.plugins.toggleActive = function (socket, plugin_id, callback) { require('../posts/cache').reset(); plugins.toggleActive(plugin_id, callback); }; -SocketAdmin.plugins.toggleInstall = function(socket, data, callback) { +SocketAdmin.plugins.toggleInstall = function (socket, data, callback) { require('../posts/cache').reset(); plugins.toggleInstall(data.id, data.version, callback); }; -SocketAdmin.plugins.getActive = function(socket, data, callback) { +SocketAdmin.plugins.getActive = function (socket, data, callback) { plugins.getActive(callback); }; -SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) { - async.each(data, function(plugin, next) { +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 { @@ -124,11 +115,11 @@ SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) { }, callback); }; -SocketAdmin.plugins.upgrade = function(socket, data, callback) { +SocketAdmin.plugins.upgrade = function (socket, data, callback) { plugins.upgrade(data.id, data.version, callback); }; -SocketAdmin.widgets.set = function(socket, data, callback) { +SocketAdmin.widgets.set = function (socket, data, callback) { if(!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -136,12 +127,12 @@ SocketAdmin.widgets.set = function(socket, data, callback) { widgets.setArea(data, callback); }; -SocketAdmin.config.set = function(socket, data, callback) { +SocketAdmin.config.set = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } - meta.configs.set(data.key, data.value, function(err) { + meta.configs.set(data.key, data.value, function (err) { if (err) { return callback(err); } @@ -157,12 +148,12 @@ SocketAdmin.config.set = function(socket, data, callback) { }); }; -SocketAdmin.config.setMultiple = function(socket, data, callback) { +SocketAdmin.config.setMultiple = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } - meta.configs.setMultiple(data, function(err) { + meta.configs.setMultiple(data, function (err) { if(err) { return callback(err); } @@ -182,33 +173,34 @@ SocketAdmin.config.setMultiple = function(socket, data, callback) { }); }; -SocketAdmin.config.remove = function(socket, key, callback) { +SocketAdmin.config.remove = function (socket, key, callback) { meta.configs.remove(key); callback(); }; -SocketAdmin.settings.get = function(socket, data, callback) { +SocketAdmin.settings.get = function (socket, data, callback) { meta.settings.get(data.hash, callback); }; -SocketAdmin.settings.set = function(socket, data, callback) { +SocketAdmin.settings.set = function (socket, data, callback) { meta.settings.set(data.hash, data.values, callback); }; -SocketAdmin.settings.clearSitemapCache = function(socket, data, callback) { +SocketAdmin.settings.clearSitemapCache = function (socket, data, callback) { require('../sitemap').clearCache(); callback(); }; -SocketAdmin.email.test = function(socket, data, callback) { +SocketAdmin.email.test = function (socket, data, callback) { var site_title = meta.config.title || 'NodeBB'; emailer.send(data.template, socket.uid, { subject: '[' + site_title + '] Test Email', - site_title: site_title + site_title: site_title, + url: nconf.get('url') }, callback); }; -SocketAdmin.analytics.get = function(socket, data, callback) { +SocketAdmin.analytics.get = function (socket, data, callback) { // Default returns views from past 24 hours, by hour if (data.units === 'days') { data.amount = 30; @@ -219,25 +211,25 @@ SocketAdmin.analytics.get = function(socket, data, callback) { if (data && data.graph && data.units && data.amount) { if (data.graph === 'traffic') { async.parallel({ - uniqueVisitors: function(next) { + uniqueVisitors: function (next) { if (data.units === 'days') { analytics.getDailyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); } else { analytics.getHourlyStatsForSet('analytics:uniquevisitors', data.until || Date.now(), data.amount, next); } }, - pageviews: function(next) { + pageviews: function (next) { if (data.units === 'days') { analytics.getDailyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); } else { analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); } }, - monthlyPageViews: function(next) { + monthlyPageViews: function (next) { analytics.getMonthlyPageViews(next); } - }, function(err, data) { - data.pastDay = data.pageviews.reduce(function(a, b) {return parseInt(a, 10) + parseInt(b, 10);}); + }, function (err, data) { + data.pastDay = data.pageviews.reduce(function (a, b) {return parseInt(a, 10) + parseInt(b, 10);}); data.pageviews[data.pageviews.length - 1] = parseInt(data.pageviews[data.pageviews.length - 1], 10) + analytics.getUnwrittenPageviews(); callback(err, data); }); @@ -247,29 +239,19 @@ SocketAdmin.analytics.get = function(socket, data, callback) { } }; -SocketAdmin.logs.get = function(socket, data, callback) { +SocketAdmin.logs.get = function (socket, data, callback) { meta.logs.get(callback); }; -SocketAdmin.logs.clear = function(socket, data, callback) { +SocketAdmin.logs.clear = function (socket, data, callback) { meta.logs.clear(callback); }; -SocketAdmin.getMoreEvents = function(socket, next, callback) { - var start = parseInt(next, 10); - if (start < 0) { - return callback(null, {data: [], next: next}); - } - var stop = start + 10; - events.getEvents(start, stop, function(err, events) { - if (err) { - return callback(err); - } - callback(null, {events: events, next: stop + 1}); - }); +SocketAdmin.errors.clear = function (socket, data, callback) { + meta.errors.clear(callback); }; -SocketAdmin.deleteAllEvents = function(socket, data, callback) { +SocketAdmin.deleteAllEvents = function (socket, data, callback) { events.deleteAll(callback); }; diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 3b35847366..b90ec3e4b6 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -9,7 +9,7 @@ var privileges = require('../../privileges'); var plugins = require('../../plugins'); var Categories = {}; -Categories.create = function(socket, data, callback) { +Categories.create = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -17,18 +17,18 @@ Categories.create = function(socket, data, callback) { categories.create(data, callback); }; -Categories.getAll = function(socket, data, callback) { +Categories.getAll = function (socket, data, callback) { async.waterfall([ async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), async.apply(categories.getCategoriesData), - function(categories, next) { + function (categories, next) { //Hook changes, there is no req, and res plugins.fireHook('filter:admin.categories.get', {categories: categories}, next); }, - function(result, next){ + function (result, next){ next(null, categories.getTree(result.categories, 0)); } - ], function(err, categoriesTree) { + ], function (err, categoriesTree) { if (err) { return callback(err); } @@ -37,15 +37,15 @@ Categories.getAll = function(socket, data, callback) { }); }; -Categories.getNames = function(socket, data, callback) { +Categories.getNames = function (socket, data, callback) { categories.getAllCategoryFields(['cid', 'name'], callback); }; -Categories.purge = function(socket, cid, callback) { +Categories.purge = function (socket, cid, callback) { categories.purge(cid, socket.uid, callback); }; -Categories.update = function(socket, data, callback) { +Categories.update = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -53,13 +53,13 @@ Categories.update = function(socket, data, callback) { categories.update(data, callback); }; -Categories.setPrivilege = function(socket, data, callback) { - if(!data) { +Categories.setPrivilege = function (socket, data, callback) { + if (!data) { return callback(new Error('[[error:invalid-data]]')); } if (Array.isArray(data.privilege)) { - async.each(data.privilege, function(privilege, next) { + async.each(data.privilege, function (privilege, next) { groups[data.set ? 'join' : 'leave']('cid:' + data.cid + ':privileges:' + privilege, data.member, next); }, callback); } else { @@ -67,49 +67,40 @@ Categories.setPrivilege = function(socket, data, callback) { } }; -Categories.getPrivilegeSettings = function(socket, cid, callback) { +Categories.getPrivilegeSettings = function (socket, cid, callback) { privileges.categories.list(cid, callback); }; -Categories.copyPrivilegesToChildren = function(socket, cid, callback) { - async.parallel({ - category: function(next) { - categories.getCategories([cid], socket.uid, next); - }, - privileges: function(next) { - privileges.categories.list(cid, next); - } - }, function(err, results) { +Categories.copyPrivilegesToChildren = function (socket, cid, callback) { + categories.getCategories([cid], socket.uid, function (err, categories) { if (err) { return callback(err); } - var category = results.category[0]; + var category = categories[0]; - async.eachSeries(category.children, function(child, next) { - copyPrivilegesToChildrenRecursive(child, results.privileges.groups, next); + async.eachSeries(category.children, function (child, next) { + copyPrivilegesToChildrenRecursive(cid, child, next); }, callback); }); }; -function copyPrivilegesToChildrenRecursive(category, privilegeGroups, callback) { - async.eachSeries(privilegeGroups, function(privGroup, next) { - var privs = Object.keys(privGroup.privileges); - async.each(privs, function(privilege, next) { - var isSet = privGroup.privileges[privilege]; - groups[isSet ? 'join' : 'leave']('cid:' + category.cid + ':privileges:' + privilege, privGroup.name, next); - }, next); - }, function(err) { +function copyPrivilegesToChildrenRecursive(parentCid, category, callback) { + categories.copyPrivilegesFrom(parentCid, category.cid, function (err) { if (err) { return callback(err); } - async.eachSeries(category.children, function(child, next) { - copyPrivilegesToChildrenRecursive(child, privilegeGroups, next); + async.eachSeries(category.children, function (child, next) { + copyPrivilegesToChildrenRecursive(parentCid, child, next); }, callback); }); } -Categories.copySettingsFrom = function(socket, data, callback) { - categories.copySettingsFrom(data.fromCid, data.toCid, callback); +Categories.copySettingsFrom = function (socket, data, callback) { + categories.copySettingsFrom(data.fromCid, data.toCid, true, callback); +}; + +Categories.copyPrivilegesFrom = function (socket, data, callback) { + categories.copyPrivilegesFrom(data.fromCid, data.toCid, callback); }; module.exports = Categories; \ No newline at end of file diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index 206604ec8f..6b9e404ae1 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -1,12 +1,15 @@ "use strict"; var async = require('async'); -var groups = require('../../groups'), - Groups = {}; +var groups = require('../../groups'); -Groups.create = function(socket, data, callback) { - if(!data) { +var Groups = {}; + +Groups.create = function (socket, data, callback) { + if (!data) { return callback(new Error('[[error:invalid-data]]')); + } else if (groups.isPrivilegeGroup(data.name)) { + return callback(new Error('[[error:invalid-group-name]]')); } groups.create({ @@ -16,7 +19,7 @@ Groups.create = function(socket, data, callback) { }, callback); }; -Groups.join = function(socket, data, callback) { +Groups.join = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -34,7 +37,7 @@ Groups.join = function(socket, data, callback) { ], callback); }; -Groups.leave = function(socket, data, callback) { +Groups.leave = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -56,7 +59,7 @@ Groups.leave = function(socket, data, callback) { ], callback); }; -Groups.update = function(socket, data, callback) { +Groups.update = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } diff --git a/src/socket.io/admin/navigation.js b/src/socket.io/admin/navigation.js index 0f3f7c168e..2f4d9817b4 100644 --- a/src/socket.io/admin/navigation.js +++ b/src/socket.io/admin/navigation.js @@ -3,7 +3,7 @@ var navigationAdmin = require('../../navigation/admin'), SocketNavigation = {}; -SocketNavigation.save = function(socket, data, callback) { +SocketNavigation.save = function (socket, data, callback) { navigationAdmin.save(data, callback); }; diff --git a/src/socket.io/admin/rewards.js b/src/socket.io/admin/rewards.js index b130a25455..266d5f532f 100644 --- a/src/socket.io/admin/rewards.js +++ b/src/socket.io/admin/rewards.js @@ -3,11 +3,11 @@ var rewardsAdmin = require('../../rewards/admin'), SocketRewards = {}; -SocketRewards.save = function(socket, data, callback) { +SocketRewards.save = function (socket, data, callback) { rewardsAdmin.save(data, callback); }; -SocketRewards.delete = function(socket, data, callback) { +SocketRewards.delete = function (socket, data, callback) { rewardsAdmin.delete(data, callback); }; diff --git a/src/socket.io/admin/rooms.js b/src/socket.io/admin/rooms.js index 50ba4661de..a4ebfeb44e 100644 --- a/src/socket.io/admin/rooms.js +++ b/src/socket.io/admin/rooms.js @@ -16,8 +16,8 @@ var SocketRooms = { }; -pubsub.on('sync:stats:start', function() { - getLocalStats(function(err, stats) { +pubsub.on('sync:stats:start', function () { + SocketRooms.getLocalStats(function (err, stats) { if (err) { return winston.error(err); } @@ -25,11 +25,11 @@ pubsub.on('sync:stats:start', function() { }); }); -pubsub.on('sync:stats:end', function(data) { +pubsub.on('sync:stats:end', function (data) { stats[data.id] = data.stats; }); -pubsub.on('sync:stats:guests', function() { +pubsub.on('sync:stats:guests', function () { var io = require('../index').server; var roomClients = io.sockets.adapter.rooms; @@ -37,23 +37,23 @@ pubsub.on('sync:stats:guests', function() { pubsub.publish('sync:stats:guests:end', guestCount); }); -SocketRooms.getTotalGuestCount = function(callback) { +SocketRooms.getTotalGuestCount = function (callback) { var count = 0; - pubsub.on('sync:stats:guests:end', function(guestCount) { + pubsub.on('sync:stats:guests:end', function (guestCount) { count += guestCount; }); pubsub.publish('sync:stats:guests'); - setTimeout(function() { + setTimeout(function () { pubsub.removeAllListeners('sync:stats:guests:end'); callback(null, count); }, 100); -} +}; -SocketRooms.getAll = function(socket, data, callback) { +SocketRooms.getAll = function (socket, data, callback) { pubsub.publish('sync:stats:start'); totals.onlineGuestCount = 0; @@ -79,7 +79,7 @@ SocketRooms.getAll = function(socket, data, callback) { totals.users.topics += stats[instance].users.topics; totals.users.category += stats[instance].users.category; - stats[instance].topics.forEach(function(topic) { + stats[instance].topics.forEach(function (topic) { totals.topics[topic.tid] = totals.topics[topic.tid] || {count: 0, tid: topic.tid}; totals.topics[topic.tid].count += topic.count; }); @@ -87,27 +87,27 @@ SocketRooms.getAll = function(socket, data, callback) { } var topTenTopics = []; - Object.keys(totals.topics).forEach(function(tid) { + Object.keys(totals.topics).forEach(function (tid) { topTenTopics.push({tid: tid, count: totals.topics[tid].count}); }); - topTenTopics = topTenTopics.sort(function(a, b) { + topTenTopics = topTenTopics.sort(function (a, b) { return b.count - a.count; }).slice(0, 10); - var topTenTids = topTenTopics.map(function(topic) { + var topTenTids = topTenTopics.map(function (topic) { return topic.tid; }); - topics.getTopicsFields(topTenTids, ['title'], function(err, titles) { + topics.getTopicsFields(topTenTids, ['title'], function (err, titles) { if (err) { return callback(err); } totals.topics = {}; - topTenTopics.forEach(function(topic, index) { + topTenTopics.forEach(function (topic, index) { totals.topics[topic.tid] = { value: topic.count || 0, - title: validator.escape(titles[index].title) + title: validator.escape(String(titles[index].title)) }; }); @@ -115,7 +115,7 @@ SocketRooms.getAll = function(socket, data, callback) { }); }; -SocketRooms.getOnlineUserCount = function(io) { +SocketRooms.getOnlineUserCount = function (io) { if (!io) { return 0; } @@ -129,7 +129,7 @@ SocketRooms.getOnlineUserCount = function(io) { return count; }; -function getLocalStats(callback) { +SocketRooms.getLocalStats = function (callback) { var io = require('../index').server; if (!io) { @@ -144,7 +144,7 @@ function getLocalStats(callback) { users: { categories: roomClients.categories ? roomClients.categories.length : 0, recent: roomClients.recent_topics ? roomClients.recent_topics.length : 0, - unread: roomClients.unread_topics ? roomClients.unread_topics.length: 0, + unread: roomClients.unread_topics ? roomClients.unread_topics.length : 0, topics: 0, category: 0 }, @@ -166,13 +166,13 @@ function getLocalStats(callback) { } } - topTenTopics = topTenTopics.sort(function(a, b) { + topTenTopics = topTenTopics.sort(function (a, b) { return b.count - a.count; }).slice(0, 10); socketData.topics = topTenTopics; callback(null, socketData); -} +}; module.exports = SocketRooms; \ No newline at end of file diff --git a/src/socket.io/admin/social.js b/src/socket.io/admin/social.js index 68b3241c64..77227ea760 100644 --- a/src/socket.io/admin/social.js +++ b/src/socket.io/admin/social.js @@ -3,7 +3,7 @@ var social = require('../../social'), SocketSocial = {}; -SocketSocial.savePostSharingNetworks = function(socket, data, callback) { +SocketSocial.savePostSharingNetworks = function (socket, data, callback) { social.setActivePostSharingNetworks(data, callback); }; diff --git a/src/socket.io/admin/tags.js b/src/socket.io/admin/tags.js index ad7ffa7201..c00740d9d5 100644 --- a/src/socket.io/admin/tags.js +++ b/src/socket.io/admin/tags.js @@ -1,9 +1,17 @@ "use strict"; -var topics = require('../../topics'), - Tags = {}; +var topics = require('../../topics'); +var Tags = {}; -Tags.update = function(socket, data, callback) { +Tags.create = function (socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + topics.createEmptyTag(data.tag, callback); +}; + +Tags.update = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -11,7 +19,7 @@ Tags.update = function(socket, data, callback) { topics.updateTag(data.tag, data, callback); }; -Tags.deleteTags = function(socket, data, callback) { +Tags.deleteTags = function (socket, data, callback) { topics.deleteTags(data.tags, callback); }; diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 2633425dc4..3d0a114695 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -1,7 +1,8 @@ "use strict"; - var async = require('async'); +var validator = require('validator'); + var db = require('../../database'); var groups = require('../../groups'); var user = require('../../user'); @@ -10,35 +11,35 @@ var meta = require('../../meta'); var User = {}; -User.makeAdmins = function(socket, uids, callback) { +User.makeAdmins = function (socket, uids, callback) { if(!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } - user.getUsersFields(uids, ['banned'], function(err, userData) { + user.getUsersFields(uids, ['banned'], function (err, userData) { if (err) { return callback(err); } - for(var i=0; i<userData.length; i++) { + for(var i = 0; i < userData.length; i++) { if (userData[i] && parseInt(userData[i].banned, 10) === 1) { return callback(new Error('[[error:cant-make-banned-users-admin]]')); } } - async.each(uids, function(uid, next) { + async.each(uids, function (uid, next) { groups.join('administrators', uid, next); }, callback); }); }; -User.removeAdmins = function(socket, uids, callback) { +User.removeAdmins = function (socket, uids, callback) { if(!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } - async.eachSeries(uids, function(uid, next) { - groups.getMemberCount('administrators', function(err, count) { + async.eachSeries(uids, function (uid, next) { + groups.getMemberCount('administrators', function (err, count) { if (err) { return next(err); } @@ -52,7 +53,7 @@ User.removeAdmins = function(socket, uids, callback) { }, callback); }; -User.createUser = function(socket, userData, callback) { +User.createUser = function (socket, userData, callback) { if (!userData) { return callback(new Error('[[error:invalid-data]]')); } @@ -60,7 +61,7 @@ User.createUser = function(socket, userData, callback) { }; -User.resetLockouts = function(socket, uids, callback) { +User.resetLockouts = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } @@ -68,7 +69,7 @@ User.resetLockouts = function(socket, uids, callback) { async.each(uids, user.auth.resetLockout, callback); }; -User.resetFlags = function(socket, uids, callback) { +User.resetFlags = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } @@ -76,18 +77,18 @@ User.resetFlags = function(socket, uids, callback) { user.resetFlags(uids, callback); }; -User.validateEmail = function(socket, uids, callback) { +User.validateEmail = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } - uids = uids.filter(function(uid) { + uids = uids.filter(function (uid) { return parseInt(uid, 10); }); - async.each(uids, function(uid, next) { + async.each(uids, function (uid, next) { user.setUserField(uid, 'email:confirmed', 1, next); - }, function(err) { + }, function (err) { if (err) { return callback(err); } @@ -95,7 +96,7 @@ User.validateEmail = function(socket, uids, callback) { }); }; -User.sendValidationEmail = function(socket, uids, callback) { +User.sendValidationEmail = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } @@ -104,12 +105,12 @@ User.sendValidationEmail = function(socket, uids, callback) { return callback(new Error('[[error:email-confirmations-are-disabled]]')); } - user.getUsersFields(uids, ['uid', 'email'], function(err, usersData) { + user.getUsersFields(uids, ['uid', 'email'], function (err, usersData) { if (err) { return callback(err); } - async.eachLimit(usersData, 50, function(userData, next) { + async.eachLimit(usersData, 50, function (userData, next) { if (userData.email && userData.uid) { user.email.sendValidationEmail(userData.uid, userData.email, next); } else { @@ -119,17 +120,17 @@ User.sendValidationEmail = function(socket, uids, callback) { }); }; -User.sendPasswordResetEmail = function(socket, uids, callback) { +User.sendPasswordResetEmail = function (socket, uids, callback) { if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } - uids = uids.filter(function(uid) { + uids = uids.filter(function (uid) { return parseInt(uid, 10); }); - async.each(uids, function(uid, next) { - user.getUserFields(uid, ['email', 'username'], function(err, userData) { + async.each(uids, function (uid, next) { + user.getUserFields(uid, ['email', 'username'], function (err, userData) { if (err) { return next(err); } @@ -141,12 +142,24 @@ User.sendPasswordResetEmail = function(socket, uids, callback) { }, callback); }; -User.deleteUsers = function(socket, uids, callback) { - if(!Array.isArray(uids)) { +User.deleteUsers = function (socket, uids, callback) { + deleteUsers(socket, uids, function (uid, next) { + user.deleteAccount(uid, next); + }, callback); +}; + +User.deleteUsersAndContent = function (socket, uids, callback) { + deleteUsers(socket, uids, function (uid, next) { + user.delete(socket.uid, uid, next); + }, callback); +}; + +function deleteUsers(socket, uids, method, callback) { + if (!Array.isArray(uids)) { return callback(new Error('[[error:invalid-data]]')); } - async.each(uids, function(uid, next) { + async.each(uids, function (uid, next) { async.waterfall([ function (next) { user.isAdministrator(uid, next); @@ -156,7 +169,7 @@ User.deleteUsers = function(socket, uids, callback) { return next(new Error('[[error:cant-delete-other-admins]]')); } - user.delete(socket.uid, uid, next); + method(uid, next); }, function (next) { events.log({ @@ -169,10 +182,10 @@ User.deleteUsers = function(socket, uids, callback) { } ], next); }, callback); -}; +} -User.search = function(socket, data, callback) { - user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function(err, searchData) { +User.search = function (socket, data, callback) { + user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function (err, searchData) { if (err) { return callback(err); } @@ -181,29 +194,21 @@ User.search = function(socket, data, callback) { } var userData = searchData.users; - var uids = userData.map(function(user) { + var uids = userData.map(function (user) { return user && user.uid; }); - async.parallel({ - users: function(next) { - user.getUsersFields(uids, ['email'], next); - }, - flagCounts: function(next) { - var sets = uids.map(function(uid) { - return 'uid:' + uid + ':flagged_by'; - }); - db.setsCount(sets, next); - } - }, function(err, results) { + user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate'], function (err, userInfo) { if (err) { return callback(err); } - userData.forEach(function(user, index) { - if (user) { - user.email = (results.users[index] && results.users[index].email) || ''; - user.flags = results.flagCounts[index] || 0; + userData.forEach(function (user, index) { + if (user && userInfo[index]) { + user.email = validator.escape(String(userInfo[index].email || '')); + user.flags = userInfo[index].flags || 0; + user.lastonlineISO = userInfo[index].lastonlineISO; + user.joindateISO = userInfo[index].joindateISO; } }); @@ -212,17 +217,20 @@ User.search = function(socket, data, callback) { }); }; -User.deleteInvitation = function(socket, data, callback) { +User.deleteInvitation = function (socket, data, callback) { user.deleteInvitation(data.invitedBy, data.email, callback); }; -User.acceptRegistration = function(socket, data, callback) { +User.acceptRegistration = function (socket, data, callback) { user.acceptRegistration(data.username, callback); }; -User.rejectRegistration = function(socket, data, callback) { +User.rejectRegistration = function (socket, data, callback) { user.rejectRegistration(data.username, callback); }; +User.restartJobs = function (socket, data, callback) { + user.startJobs(callback); +}; module.exports = User; diff --git a/src/socket.io/blacklist.js b/src/socket.io/blacklist.js index f4158dc94b..a05d632f3f 100644 --- a/src/socket.io/blacklist.js +++ b/src/socket.io/blacklist.js @@ -9,12 +9,12 @@ var meta = require('../meta'); var SocketBlacklist = {}; -SocketBlacklist.validate = function(socket, data, callback) { +SocketBlacklist.validate = function (socket, data, callback) { meta.blacklist.validate(data.rules, callback); }; -SocketBlacklist.save = function(socket, rules, callback) { - user.isAdminOrGlobalMod(socket.uid, function(err, isAdminOrGlobalMod) { +SocketBlacklist.save = function (socket, rules, callback) { + user.isAdminOrGlobalMod(socket.uid, function (err, isAdminOrGlobalMod) { if (err || !isAdminOrGlobalMod) { return callback(err || new Error('[[error:no-privileges]]')); } diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 262a5e8a41..acc8652057 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,6 +1,6 @@ 'use strict'; -var async = require('async'); +var async = require('async'); var db = require('../database'); var categories = require('../categories'); var privileges = require('../privileges'); @@ -10,25 +10,25 @@ var apiController = require('../controllers/api'); var SocketCategories = {}; -SocketCategories.getRecentReplies = function(socket, cid, callback) { +SocketCategories.getRecentReplies = function (socket, cid, callback) { categories.getRecentReplies(cid, socket.uid, 4, callback); }; -SocketCategories.get = function(socket, data, callback) { +SocketCategories.get = function (socket, data, callback) { async.parallel({ isAdmin: async.apply(user.isAdministrator, socket.uid), - categories: function(next) { + categories: function (next) { async.waterfall([ async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), async.apply(categories.getCategoriesData), ], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.categories = results.categories.filter(function(category) { + results.categories = results.categories.filter(function (category) { return category && (!category.disabled || results.isAdmin); }); @@ -36,41 +36,41 @@ SocketCategories.get = function(socket, data, callback) { }); }; -SocketCategories.getWatchedCategories = function(socket, data, callback) { +SocketCategories.getWatchedCategories = function (socket, data, callback) { async.parallel({ categories: async.apply(categories.getCategoriesByPrivilege, socket.uid, 'find'), ignoredCids: async.apply(user.getIgnoredCategories, socket.uid) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - var watchedCategories = results.categories.filter(function(category) { + var watchedCategories = results.categories.filter(function (category) { return category && results.ignoredCids.indexOf(category.cid.toString()) === -1; }); callback(null, watchedCategories); }); }; -SocketCategories.loadMore = function(socket, data, callback) { +SocketCategories.loadMore = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.categories.get(data.cid, socket.uid, next); }, - settings: function(next) { + settings: function (next) { user.getSettings(socket.uid, next); }, - targetUid: function(next) { + targetUid: function (next) { if (data.author) { user.getUidByUserslug(data.author, next); } else { next(); } } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -80,12 +80,12 @@ SocketCategories.loadMore = function(socket, data, callback) { } var infScrollTopicsPerPage = 20; - var set = 'cid:' + data.cid + ':tids', - reverse = false; + var set = 'cid:' + data.cid + ':tids'; + var reverse = false; - if (results.settings.categoryTopicSort === 'newest_to_oldest') { + if (data.categoryTopicSort === 'newest_to_oldest') { reverse = true; - } else if (results.settings.categoryTopicSort === 'most_posts') { + } else if (data.categoryTopicSort === 'most_posts') { reverse = true; set = 'cid:' + data.cid + ':tids:posts'; } @@ -105,6 +105,10 @@ SocketCategories.loadMore = function(socket, data, callback) { set = 'cid:' + data.cid + ':uid:' + results.targetUid + ':tids'; } + if (data.tag) { + set = [set, 'tag:' + data.tag + ':topics']; + } + categories.getCategoryTopics({ cid: data.cid, set: set, @@ -114,7 +118,7 @@ SocketCategories.loadMore = function(socket, data, callback) { uid: socket.uid, targetUid: results.targetUid, settings: results.settings - }, function(err, data) { + }, function (err, data) { if (err) { return callback(err); } @@ -132,22 +136,22 @@ SocketCategories.loadMore = function(socket, data, callback) { }); }; -SocketCategories.getPageCount = function(socket, cid, callback) { +SocketCategories.getPageCount = function (socket, cid, callback) { categories.getPageCount(cid, socket.uid, callback); }; -SocketCategories.getTopicCount = function(socket, cid, callback) { +SocketCategories.getTopicCount = function (socket, cid, callback) { categories.getCategoryField(cid, 'topic_count', callback); }; -SocketCategories.getCategoriesByPrivilege = function(socket, privilege, callback) { +SocketCategories.getCategoriesByPrivilege = function (socket, privilege, callback) { categories.getCategoriesByPrivilege('categories:cid', socket.uid, privilege, callback); }; -SocketCategories.getMoveCategories = function(socket, data, callback) { +SocketCategories.getMoveCategories = function (socket, data, callback) { async.parallel({ isAdmin: async.apply(user.isAdministrator, socket.uid), - categories: function(next) { + categories: function (next) { async.waterfall([ function (next) { db.getSortedSetRange('cid:0:children', 0, -1, next); @@ -157,12 +161,12 @@ SocketCategories.getMoveCategories = function(socket, data, callback) { } ], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.categories = results.categories.filter(function(category) { + results.categories = results.categories.filter(function (category) { return category && (!category.disabled || results.isAdmin) && !category.link; }); @@ -170,30 +174,59 @@ SocketCategories.getMoveCategories = function(socket, data, callback) { }); }; -SocketCategories.watch = function(socket, cid, callback) { - user.watchCategory(socket.uid, cid, function(err) { - if (err) { - return callback(err); - } - topics.pushUnreadCount(socket.uid, callback); - }); +SocketCategories.watch = function (socket, cid, callback) { + ignoreOrWatch(user.watchCategory, socket, cid, callback); }; -SocketCategories.ignore = function(socket, cid, callback) { - user.ignoreCategory(socket.uid, cid, function(err) { - if (err) { - return callback(err); - } - topics.pushUnreadCount(socket.uid, callback); - }); +SocketCategories.ignore = function (socket, cid, callback) { + ignoreOrWatch(user.ignoreCategory, socket, cid, callback); }; -SocketCategories.isModerator = function(socket, cid, callback) { +function ignoreOrWatch(fn, socket, cid, callback) { + async.waterfall([ + function (next) { + db.getSortedSetRange('categories:cid', 0, -1, next); + }, + function (cids, next) { + categories.getCategoriesFields(cids, ['cid', 'parentCid'], next); + }, + function (categoryData, next) { + categoryData.forEach(function (c) { + c.cid = parseInt(c.cid, 10); + c.parentCid = parseInt(c.parentCid, 10); + }); + + var cids = [parseInt(cid, 10)]; + + // filter to subcategories of cid + + var any = true; + while (any) { + any = false; + categoryData.forEach(function (c) { + if (cids.indexOf(c.cid) === -1 && cids.indexOf(c.parentCid) !== -1) { + cids.push(c.cid); + any = true; + } + }); + } + + async.each(cids, function (cid, next) { + fn(socket.uid, cid, next); + }, next); + }, + function (next) { + topics.pushUnreadCount(socket.uid, next); + } + ], callback); +} + +SocketCategories.isModerator = function (socket, cid, callback) { user.isModerator(socket.uid, cid, callback); }; -SocketCategories.getCategory = function(socket, cid, callback) { - apiController.getObjectByType(socket.uid, 'category', cid, callback); +SocketCategories.getCategory = function (socket, cid, callback) { + apiController.getCategoryData(cid, socket.uid, callback); }; module.exports = SocketCategories; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index a54db76b05..25d92003e9 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -5,19 +5,20 @@ var async = require('async'), groups = require('../groups'), meta = require('../meta'), user = require('../user'), + utils = require('../../public/src/utils'), groupsController = require('../controllers/groups'), SocketGroups = {}; -SocketGroups.before = function(socket, method, data, next) { +SocketGroups.before = function (socket, method, data, next) { if (!data) { return next(new Error('[[error:invalid-data]]')); } next(); }; -SocketGroups.join = function(socket, data, callback) { +SocketGroups.join = function (socket, data, callback) { if (!parseInt(socket.uid, 10)) { return callback(new Error('[[error:invalid-uid]]')); } @@ -26,7 +27,7 @@ SocketGroups.join = function(socket, data, callback) { return callback(new Error('[[error:not-allowed]]')); } - groups.exists(data.groupName, function(err, exists) { + groups.exists(data.groupName, function (err, exists) { if (err || !exists) { return callback(err || new Error('[[error:no-group]]')); } @@ -38,7 +39,7 @@ SocketGroups.join = function(socket, data, callback) { async.parallel({ isAdmin: async.apply(user.isAdministrator, socket.uid), groupData: async.apply(groups.getGroupData, data.groupName) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -56,7 +57,7 @@ SocketGroups.join = function(socket, data, callback) { }); }; -SocketGroups.leave = function(socket, data, callback) { +SocketGroups.leave = function (socket, data, callback) { if (!parseInt(socket.uid, 10)) { return callback(new Error('[[error:invalid-uid]]')); } @@ -73,7 +74,7 @@ function isOwner(next) { async.parallel({ isAdmin: async.apply(user.isAdministrator, socket.uid), isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName) - }, function(err, results) { + }, function (err, results) { if (err || (!isOwner && !results.isAdmin)) { return callback(err || new Error('[[error:no-privileges]]')); } @@ -84,7 +85,7 @@ function isOwner(next) { function isInvited(next) { return function (socket, data, callback) { - groups.isInvited(socket.uid, data.groupName, function(err, invited) { + groups.isInvited(socket.uid, data.groupName, function (err, invited) { if (err || !invited) { return callback(err || new Error('[[error:not-invited]]')); } @@ -93,70 +94,92 @@ function isInvited(next) { }; } -SocketGroups.grant = isOwner(function(socket, data, callback) { +SocketGroups.grant = isOwner(function (socket, data, callback) { groups.ownership.grant(data.toUid, data.groupName, callback); }); -SocketGroups.rescind = isOwner(function(socket, data, callback) { +SocketGroups.rescind = isOwner(function (socket, data, callback) { groups.ownership.rescind(data.toUid, data.groupName, callback); }); -SocketGroups.accept = isOwner(function(socket, data, callback) { +SocketGroups.accept = isOwner(function (socket, data, callback) { groups.acceptMembership(data.groupName, data.toUid, callback); }); -SocketGroups.reject = isOwner(function(socket, data, callback) { +SocketGroups.reject = isOwner(function (socket, data, callback) { groups.rejectMembership(data.groupName, data.toUid, callback); }); -SocketGroups.acceptAll = isOwner(function(socket, data, callback) { +SocketGroups.acceptAll = isOwner(function (socket, data, callback) { acceptRejectAll(groups.acceptMembership, socket, data, callback); }); -SocketGroups.rejectAll = isOwner(function(socket, data, callback) { +SocketGroups.rejectAll = isOwner(function (socket, data, callback) { acceptRejectAll(groups.rejectMembership, socket, data, callback); }); function acceptRejectAll(method, socket, data, callback) { async.waterfall([ - function(next) { + function (next) { groups.getPending(data.groupName, next); }, - function(uids, next) { - async.each(uids, function(uid, next) { + function (uids, next) { + async.each(uids, function (uid, next) { method(data.groupName, uid, next); }, next); } ], callback); } -SocketGroups.issueInvite = isOwner(function(socket, data, callback) { +SocketGroups.issueInvite = isOwner(function (socket, data, callback) { groups.invite(data.groupName, data.toUid, callback); }); -SocketGroups.rescindInvite = isOwner(function(socket, data, callback) { +SocketGroups.issueMassInvite = isOwner(function (socket, data, callback) { + if (!data || !data.usernames || !data.groupName) { + return callback(new Error('[[error:invalid-data]]')); + } + var usernames = data.usernames.split(','); + usernames = usernames.map(function (username) { + return username && username.trim(); + }); + user.getUidsByUsernames(usernames, function (err, uids) { + if (err) { + return callback(err); + } + uids = uids.filter(function (uid) { + return !!uid && parseInt(uid, 10); + }); + + async.eachSeries(uids, function (uid, next) { + groups.invite(data.groupName, uid, next); + }, callback); + }); +}); + +SocketGroups.rescindInvite = isOwner(function (socket, data, callback) { groups.rejectMembership(data.groupName, data.toUid, callback); }); -SocketGroups.acceptInvite = isInvited(function(socket, data, callback) { +SocketGroups.acceptInvite = isInvited(function (socket, data, callback) { groups.acceptMembership(data.groupName, socket.uid, callback); }); -SocketGroups.rejectInvite = isInvited(function(socket, data, callback) { +SocketGroups.rejectInvite = isInvited(function (socket, data, callback) { groups.rejectMembership(data.groupName, socket.uid, callback); }); -SocketGroups.update = isOwner(function(socket, data, callback) { +SocketGroups.update = isOwner(function (socket, data, callback) { groups.update(data.groupName, data.values, callback); }); -SocketGroups.kick = isOwner(function(socket, data, callback) { +SocketGroups.kick = isOwner(function (socket, data, callback) { if (socket.uid === parseInt(data.uid, 10)) { return callback(new Error('[[error:cant-kick-self]]')); } - groups.ownership.isOwner(data.uid, data.groupName, function(err, isOwner) { + groups.ownership.isOwner(data.uid, data.groupName, function (err, isOwner) { if (err) { return callback(err); } @@ -165,11 +188,13 @@ SocketGroups.kick = isOwner(function(socket, data, callback) { }); -SocketGroups.create = function(socket, data, callback) { +SocketGroups.create = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } else if (parseInt(meta.config.allowGroupCreation, 10) !== 1) { return callback(new Error('[[error:group-creation-disabled]]')); + } else if (groups.isPrivilegeGroup(data.name)) { + return callback(new Error('[[error:invalid-group-name]]')); } @@ -177,7 +202,7 @@ SocketGroups.create = function(socket, data, callback) { groups.create(data, callback); }; -SocketGroups.delete = function(socket, data, callback) { +SocketGroups.delete = function (socket, data, callback) { if (data.groupName === 'administrators' || data.groupName === 'registered-users' || data.groupName === 'Global Moderators') { @@ -187,7 +212,7 @@ SocketGroups.delete = function(socket, data, callback) { async.parallel({ isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName), isAdmin: async.apply(user.isAdministrator, socket.uid) - }, function(err, checks) { + }, function (err, checks) { if (err) { return callback(err); } @@ -199,12 +224,12 @@ SocketGroups.delete = function(socket, data, callback) { }); }; -SocketGroups.search = function(socket, data, callback) { +SocketGroups.search = function (socket, data, callback) { data.options = data.options || {}; if (!data.query) { var groupsPerPage = 15; - groupsController.getGroupsFromSet(socket.uid, data.options.sort, 0, groupsPerPage - 1, function(err, data) { + groupsController.getGroupsFromSet(socket.uid, data.options.sort, 0, groupsPerPage - 1, function (err, data) { callback(err, !err ? data.groups : null); }); return; @@ -213,28 +238,28 @@ SocketGroups.search = function(socket, data, callback) { groups.search(data.query, data.options || {}, callback); }; -SocketGroups.loadMore = function(socket, data, callback) { - if (!data.sort || !data.after) { +SocketGroups.loadMore = function (socket, data, callback) { + if (!data.sort || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { return callback(); } var groupsPerPage = 9; - var start = parseInt(data.after); + var start = parseInt(data.after, 10); var stop = start + groupsPerPage - 1; groupsController.getGroupsFromSet(socket.uid, data.sort, start, stop, callback); }; -SocketGroups.searchMembers = function(socket, data, callback) { +SocketGroups.searchMembers = function (socket, data, callback) { data.uid = socket.uid; groups.searchMembers(data, callback); }; -SocketGroups.loadMoreMembers = function(socket, data, callback) { - if (!data.groupName || !parseInt(data.after, 10)) { +SocketGroups.loadMoreMembers = function (socket, data, callback) { + if (!data.groupName || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { return callback(new Error('[[error:invalid-data]]')); } data.after = parseInt(data.after, 10); - user.getUsersFromSet('group:' + data.groupName + ':members', socket.uid, data.after, data.after + 9, function(err, users) { + user.getUsersFromSet('group:' + data.groupName + ':members', socket.uid, data.after, data.after + 9, function (err, users) { if (err) { return callback(err); } @@ -245,12 +270,12 @@ SocketGroups.loadMoreMembers = function(socket, data, callback) { SocketGroups.cover = {}; -SocketGroups.cover.update = function(socket, data, callback) { +SocketGroups.cover.update = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } - groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + groups.ownership.isOwner(socket.uid, data.groupName, function (err, isOwner) { if (err || !isOwner) { return callback(err || new Error('[[error:no-privileges]]')); } @@ -259,12 +284,12 @@ SocketGroups.cover.update = function(socket, data, callback) { }); }; -SocketGroups.cover.remove = function(socket, data, callback) { +SocketGroups.cover.remove = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } - groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + groups.ownership.isOwner(socket.uid, data.groupName, function (err, isOwner) { if (err || !isOwner) { return callback(err || new Error('[[error:no-privileges]]')); } diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 12a0e9d9b5..54c8152199 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -3,8 +3,8 @@ var async = require('async'); var winston = require('winston'); var S = require('string'); -var nconf = require('nconf'); +var db = require('../database'); var websockets = require('./index'); var user = require('../user'); var posts = require('../posts'); @@ -15,30 +15,33 @@ var plugins = require('../plugins'); var SocketHelpers = {}; -SocketHelpers.notifyOnlineUsers = function(uid, result) { +SocketHelpers.notifyOnlineUsers = function (uid, result) { winston.warn('[deprecated] SocketHelpers.notifyOnlineUsers, consider using socketHelpers.notifyNew(uid, \'newPost\', result);'); SocketHelpers.notifyNew(uid, 'newPost', result); }; -SocketHelpers.notifyNew = function(uid, type, result) { +SocketHelpers.notifyNew = function (uid, type, result) { async.waterfall([ - function(next) { + function (next) { user.getUidsFromSet('users:online', 0, -1, next); }, - function(uids, next) { + function (uids, next) { privileges.topics.filterUids('read', result.posts[0].topic.tid, uids, next); }, - function(uids, next) { + function (uids, next) { + filterTidCidIgnorers(uids, result.posts[0].topic.tid, result.posts[0].topic.cid, next); + }, + function (uids, next) { plugins.fireHook('filter:sockets.sendNewPostToUids', {uidsTo: uids, uidFrom: uid, type: type}, next); } - ], function(err, data) { + ], function (err, data) { if (err) { return winston.error(err.stack); } result.posts[0].ip = undefined; - data.uidsTo.forEach(function(toUid) { + data.uidsTo.forEach(function (toUid) { if (parseInt(toUid, 10) !== uid) { websockets.in('uid_' + toUid).emit('event:new_post', result); if (result.topic && type === 'newTopic') { @@ -49,28 +52,56 @@ SocketHelpers.notifyNew = function(uid, type, result) { }); }; -SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification) { +function filterTidCidIgnorers(uids, tid, cid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + topicFollowed: function (next) { + db.isSetMembers('tid:' + tid + ':followers', uids, next); + }, + topicIgnored: function (next) { + db.isSetMembers('tid:' + tid + ':ignorers', uids, next); + }, + categoryIgnored: function (next) { + db.sortedSetScores('cid:' + cid + ':ignorers', uids, next); + } + }, next); + }, + function (results, next) { + uids = uids.filter(function (uid, index) { + return results.topicFollowed[index] || + (!results.topicFollowed[index] && !results.topicIgnored[index] && !results.categoryIgnored[index]); + }); + next(null, uids); + } + ], callback); +} + +SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, notification) { if (!pid || !fromuid || !notification) { return; } - posts.getPostFields(pid, ['tid', 'uid', 'content'], function(err, postData) { - if (err) { - return; - } - - if (!postData.uid || fromuid === parseInt(postData.uid, 10)) { - return; - } - - async.parallel({ - username: async.apply(user.getUserField, fromuid, 'username'), - topicTitle: async.apply(topics.getTopicField, postData.tid, 'title'), - postObj: async.apply(posts.parsePost, postData) - }, function(err, results) { - if (err) { + fromuid = parseInt(fromuid, 10); + var postData; + async.waterfall([ + function (next) { + posts.getPostFields(pid, ['tid', 'uid', 'content'], next); + }, + function (_postData, next) { + postData = _postData; + privileges.posts.can('read', pid, postData.uid, next); + }, + function (canRead, next) { + if (!canRead || !postData.uid || fromuid === parseInt(postData.uid, 10)) { return; } - + async.parallel({ + username: async.apply(user.getUserField, fromuid, 'username'), + topicTitle: async.apply(topics.getTopicField, postData.tid, 'title'), + postObj: async.apply(posts.parsePost, postData) + }, next); + }, + function (results, next) { var title = S(results.topicTitle).decodeHTMLEntities().s; var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); @@ -78,50 +109,84 @@ SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification) bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]', bodyLong: results.postObj.content, pid: pid, - nid: 'post:' + pid + ':uid:' + fromuid, + path: '/post/' + pid, + nid: command + ':post:' + pid + ':uid:' + fromuid, from: fromuid, - mergeId: notification + '|' + postData.tid, + mergeId: notification + '|' + pid, topicTitle: results.topicTitle - }, function(err, notification) { - if (!err && notification) { - notifications.push(notification, [postData.uid]); - } - }); - }); + }, next); + } + ], function (err, notification) { + if (err) { + return winston.error(err); + } + if (notification) { + notifications.push(notification, [postData.uid]); + } }); }; -SocketHelpers.sendNotificationToTopicOwner = function(tid, fromuid, notification) { +SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, notification) { if (!tid || !fromuid || !notification) { return; } - async.parallel({ - username: async.apply(user.getUserField, fromuid, 'username'), - topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']), - }, function(err, results) { - if (err || fromuid === parseInt(results.topicData.uid, 10)) { - return; + fromuid = parseInt(fromuid, 10); + + var ownerUid; + async.waterfall([ + function (next) { + async.parallel({ + username: async.apply(user.getUserField, fromuid, 'username'), + topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']), + }, next); + }, + function (results, next) { + if (fromuid === parseInt(results.topicData.uid, 10)) { + return; + } + ownerUid = results.topicData.uid; + var title = S(results.topicData.title).decodeHTMLEntities().s; + var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + notifications.create({ + bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]', + path: '/topic/' + results.topicData.slug, + nid: command + ':tid:' + tid + ':uid:' + fromuid, + from: fromuid + }, next); + } + ], function (err, notification) { + if (err) { + return winston.error(err); + } + if (notification && parseInt(ownerUid, 10)) { + notifications.push(notification, [ownerUid]); + } + }); +}; + +SocketHelpers.rescindUpvoteNotification = function (pid, fromuid) { + var nid = 'upvote:post:' + pid + ':uid:' + fromuid; + notifications.rescind(nid); + + posts.getPostField(pid, 'uid', function (err, uid) { + if (err) { + return winston.error(err); } - var title = S(results.topicData.title).decodeHTMLEntities().s; - var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); - - notifications.create({ - bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]', - path: nconf.get('relative_path') + '/topic/' + results.topicData.slug, - nid: 'tid:' + tid + ':uid:' + fromuid, - from: fromuid - }, function(err, notification) { - if (!err && notification) { - notifications.push(notification, [results.topicData.uid]); + user.notifications.getUnreadCount(uid, function (err, count) { + if (err) { + return winston.error(err); } + + websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); }); }); }; -SocketHelpers.emitToTopicAndCategory = function(event, data) { +SocketHelpers.emitToTopicAndCategory = function (event, data) { websockets.in('topic_' + data.tid).emit(event, data); websockets.in('category_' + data.cid).emit(event, data); }; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 8f6f3c4c5a..7df8684d3d 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -1,218 +1,227 @@ "use strict"; -var SocketIO = require('socket.io'); +var SocketIO = require('socket.io'); var socketioWildcard = require('socketio-wildcard')(); var async = require('async'); var nconf = require('nconf'); var cookieParser = require('cookie-parser')(nconf.get('secret')); var winston = require('winston'); +var url = require('url'); var db = require('../database'); var logger = require('../logger'); var ratelimit = require('../middleware/ratelimit'); -var Sockets = {}; -var Namespaces = {}; +(function (Sockets) { + var Namespaces = {}; + var io; -var io; + Sockets.init = function (server) { + requireModules(); -Sockets.init = function(server) { - requireModules(); + io = new SocketIO({ + path: nconf.get('relative_path') + '/socket.io' + }); - io = new SocketIO({ - path: nconf.get('relative_path') + '/socket.io' - }); + addRedisAdapter(io); - addRedisAdapter(io); + io.use(socketioWildcard); + io.use(authorize); - io.use(socketioWildcard); - io.use(authorize); + io.on('connection', onConnection); - io.on('connection', onConnection); + io.listen(server, { + transports: nconf.get('socket.io:transports') + }); - io.listen(server, { - transports: nconf.get('socket.io:transports') - }); + Sockets.server = io; + }; - Sockets.server = io; -}; + function onConnection(socket) { + socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; -function onConnection(socket) { - socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; + logger.io_one(socket, socket.uid); - logger.io_one(socket, socket.uid); + onConnect(socket); - onConnect(socket); + socket.on('*', function (payload) { + onMessage(socket, payload); + }); + } - socket.on('*', function(payload) { - onMessage(socket, payload); - }); -} + function onConnect(socket) { + if (socket.uid) { + socket.join('uid_' + socket.uid); + socket.join('online_users'); + } else { + socket.join('online_guests'); + } -function onConnect(socket) { - if (socket.uid) { - socket.join('uid_' + socket.uid); - socket.join('online_users'); - } else { - socket.join('online_guests'); + io.sockets.sockets[socket.id].emit('checkSession', socket.uid); } -} + function onMessage(socket, payload) { + if (!payload.data.length) { + return winston.warn('[socket.io] Empty payload'); + } -function onMessage(socket, payload) { - if (!payload.data.length) { - return winston.warn('[socket.io] Empty payload'); - } + var eventName = payload.data[0]; + var params = payload.data[1]; + var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () { + }; - var eventName = payload.data[0]; - var params = payload.data[1]; - var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function() {}; + if (!eventName) { + return winston.warn('[socket.io] Empty method name'); + } - if (!eventName) { - return winston.warn('[socket.io] Empty method name'); - } + var parts = eventName.toString().split('.'); + var namespace = parts[0]; + var methodToCall = parts.reduce(function (prev, cur) { + if (prev !== null && prev[cur]) { + return prev[cur]; + } else { + return null; + } + }, Namespaces); - var parts = eventName.toString().split('.'); - var namespace = parts[0]; - var methodToCall = parts.reduce(function(prev, cur) { - if (prev !== null && prev[cur]) { - return prev[cur]; - } else { - return null; + if (!methodToCall) { + if (process.env.NODE_ENV === 'development') { + winston.warn('[socket.io] Unrecognized message: ' + eventName); + } + return; } - }, Namespaces); - if(!methodToCall) { - if (process.env.NODE_ENV === 'development') { - winston.warn('[socket.io] Unrecognized message: ' + eventName); + socket.previousEvents = socket.previousEvents || []; + socket.previousEvents.push(eventName); + if (socket.previousEvents.length > 20) { + socket.previousEvents.shift(); } - return; - } - socket.previousEvents = socket.previousEvents || []; - socket.previousEvents.push(eventName); - if (socket.previousEvents.length > 20) { - socket.previousEvents.shift(); + if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { + winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents); + return socket.disconnect(); + } + + async.waterfall([ + function (next) { + validateSession(socket, next); + }, + function (next) { + if (Namespaces[namespace].before) { + Namespaces[namespace].before(socket, eventName, params, next); + } else { + next(); + } + }, + function (next) { + methodToCall(socket, params, next); + } + ], function (err, result) { + callback(err ? {message: err.message} : null, result); + }); } - if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { - winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents); - return socket.disconnect(); + function requireModules() { + var modules = ['admin', 'categories', 'groups', 'meta', 'modules', + 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist' + ]; + + modules.forEach(function (module) { + Namespaces[module] = require('./' + module); + }); } - async.waterfall([ - function (next) { - validateSession(socket, next); - }, - function (next) { - if (Namespaces[namespace].before) { - Namespaces[namespace].before(socket, eventName, params, next); - } else { - next(); - } - }, - function (next) { - methodToCall(socket, params, next); + function validateSession(socket, callback) { + var req = socket.request; + if (!req.signedCookies || !req.signedCookies[nconf.get('sessionKey')]) { + return callback(new Error('[[error:invalid-session]]')); } - ], function(err, result) { - callback(err ? {message: err.message} : null, result); - }); -} - -function requireModules() { - var modules = ['admin', 'categories', 'groups', 'meta', 'modules', - 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist' - ]; - - modules.forEach(function(module) { - Namespaces[module] = require('./' + module); - }); -} - -function validateSession(socket, callback) { - var req = socket.request; - if (!req.signedCookies || !req.signedCookies['express.sid']) { - return callback(new Error('[[error:invalid-session]]')); + db.sessionStore.get(req.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { + if (err || !sessionData) { + return callback(err || new Error('[[error:invalid-session]]')); + } + + callback(); + }); } - db.sessionStore.get(req.signedCookies['express.sid'], function(err, sessionData) { - if (err || !sessionData) { - return callback(err || new Error('[[error:invalid-session]]')); - } - callback(); - }); -} + function authorize(socket, callback) { + var request = socket.request; -function authorize(socket, callback) { - var request = socket.request; + if (!request) { + return callback(new Error('[[error:not-authorized]]')); + } - if (!request) { - return callback(new Error('[[error:not-authorized]]')); + async.waterfall([ + function (next) { + cookieParser(request, {}, next); + }, + function (next) { + db.sessionStore.get(request.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { + if (err) { + return next(err); + } + if (sessionData && sessionData.passport && sessionData.passport.user) { + request.session = sessionData; + socket.uid = parseInt(sessionData.passport.user, 10); + } else { + socket.uid = 0; + } + next(); + }); + } + ], callback); } - async.waterfall([ - function(next) { - cookieParser(request, {}, next); - }, - function(next) { - db.sessionStore.get(request.signedCookies['express.sid'], function(err, sessionData) { - if (err) { - return next(err); - } - if (sessionData && sessionData.passport && sessionData.passport.user) { - request.session = sessionData; - socket.uid = parseInt(sessionData.passport.user, 10); - } else { - socket.uid = 0; - } - next(); - }); + function addRedisAdapter(io) { + if (nconf.get('redis')) { + var redisAdapter = require('socket.io-redis'); + var redis = require('../database/redis'); + var pub = redis.connect(); + var sub = redis.connect({return_buffers: true}); + io.adapter(redisAdapter({pubClient: pub, subClient: sub})); + } else if (nconf.get('isCluster') === 'true') { + winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.'); } - ], callback); -} - -function addRedisAdapter(io) { - if (nconf.get('redis')) { - var redisAdapter = require('socket.io-redis'); - var redis = require('../database/redis'); - var pub = redis.connect({return_buffers: true}); - var sub = redis.connect({return_buffers: true}); - - io.adapter(redisAdapter({pubClient: pub, subClient: sub})); - } else if (nconf.get('isCluster') === 'true') { - winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.'); } -} -Sockets.in = function(room) { - return io.in(room); -}; + Sockets.in = function (room) { + return io.in(room); + }; -Sockets.getUserSocketCount = function(uid) { - if (!io) { - return 0; - } + Sockets.getUserSocketCount = function (uid) { + if (!io) { + return 0; + } - var room = io.sockets.adapter.rooms['uid_' + uid]; - return room ? room.length : 0; -}; + var room = io.sockets.adapter.rooms['uid_' + uid]; + return room ? room.length : 0; + }; -Sockets.reqFromSocket = function(socket) { - var headers = socket.request.headers; - var host = headers.host; - var referer = headers.referer || ''; + Sockets.reqFromSocket = function (socket, payload, event) { + var headers = socket.request.headers; + var host = headers.host; + var referer = headers.referer || ''; + var data = ((payload || {}).data || []); - return { - ip: headers['x-forwarded-for'] || socket.ip, - host: host, - protocol: socket.request.connection.encrypted ? 'https' : 'http', - secure: !!socket.request.connection.encrypted, - url: referer, - path: referer.substr(referer.indexOf(host) + host.length), - headers: headers - }; -}; + if (!host) { + host = url.parse(referer).host; + } + return { + uid: socket.uid, + params: data[1], + method: event || data[0], + body: payload, + ip: headers['x-forwarded-for'] || socket.ip, + host: host, + protocol: socket.request.connection.encrypted ? 'https' : 'http', + secure: !!socket.request.connection.encrypted, + url: referer, + path: referer.substr(referer.indexOf(host) + host.length), + headers: headers + }; + }; -module.exports = Sockets; +}(exports)); diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 39603c8d0c..13fe018654 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -11,14 +11,14 @@ var meta = require('../meta'), rooms: {} }; -SocketMeta.reconnected = function(socket, data, callback) { +SocketMeta.reconnected = function (socket, data, callback) { if (socket.uid) { topics.pushUnreadCount(socket.uid); user.notifications.pushCount(socket.uid); } }; -emitter.on('nodebb:ready', function() { +emitter.on('nodebb:ready', function () { websockets.server.emit('event:nodebb.ready', { 'cache-buster': meta.config['cache-buster'] }); @@ -27,7 +27,7 @@ emitter.on('nodebb:ready', function() { /* Rooms */ -SocketMeta.rooms.enter = function(socket, data, callback) { +SocketMeta.rooms.enter = function (socket, data, callback) { if (!socket.uid) { return callback(); } @@ -53,7 +53,7 @@ SocketMeta.rooms.enter = function(socket, data, callback) { callback(); }; -SocketMeta.rooms.leaveCurrent = function(socket, data, callback) { +SocketMeta.rooms.leaveCurrent = function (socket, data, callback) { if (!socket.uid || !socket.currentRoom) { return callback(); } @@ -68,6 +68,9 @@ function leaveCurrentRoom(socket) { } } - +SocketMeta.getServerTime = function (socket, data, callback) { + // Returns server time in milliseconds + callback(null, Date.now()); +}; module.exports = SocketMeta; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index ebd27730e4..011a68567f 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -5,33 +5,21 @@ var validator = require('validator'); var meta = require('../meta'); var notifications = require('../notifications'); +var plugins = require('../plugins'); var Messaging = require('../messaging'); var utils = require('../../public/src/utils'); var server = require('./'); var user = require('../user'); var SocketModules = { - chats: {}, - sounds: {}, - settings: {} - }; + chats: {}, + sounds: {}, + settings: {} +}; /* Chat */ -SocketModules.chats.get = function(socket, data, callback) { - if(!data || !data.roomId) { - return callback(new Error('[[error:invalid-data]]')); - } - - Messaging.getMessages({ - uid: socket.uid, - roomId: data.roomId, - since: data.since, - isNew: false - }, callback); -}; - -SocketModules.chats.getRaw = function(socket, data, callback) { +SocketModules.chats.getRaw = function (socket, data, callback) { if (!data || !data.hasOwnProperty('mid')) { return callback(new Error('[[error:invalid-data]]')); } @@ -48,20 +36,16 @@ SocketModules.chats.getRaw = function(socket, data, callback) { ], callback); }; -SocketModules.chats.newRoom = function(socket, data, callback) { +SocketModules.chats.newRoom = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } - var now = Date.now(); - // Websocket rate limiting - socket.lastChatMessageTime = socket.lastChatMessageTime || 0; - if (now - socket.lastChatMessageTime < 200) { + + if (rateLimitExceeded(socket)) { return callback(new Error('[[error:too-many-messages]]')); - } else { - socket.lastChatMessageTime = now; } - Messaging.canMessageUser(socket.uid, data.touid, function(err) { + Messaging.canMessageUser(socket.uid, data.touid, function (err) { if (err) { return callback(err); } @@ -70,27 +54,30 @@ SocketModules.chats.newRoom = function(socket, data, callback) { }); }; -SocketModules.chats.send = function(socket, data, callback) { - if (!data || !data.roomId) { +SocketModules.chats.send = function (socket, data, callback) { + if (!data || !data.roomId || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - var now = Date.now(); - - // Websocket rate limiting - socket.lastChatMessageTime = socket.lastChatMessageTime || 0; - if (now - socket.lastChatMessageTime < 200) { + if (rateLimitExceeded(socket)) { return callback(new Error('[[error:too-many-messages]]')); - } else { - socket.lastChatMessageTime = now; } async.waterfall([ + function (next) { + plugins.fireHook('filter:messaging.send', { + data: data, + uid: socket.uid + }, function (err, results) { + data = results.data; + next(err); + }); + }, function (next) { Messaging.canMessageRoom(socket.uid, data.roomId, next); }, function (next) { - Messaging.sendMessage(socket.uid, data.roomId, data.message, now, next); + Messaging.sendMessage(socket.uid, data.roomId, data.message, Date.now(), next); }, function (message, next) { Messaging.notifyUsersInRoom(socket.uid, data.roomId, message); @@ -100,7 +87,19 @@ SocketModules.chats.send = function(socket, data, callback) { ], callback); }; -SocketModules.chats.loadRoom = function(socket, data, callback) { +function rateLimitExceeded(socket) { + var now = Date.now(); + socket.lastChatMessageTime = socket.lastChatMessageTime || 0; + var delay = meta.config.hasOwnProperty('chatMessageDelay') ? parseInt(meta.config.chatMessageDelay, 10) : 200; + if (now - socket.lastChatMessageTime < delay) { + return true; + } else { + socket.lastChatMessageTime = now; + } + return false; +} + +SocketModules.chats.loadRoom = function (socket, data, callback) { if (!data || !data.roomId) { return callback(new Error('[[error:invalid-data]]')); } @@ -116,11 +115,20 @@ SocketModules.chats.loadRoom = function(socket, data, callback) { async.parallel({ roomData: async.apply(Messaging.getRoomData, data.roomId), - users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1) + users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1), + messages: async.apply(Messaging.getMessages, { + callerUid: socket.uid, + uid: data.uid || socket.uid, + roomId: data.roomId, + isNew: false + }), }, next); }, function (results, next) { results.roomData.users = results.users; + results.roomData.usernames = Messaging.generateUsernames(results.users, socket.uid); + results.roomData.messages = results.messages; + results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 2; results.roomData.isOwner = parseInt(results.roomData.owner, 10) === socket.uid; results.roomData.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; results.roomData.showUserInput = !results.roomData.maximumUsersInChatRoom || results.roomData.maximumUsersInChatRoom > 2; @@ -129,10 +137,11 @@ SocketModules.chats.loadRoom = function(socket, data, callback) { ], callback); }; -SocketModules.chats.addUserToRoom = function(socket, data, callback) { +SocketModules.chats.addUserToRoom = function (socket, data, callback) { if (!data || !data.roomId || !data.username) { return callback(new Error('[[error:invalid-data]]')); } + var uid; async.waterfall([ function (next) { Messaging.getUserCountInRoom(data.roomId, next); @@ -147,19 +156,31 @@ SocketModules.chats.addUserToRoom = function(socket, data, callback) { function (next) { user.getUidByUsername(data.username, next); }, - function (uid, next) { + function (_uid, next) { + uid = _uid; if (!uid) { return next(new Error('[[error:no-user]]')); } if (socket.uid === parseInt(uid, 10)) { return next(new Error('[[error:cant-add-self-to-chat-room]]')); } + async.parallel({ + settings: async.apply(user.getSettings, uid), + isAdmin: async.apply(user.isAdministrator, socket.uid), + isFollowing: async.apply(user.isFollowing, uid, socket.uid) + }, next); + }, + function (results, next) { + if (results.settings.restrictChat && !results.isAdmin && !results.isFollowing) { + return next(new Error('[[error:chat-restricted]]')); + } + Messaging.addUsersToRoom(socket.uid, [uid], data.roomId, next); } ], callback); }; -SocketModules.chats.removeUserFromRoom = function(socket, data, callback) { +SocketModules.chats.removeUserFromRoom = function (socket, data, callback) { if (!data || !data.roomId) { return callback(new Error('[[error:invalid-data]]')); } @@ -177,7 +198,7 @@ SocketModules.chats.removeUserFromRoom = function(socket, data, callback) { ], callback); }; -SocketModules.chats.leave = function(socket, roomid, callback) { +SocketModules.chats.leave = function (socket, roomid, callback) { if (!socket.uid || !roomid) { return callback(new Error('[[error:invalid-data]]')); } @@ -186,12 +207,12 @@ SocketModules.chats.leave = function(socket, roomid, callback) { }; -SocketModules.chats.edit = function(socket, data, callback) { +SocketModules.chats.edit = function (socket, data, callback) { if (!data || !data.roomId) { return callback(new Error('[[error:invalid-data]]')); } - Messaging.canEdit(data.mid, socket.uid, function(err, allowed) { + Messaging.canEdit(data.mid, socket.uid, function (err, allowed) { if (err || !allowed) { return callback(err || new Error('[[error:cant-edit-chat-message]]')); } @@ -200,12 +221,12 @@ SocketModules.chats.edit = function(socket, data, callback) { }); }; -SocketModules.chats.delete = function(socket, data, callback) { +SocketModules.chats.delete = function (socket, data, callback) { if (!data || !data.roomId || !data.messageId) { return callback(new Error('[[error:invalid-data]]')); } - Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) { + Messaging.canEdit(data.messageId, socket.uid, function (err, allowed) { if (err || !allowed) { return callback(err || new Error('[[error:cant-delete-chat-message]]')); } @@ -214,15 +235,18 @@ SocketModules.chats.delete = function(socket, data, callback) { }); }; -SocketModules.chats.canMessage = function(socket, roomId, callback) { +SocketModules.chats.canMessage = function (socket, roomId, callback) { Messaging.canMessageRoom(socket.uid, roomId, callback); }; -SocketModules.chats.markRead = function(socket, roomId, callback) { +SocketModules.chats.markRead = function (socket, roomId, callback) { + if (!socket.uid) { + return callback(new Error('[[error:invalid-data]]')); + } async.parallel({ usersInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), markRead: async.apply(Messaging.markRead, socket.uid, roomId) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -230,13 +254,13 @@ SocketModules.chats.markRead = function(socket, roomId, callback) { Messaging.pushUnreadCount(socket.uid); // Mark notification read - var nids = results.usersInRoom.filter(function(uid) { + var nids = results.usersInRoom.filter(function (uid) { return parseInt(uid, 10) !== socket.uid; - }).map(function(uid) { + }).map(function (uid) { return 'chat_' + uid + '_' + roomId; }); - notifications.markReadMultiple(nids, socket.uid, function() { + notifications.markReadMultiple(nids, socket.uid, function () { user.notifications.pushCount(socket.uid); }); @@ -245,7 +269,7 @@ SocketModules.chats.markRead = function(socket, roomId, callback) { }); }; -SocketModules.chats.markAllRead = function(socket, data, callback) { +SocketModules.chats.markAllRead = function (socket, data, callback) { async.waterfall([ function (next) { Messaging.markAllRead(socket.uid, next); @@ -257,7 +281,7 @@ SocketModules.chats.markAllRead = function(socket, data, callback) { ], callback); }; -SocketModules.chats.renameRoom = function(socket, data, callback) { +SocketModules.chats.renameRoom = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-name]]')); } @@ -270,8 +294,8 @@ SocketModules.chats.renameRoom = function(socket, data, callback) { Messaging.getUidsInRoom(data.roomId, 0, -1, next); }, function (uids, next) { - var eventData = {roomId: data.roomId, newName: validator.escape(data.newName)}; - uids.forEach(function(uid) { + var eventData = {roomId: data.roomId, newName: validator.escape(String(data.newName))}; + uids.forEach(function (uid) { server.in('uid_' + uid).emit('event:chats.roomRename', eventData); }); next(); @@ -279,36 +303,56 @@ SocketModules.chats.renameRoom = function(socket, data, callback) { ], callback); }; -SocketModules.chats.getRecentChats = function(socket, data, callback) { - if (!data || !utils.isNumber(data.after)) { +SocketModules.chats.getRecentChats = function (socket, data, callback) { + if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) { return callback(new Error('[[error:invalid-data]]')); } - var start = parseInt(data.after, 10), - stop = start + 9; - - Messaging.getRecentChats(socket.uid, start, stop, callback); + var start = parseInt(data.after, 10); + var stop = start + 9; + Messaging.getRecentChats(socket.uid, data.uid, start, stop, callback); }; -SocketModules.chats.hasPrivateChat = function(socket, uid, callback) { +SocketModules.chats.hasPrivateChat = function (socket, uid, callback) { if (!socket.uid || !uid) { return callback(null, new Error('[[error:invalid-data]]')); } Messaging.hasPrivateChat(socket.uid, uid, callback); }; +SocketModules.chats.getMessages = function (socket, data, callback) { + if (!socket.uid || !data.uid || !data.roomId) { + return callback(new Error('[[error:invalid-data]]')); + } + + var params = { + callerUid: socket.uid, + uid: data.uid, + roomId: data.roomId, + start: parseInt(data.start, 10) || 0, + count: 50, + markRead: false + }; + + if (data.hasOwnProperty('markRead')) { + params.markRead = data.markRead; + } + + Messaging.getMessages(params, callback); +}; + /* Sounds */ -SocketModules.sounds.getSounds = function(socket, data, callback) { +SocketModules.sounds.getSounds = function (socket, data, callback) { // Read sounds from local directory meta.sounds.getFiles(callback); }; -SocketModules.sounds.getMapping = function(socket, data, callback) { - meta.sounds.getMapping(callback); +SocketModules.sounds.getMapping = function (socket, data, callback) { + meta.sounds.getMapping(socket.uid, callback); }; -SocketModules.sounds.getData = function(socket, data, callback) { +SocketModules.sounds.getData = function (socket, data, callback) { async.parallel({ - mapping: async.apply(meta.sounds.getMapping), + mapping: async.apply(meta.sounds.getMapping, socket.uid), files: async.apply(meta.sounds.getFiles) }, callback); }; diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js index 121ede2a96..ea32cd7394 100644 --- a/src/socket.io/notifications.js +++ b/src/socket.io/notifications.js @@ -7,7 +7,7 @@ var utils = require('../../public/src/utils'); var SocketNotifs = {}; -SocketNotifs.get = function(socket, data, callback) { +SocketNotifs.get = function (socket, data, callback) { if (data && Array.isArray(data.nids) && socket.uid) { user.notifications.getNotifications(data.nids, socket.uid, callback); } else { @@ -15,7 +15,7 @@ SocketNotifs.get = function(socket, data, callback) { } }; -SocketNotifs.loadMore = function(socket, data, callback) { +SocketNotifs.loadMore = function (socket, data, callback) { if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { return callback(new Error('[[error:invalid-data]]')); } @@ -24,7 +24,7 @@ SocketNotifs.loadMore = function(socket, data, callback) { } var start = parseInt(data.after, 10); var stop = start + 20; - user.notifications.getAll(socket.uid, start, stop, function(err, notifications) { + user.notifications.getAll(socket.uid, start, stop, function (err, notifications) { if (err) { return callback(err); } @@ -32,11 +32,11 @@ SocketNotifs.loadMore = function(socket, data, callback) { }); }; -SocketNotifs.getCount = function(socket, data, callback) { +SocketNotifs.getCount = function (socket, data, callback) { user.notifications.getUnreadCount(socket.uid, callback); }; -SocketNotifs.deleteAll = function(socket, data, callback) { +SocketNotifs.deleteAll = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -44,40 +44,16 @@ SocketNotifs.deleteAll = function(socket, data, callback) { user.notifications.deleteAll(socket.uid, callback); }; -SocketNotifs.markRead = function(socket, nid, callback) { +SocketNotifs.markRead = function (socket, nid, callback) { notifications.markRead(nid, socket.uid, callback); }; -SocketNotifs.markUnread = function(socket, nid, callback) { +SocketNotifs.markUnread = function (socket, nid, callback) { notifications.markUnread(nid, socket.uid, callback); }; -SocketNotifs.markAllRead = function(socket, data, callback) { +SocketNotifs.markAllRead = function (socket, data, callback) { notifications.markAllRead(socket.uid, callback); }; -SocketNotifs.generatePath = function(socket, nid, callback) { - if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]'));; - } - async.waterfall([ - function (next) { - notifications.get(nid, next); - }, - function (notification, next) { - if (!notification) { - return next(null, ''); - } - user.notifications.generateNotificationPaths([notification], socket.uid, next); - }, - function (notificationsData, next) { - if (notificationsData && notificationsData.length) { - next(null, notificationsData[0].path); - } else { - next(); - } - } - ], callback); -}; - module.exports = SocketNotifs; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 5e31e0c611..379f2315ae 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -8,7 +8,6 @@ var meta = require('../meta'); var topics = require('../topics'); var user = require('../user'); var websockets = require('./index'); -var socketTopics = require('./topics'); var socketHelpers = require('./helpers'); var utils = require('../../public/src/utils'); @@ -16,31 +15,29 @@ var apiController = require('../controllers/api'); var SocketPosts = {}; - require('./posts/edit')(SocketPosts); require('./posts/move')(SocketPosts); -require('./posts/favourites')(SocketPosts); +require('./posts/votes')(SocketPosts); +require('./posts/bookmarks')(SocketPosts); require('./posts/tools')(SocketPosts); require('./posts/flag')(SocketPosts); -SocketPosts.reply = function(socket, data, callback) { +SocketPosts.reply = function (socket, data, callback) { if (!data || !data.tid || !data.content) { return callback(new Error('[[error:invalid-data]]')); } data.uid = socket.uid; data.req = websockets.reqFromSocket(socket); + data.timestamp = Date.now(); - topics.reply(data, function(err, postData) { + topics.reply(data, function (err, postData) { if (err) { return callback(err); } var result = { posts: [postData], - privileges: { - 'topics:reply': true - }, 'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1, 'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1, }; @@ -52,25 +49,21 @@ SocketPosts.reply = function(socket, data, callback) { user.updateOnlineUsers(socket.uid); socketHelpers.notifyNew(socket.uid, 'newPost', result); - - if (data.lock) { - socketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [postData.topic.tid], cid: postData.topic.cid}); - } }); }; -SocketPosts.getRawPost = function(socket, pid, callback) { +SocketPosts.getRawPost = function (socket, pid, callback) { async.waterfall([ - function(next) { + function (next) { privileges.posts.can('read', pid, socket.uid, next); }, - function(canRead, next) { + function (canRead, next) { if (!canRead) { return next(new Error('[[error:no-privileges]]')); } posts.getPostFields(pid, ['content', 'deleted'], next); }, - function(postData, next) { + function (postData, next) { if (parseInt(postData.deleted, 10) === 1) { return next(new Error('[[error:no-post]]')); } @@ -79,37 +72,27 @@ SocketPosts.getRawPost = function(socket, pid, callback) { ], callback); }; -SocketPosts.getPost = function(socket, pid, callback) { - async.waterfall([ - function(next) { - apiController.getObjectByType(socket.uid, 'post', pid, next); - }, - function(postData, next) { - if (parseInt(postData.deleted, 10) === 1) { - return next(new Error('[[error:no-post]]')); - } - next(null, postData); - } - ], callback); +SocketPosts.getPost = function (socket, pid, callback) { + apiController.getPostData(pid, socket.uid, callback); }; -SocketPosts.loadMoreFavourites = function(socket, data, callback) { - loadMorePosts('uid:' + data.uid + ':favourites', socket.uid, data, callback); +SocketPosts.loadMoreBookmarks = function (socket, data, callback) { + loadMorePosts('uid:' + data.uid + ':bookmarks', socket.uid, data, callback); }; -SocketPosts.loadMoreUserPosts = function(socket, data, callback) { +SocketPosts.loadMoreUserPosts = function (socket, data, callback) { loadMorePosts('uid:' + data.uid + ':posts', socket.uid, data, callback); }; -SocketPosts.loadMoreBestPosts = function(socket, data, callback) { +SocketPosts.loadMoreBestPosts = function (socket, data, callback) { loadMorePosts('uid:' + data.uid + ':posts:votes', socket.uid, data, callback); }; -SocketPosts.loadMoreUpVotedPosts = function(socket, data, callback) { +SocketPosts.loadMoreUpVotedPosts = function (socket, data, callback) { loadMorePosts('uid:' + data.uid + ':upvote', socket.uid, data, callback); }; -SocketPosts.loadMoreDownVotedPosts = function(socket, data, callback) { +SocketPosts.loadMoreDownVotedPosts = function (socket, data, callback) { loadMorePosts('uid:' + data.uid + ':downvote', socket.uid, data, callback); }; @@ -118,17 +101,17 @@ function loadMorePosts(set, uid, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - var start = Math.max(0, parseInt(data.after, 10)), - stop = start + 9; + var start = Math.max(0, parseInt(data.after, 10)); + var stop = start + 9; posts.getPostSummariesFromSet(set, uid, start, stop, callback); } -SocketPosts.getCategory = function(socket, pid, callback) { +SocketPosts.getCategory = function (socket, pid, callback) { posts.getCidByPid(pid, callback); }; -SocketPosts.getPidIndex = function(socket, data, callback) { +SocketPosts.getPidIndex = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } diff --git a/src/socket.io/posts/bookmarks.js b/src/socket.io/posts/bookmarks.js new file mode 100644 index 0000000000..d0bb84256c --- /dev/null +++ b/src/socket.io/posts/bookmarks.js @@ -0,0 +1,16 @@ +'use strict'; + + +var helpers = require('./helpers'); + +module.exports = function (SocketPosts) { + + SocketPosts.bookmark = function (socket, data, callback) { + helpers.postCommand(socket, 'bookmark', 'bookmarked', '', data, callback); + }; + + SocketPosts.unbookmark = function (socket, data, callback) { + helpers.postCommand(socket, 'unbookmark', 'bookmarked', '', data, callback); + }; + +}; \ No newline at end of file diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js index e5ad7a0afb..63333eac29 100644 --- a/src/socket.io/posts/edit.js +++ b/src/socket.io/posts/edit.js @@ -2,6 +2,7 @@ var async = require('async'); var winston = require('winston'); +var validator = require('validator'); var posts = require('../../posts'); var groups = require('../../groups'); @@ -9,9 +10,9 @@ var events = require('../../events'); var meta = require('../../meta'); var websockets = require('../index'); -module.exports = function(SocketPosts) { +module.exports = function (SocketPosts) { - SocketPosts.edit = function(socket, data, callback) { + SocketPosts.edit = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } else if (!data || !data.pid || !data.content) { @@ -30,16 +31,9 @@ module.exports = function(SocketPosts) { return callback(new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]')); } - posts.edit({ - uid: socket.uid, - handle: data.handle, - pid: data.pid, - title: data.title, - content: data.content, - topic_thumb: data.topic_thumb, - tags: data.tags, - req: websockets.reqFromSocket(socket) - }, function(err, result) { + data.uid = socket.uid; + data.req = websockets.reqFromSocket(socket); + posts.edit(data, function (err, result) { if (err) { return callback(err); } @@ -49,8 +43,8 @@ module.exports = function(SocketPosts) { type: 'topic-rename', uid: socket.uid, ip: socket.ip, - oldTitle: result.topic.oldTitle, - newTitle: result.topic.title + oldTitle: validator.escape(String(result.topic.oldTitle)), + newTitle: validator.escape(String(result.topic.title)) }); } @@ -65,16 +59,16 @@ module.exports = function(SocketPosts) { async.parallel({ admins: async.apply(groups.getMembers, 'administrators', 0, -1), moderators: async.apply(groups.getMembers, 'cid:' + result.topic.cid + ':privileges:mods', 0, -1) - }, function(err, results) { + }, function (err, results) { if (err) { return winston.error(err); } - var uids = results.admins.concat(results.moderators).filter(function(uid, index, array) { + var uids = results.admins.concat(results.moderators).filter(function (uid, index, array) { return uid && array.indexOf(uid) === index; }); - uids.forEach(function(uid) { + uids.forEach(function (uid) { websockets.in('uid_' + uid).emit('event:post_edited', result); }); }); diff --git a/src/socket.io/posts/favourites.js b/src/socket.io/posts/favourites.js deleted file mode 100644 index 6883ece06a..0000000000 --- a/src/socket.io/posts/favourites.js +++ /dev/null @@ -1,160 +0,0 @@ -'use strict'; - -var async = require('async'); - -var db = require('../../database'); -var user = require('../../user'); -var posts = require('../../posts'); -var favourites = require('../../favourites'); -var plugins = require('../../plugins'); -var websockets = require('../index'); -var privileges = require('../../privileges'); -var socketHelpers = require('../helpers'); - -module.exports = function(SocketPosts) { - SocketPosts.getVoters = function(socket, data, callback) { - if (!data || !data.pid || !data.cid) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - privileges.categories.isAdminOrMod(data.cid, socket.uid, next); - }, - function (isAdminOrMod, next) { - if (!isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } - - async.parallel({ - upvoteUids: function(next) { - db.getSetMembers('pid:' + data.pid + ':upvote', next); - }, - downvoteUids: function(next) { - db.getSetMembers('pid:' + data.pid + ':downvote', next); - } - }, next); - }, - function (results, next) { - async.parallel({ - upvoters: function(next) { - user.getUsersFields(results.upvoteUids, ['username', 'userslug', 'picture'], next); - }, - upvoteCount: function(next) { - next(null, results.upvoteUids.length); - }, - downvoters: function(next) { - user.getUsersFields(results.downvoteUids, ['username', 'userslug', 'picture'], next); - }, - downvoteCount: function(next) { - next(null, results.downvoteUids.length); - } - }, next); - } - ], callback); - }; - - SocketPosts.getUpvoters = function(socket, pids, callback) { - if (!Array.isArray(pids)) { - return callback(new Error('[[error:invalid-data]]')); - } - favourites.getUpvotedUidsByPids(pids, function(err, data) { - if (err || !Array.isArray(data) || !data.length) { - return callback(err, []); - } - - async.map(data, function(uids, next) { - var otherCount = 0; - if (uids.length > 6) { - otherCount = uids.length - 5; - uids = uids.slice(0, 5); - } - user.getUsernamesByUids(uids, function(err, usernames) { - next(err, { - otherCount: otherCount, - usernames: usernames - }); - }); - }, callback); - }); - }; - - SocketPosts.upvote = function(socket, data, callback) { - favouriteCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); - }; - - SocketPosts.downvote = function(socket, data, callback) { - favouriteCommand(socket, 'downvote', 'voted', '', data, callback); - }; - - SocketPosts.unvote = function(socket, data, callback) { - favouriteCommand(socket, 'unvote', 'voted', '', data, callback); - }; - - SocketPosts.favourite = function(socket, data, callback) { - favouriteCommand(socket, 'favourite', 'favourited', 'notifications:favourited_your_post_in', data, callback); - }; - - SocketPosts.unfavourite = function(socket, data, callback) { - favouriteCommand(socket, 'unfavourite', 'favourited', '', data, callback); - }; - - function favouriteCommand(socket, command, eventName, notification, data, callback) { - if (!socket.uid) { - return callback(new Error('[[error:not-logged-in]]')) - } - if(!data || !data.pid || !data.room_id) { - return callback(new Error('[[error:invalid-data]]')); - } - async.parallel({ - exists: function(next) { - posts.exists(data.pid, next); - }, - deleted: function(next) { - posts.getPostField(data.pid, 'deleted', next); - } - }, function(err, results) { - if (err || !results.exists) { - return callback(err || new Error('[[error:invalid-pid]]')); - } - - if (parseInt(results.deleted, 10) === 1) { - return callback(new Error('[[error:post-deleted]]')); - } - - /* - hooks: - filter.post.upvote - filter.post.downvote - filter.post.unvote - filter.post.favourite - filter.post.unfavourite - */ - plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) { - if (err) { - return callback(err); - } - - executeFavouriteCommand(socket, command, eventName, notification, filteredData.data, callback); - }); - }); - } - - function executeFavouriteCommand(socket, command, eventName, notification, data, callback) { - favourites[command](data.pid, socket.uid, function(err, result) { - if (err) { - return callback(err); - } - - if (result && eventName) { - socket.emit('posts.' + command, result); - websockets.in(data.room_id).emit('event:' + eventName, result); - } - - if (result && notification) { - socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, notification); - } - callback(); - }); - } -}; \ No newline at end of file diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js index bbf7a4721c..e34808990a 100644 --- a/src/socket.io/posts/flag.js +++ b/src/socket.io/posts/flag.js @@ -12,9 +12,9 @@ var notifications = require('../../notifications'); var plugins = require('../../plugins'); var meta = require('../../meta'); -module.exports = function(SocketPosts) { +module.exports = function (SocketPosts) { - SocketPosts.flag = function(socket, data, callback) { + SocketPosts.flag = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } @@ -23,8 +23,8 @@ module.exports = function(SocketPosts) { return callback(new Error('[[error:invalid-data]]')); } - var flaggingUser = {}, - post; + var flaggingUser = {}; + var post; async.waterfall([ function (next) { @@ -40,14 +40,12 @@ module.exports = function(SocketPosts) { }, function (topicData, next) { post.topic = topicData; - next(); - }, - function (next) { + async.parallel({ - isAdminOrMod: function(next) { + isAdminOrMod: function (next) { privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next); }, - userData: function(next) { + userData: function (next) { user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next); } }, next); @@ -68,16 +66,16 @@ module.exports = function(SocketPosts) { }, function (next) { async.parallel({ - post: function(next) { + post: function (next) { posts.parsePost(post, next); }, - admins: function(next) { + admins: function (next) { groups.getMembers('administrators', 0, -1, next); }, globalMods: function (next) { groups.getMembers('Global Moderators', 0, -1, next); }, - moderators: function(next) { + moderators: function (next) { groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next); } }, next); @@ -90,23 +88,24 @@ module.exports = function(SocketPosts) { bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]', bodyLong: post.content, pid: data.pid, + path: '/post/' + data.pid, nid: 'post_flag:' + data.pid + ':uid:' + socket.uid, from: socket.uid, mergeId: 'notifications:user_flagged_post_in|' + data.pid, topicTitle: post.topic.title - }, function(err, notification) { + }, function (err, notification) { if (err || !notification) { return next(err); } - plugins.fireHook('action:post.flag', {post: post, flaggingUser: flaggingUser}); + plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser}); notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next); }); } ], callback); }; - SocketPosts.dismissFlag = function(socket, pid, callback) { + SocketPosts.dismissFlag = function (socket, pid, callback) { if (!pid || !socket.uid) { return callback('[[error:invalid-data]]'); } @@ -123,7 +122,7 @@ module.exports = function(SocketPosts) { ], callback); }; - SocketPosts.dismissAllFlags = function(socket, data, callback) { + SocketPosts.dismissAllFlags = function (socket, data, callback) { async.waterfall([ function (next) { user.isAdminOrGlobalMod(socket.uid, next); @@ -137,34 +136,36 @@ module.exports = function(SocketPosts) { ], callback); }; - SocketPosts.getMoreFlags = function(socket, data, callback) { - if (!data || !parseInt(data.after, 10)) { + SocketPosts.updateFlag = function (socket, data, callback) { + if (!data || !(data.pid && data.data)) { return callback('[[error:invalid-data]]'); } - var sortBy = data.sortBy || 'count'; - var byUsername = data.byUsername || ''; - var start = parseInt(data.after, 10); - var stop = start + 19; + + var payload = {}; async.waterfall([ function (next) { - user.isAdminOrGlobalMod(socket.uid, next); + async.parallel([ + async.apply(user.isAdminOrGlobalMod, socket.uid), + async.apply(user.isModeratorOfAnyCategory, socket.uid) + ], function (err, results) { + next(err, results[0] || results[1]); + }); }, - function (isAdminOrGlobalModerator, next) { - if (!isAdminOrGlobalModerator) { + function (allowed, next) { + if (!allowed) { return next(new Error('[[no-privileges]]')); } - if (byUsername) { - posts.getUserFlags(byUsername, sortBy, socket.uid, start, stop, next); - } else { - var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; - posts.getFlags(set, socket.uid, start, stop, next); - } - }, - function (posts, next) { - next(null, {posts: posts, next: stop + 1}); + // Translate form data into object + payload = data.data.reduce(function (memo, cur) { + memo[cur.name] = cur.value; + return memo; + }, payload); + + next(null, socket.uid, data.pid, payload); }, + async.apply(posts.updateFlagData) ], callback); - }; + } }; diff --git a/src/socket.io/posts/helpers.js b/src/socket.io/posts/helpers.js new file mode 100644 index 0000000000..657ab0f64a --- /dev/null +++ b/src/socket.io/posts/helpers.js @@ -0,0 +1,71 @@ +'use strict'; + + +var async = require('async'); +var posts = require('../../posts'); +var plugins = require('../../plugins'); +var websockets = require('../index'); +var socketHelpers = require('../helpers'); + +var helpers = module.exports; + +helpers.postCommand = function (socket, command, eventName, notification, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } + if (!data || !data.pid || !data.room_id) { + return callback(new Error('[[error:invalid-data]]')); + } + async.parallel({ + exists: function (next) { + posts.exists(data.pid, next); + }, + deleted: function (next) { + posts.getPostField(data.pid, 'deleted', next); + } + }, function (err, results) { + if (err || !results.exists) { + return callback(err || new Error('[[error:invalid-pid]]')); + } + + if (parseInt(results.deleted, 10) === 1) { + return callback(new Error('[[error:post-deleted]]')); + } + + /* + hooks: + filter:post.upvote + filter:post.downvote + filter:post.unvote + filter:post.bookmark + filter:post.unbookmark + */ + plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function (err, filteredData) { + if (err) { + return callback(err); + } + + executeCommand(socket, command, eventName, notification, filteredData.data, callback); + }); + }); +}; + +function executeCommand(socket, command, eventName, notification, data, callback) { + posts[command](data.pid, socket.uid, function (err, result) { + if (err) { + return callback(err); + } + + if (result && eventName) { + socket.emit('posts.' + command, result); + websockets.in(data.room_id).emit('event:' + eventName, result); + } + + if (result && notification) { + socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification); + } else if (result && command === 'unvote') { + socketHelpers.rescindUpvoteNotification(data.pid, socket.uid); + } + callback(); + }); +} \ No newline at end of file diff --git a/src/socket.io/posts/move.js b/src/socket.io/posts/move.js index 637ef7da1f..dc830566eb 100644 --- a/src/socket.io/posts/move.js +++ b/src/socket.io/posts/move.js @@ -5,9 +5,9 @@ var privileges = require('../../privileges'); var topics = require('../../topics'); var socketHelpers = require('../helpers'); -module.exports = function(SocketPosts) { +module.exports = function (SocketPosts) { - SocketPosts.movePost = function(socket, data, callback) { + SocketPosts.movePost = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } @@ -28,7 +28,7 @@ module.exports = function(SocketPosts) { topics.movePostToTopic(data.pid, data.tid, next); }, function (next) { - socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, 'notifications:moved_your_post'); + socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, 'move', 'notifications:moved_your_post'); next(); } ], callback); diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index 5c59b28918..ae04ed6c6d 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -1,93 +1,113 @@ 'use strict'; var async = require('async'); +var winston = require('winston'); +var validator = require('validator'); var posts = require('../../posts'); +var topics = require('../../topics'); var events = require('../../events'); var websockets = require('../index'); var socketTopics = require('../topics'); var privileges = require('../../privileges'); var plugins = require('../../plugins'); var social = require('../../social'); -var favourites = require('../../favourites'); -module.exports = function(SocketPosts) { +module.exports = function (SocketPosts) { - SocketPosts.loadPostTools = function(socket, data, callback) { + SocketPosts.loadPostTools = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } async.parallel({ - posts: function(next) { - posts.getPostFields(data.pid, ['deleted', 'reputation', 'uid'], next); + posts: function (next) { + posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid'], next); }, - isAdminOrMod: function(next) { + isAdminOrMod: function (next) { privileges.categories.isAdminOrMod(data.cid, socket.uid, next); }, - favourited: function(next) { - favourites.getFavouritesByPostIDs([data.pid], socket.uid, next); + canEdit: function (next) { + privileges.posts.canEdit(data.pid, socket.uid, next); }, - tools: function(next) { + canDelete: function (next) { + privileges.posts.canDelete(data.pid, socket.uid, next); + }, + bookmarked: function (next) { + posts.hasBookmarked(data.pid, socket.uid, next); + }, + tools: function (next) { plugins.fireHook('filter:post.tools', {pid: data.pid, uid: socket.uid, tools: []}, next); }, - postSharing: function(next) { + postSharing: function (next) { social.getActivePostSharing(next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } + results.posts.tools = results.tools.tools; results.posts.deleted = parseInt(results.posts.deleted, 10) === 1; - results.posts.favourited = results.favourited[0]; + results.posts.bookmarked = results.bookmarked; results.posts.selfPost = socket.uid && socket.uid === parseInt(results.posts.uid, 10); - results.posts.display_moderator_tools = results.isAdminOrMod || results.posts.selfPost; + results.posts.display_edit_tools = results.canEdit.flag; + results.posts.display_delete_tools = results.canDelete.flag; + results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools; results.posts.display_move_tools = results.isAdminOrMod; callback(null, results); }); }; - SocketPosts.delete = function(socket, data, callback) { - doPostAction('delete', 'event:post_deleted', socket, data, callback); - }; - - SocketPosts.restore = function(socket, data, callback) { - doPostAction('restore', 'event:post_restored', socket, data, callback); - }; - - SocketPosts.deletePosts = function(socket, data, callback) { - if (!data || !Array.isArray(data.pids)) { + SocketPosts.delete = function (socket, data, callback) { + if (!data || !data.pid) { return callback(new Error('[[error:invalid-data]]')); } - async.each(data.pids, function(pid, next) { - SocketPosts.delete(socket, {pid: pid, tid: data.tid}, next); - }, callback); - }; + var postData; + async.waterfall([ + function (next) { + posts.tools.delete(socket.uid, data.pid, next); + }, + function (_postData, next) { + postData = _postData; + isMainAndLastPost(data.pid, next); + }, + function (results, next) { + if (results.isMain && results.isLast) { + deleteTopicOf(data.pid, socket, next); + } else { + next(); + } + }, + function (next) { + websockets.in('topic_' + data.tid).emit('event:post_deleted', postData); - SocketPosts.purgePosts = function(socket, data, callback) { - if (!data || !Array.isArray(data.pids)) { - return callback(new Error('[[error:invalid-data]]')); - } - async.each(data.pids, function(pid, next) { - SocketPosts.purge(socket, {pid: pid, tid: data.tid}, next); - }, callback); + events.log({ + type: 'post-delete', + uid: socket.uid, + pid: data.pid, + ip: socket.ip + }); + + next(); + } + ], callback); }; - function doPostAction(command, eventName, socket, data, callback) { - if (!data) { + SocketPosts.restore = function (socket, data, callback) { + if (!data || !data.pid) { return callback(new Error('[[error:invalid-data]]')); } - posts.tools[command](socket.uid, data.pid, function(err, postData) { + posts.tools.restore(socket.uid, data.pid, function (err, postData) { if (err) { return callback(err); } - websockets.in('topic_' + data.tid).emit(eventName, postData); + websockets.in('topic_' + data.tid).emit('event:post_restored', postData); events.log({ - type: 'post-' + command, + type: 'post-restore', uid: socket.uid, pid: data.pid, ip: socket.ip @@ -95,22 +115,46 @@ module.exports = function(SocketPosts) { callback(); }); - } + }; + + SocketPosts.deletePosts = function (socket, data, callback) { + if (!data || !Array.isArray(data.pids)) { + return callback(new Error('[[error:invalid-data]]')); + } + async.each(data.pids, function (pid, next) { + SocketPosts.delete(socket, {pid: pid, tid: data.tid}, next); + }, callback); + }; + + SocketPosts.purgePosts = function (socket, data, callback) { + if (!data || !Array.isArray(data.pids)) { + return callback(new Error('[[error:invalid-data]]')); + } + async.each(data.pids, function (pid, next) { + SocketPosts.purge(socket, {pid: pid, tid: data.tid}, next); + }, callback); + }; - SocketPosts.purge = function(socket, data, callback) { + SocketPosts.purge = function (socket, data, callback) { function purgePost() { - posts.tools.purge(socket.uid, data.pid, function(err) { + posts.tools.purge(socket.uid, data.pid, function (err) { if (err) { return callback(err); } websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid); - events.log({ - type: 'post-purge', - uid: socket.uid, - pid: data.pid, - ip: socket.ip + topics.getTopicField(data.tid, 'title', function (err, title) { + if (err) { + return winston.error(err); + } + events.log({ + type: 'post-purge', + uid: socket.uid, + pid: data.pid, + ip: socket.ip, + title: validator.escape(String(title)) + }); }); callback(); @@ -121,7 +165,7 @@ module.exports = function(SocketPosts) { return callback(new Error('[[error:invalid-data]]')); } - isMainAndLastPost(data.pid, function(err, results) { + isMainAndLastPost(data.pid, function (err, results) { if (err) { return callback(err); } @@ -134,26 +178,30 @@ module.exports = function(SocketPosts) { return callback(new Error('[[error:cant-purge-main-post]]')); } - posts.getTopicFields(data.pid, ['tid', 'cid'], function(err, topic) { - if (err) { - return callback(err); - } - socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback); - }); + deleteTopicOf(data.pid, socket, callback); }); }; + function deleteTopicOf(pid, socket, callback) { + posts.getTopicFields(pid, ['tid', 'cid'], function (err, topic) { + if (err) { + return callback(err); + } + socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback); + }); + } + function isMainAndLastPost(pid, callback) { async.parallel({ - isMain: function(next) { + isMain: function (next) { posts.isMain(pid, next); }, - isLast: function(next) { - posts.getTopicFields(pid, ['postcount'], function(err, topic) { + isLast: function (next) { + posts.getTopicFields(pid, ['postcount'], function (err, topic) { next(err, topic ? parseInt(topic.postcount, 10) === 1 : false); }); } }, callback); } -}; \ No newline at end of file +}; diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js new file mode 100644 index 0000000000..e3a9510aaa --- /dev/null +++ b/src/socket.io/posts/votes.js @@ -0,0 +1,92 @@ +'use strict'; + +var async = require('async'); + +var db = require('../../database'); +var user = require('../../user'); +var posts = require('../../posts'); +var privileges = require('../../privileges'); +var helpers = require('./helpers'); + +module.exports = function (SocketPosts) { + + SocketPosts.getVoters = function (socket, data, callback) { + if (!data || !data.pid || !data.cid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + privileges.categories.isAdminOrMod(data.cid, socket.uid, next); + }, + function (isAdminOrMod, next) { + if (!isAdminOrMod) { + return next(new Error('[[error:no-privileges]]')); + } + + async.parallel({ + upvoteUids: function (next) { + db.getSetMembers('pid:' + data.pid + ':upvote', next); + }, + downvoteUids: function (next) { + db.getSetMembers('pid:' + data.pid + ':downvote', next); + } + }, next); + }, + function (results, next) { + async.parallel({ + upvoters: function (next) { + user.getUsersFields(results.upvoteUids, ['username', 'userslug', 'picture'], next); + }, + upvoteCount: function (next) { + next(null, results.upvoteUids.length); + }, + downvoters: function (next) { + user.getUsersFields(results.downvoteUids, ['username', 'userslug', 'picture'], next); + }, + downvoteCount: function (next) { + next(null, results.downvoteUids.length); + } + }, next); + } + ], callback); + }; + + SocketPosts.getUpvoters = function (socket, pids, callback) { + if (!Array.isArray(pids)) { + return callback(new Error('[[error:invalid-data]]')); + } + + posts.getUpvotedUidsByPids(pids, function (err, data) { + if (err || !Array.isArray(data) || !data.length) { + return callback(err, []); + } + + async.map(data, function (uids, next) { + var otherCount = 0; + if (uids.length > 6) { + otherCount = uids.length - 5; + uids = uids.slice(0, 5); + } + user.getUsernamesByUids(uids, function (err, usernames) { + next(err, { + otherCount: otherCount, + usernames: usernames + }); + }); + }, callback); + }); + }; + + SocketPosts.upvote = function (socket, data, callback) { + helpers.postCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); + }; + + SocketPosts.downvote = function (socket, data, callback) { + helpers.postCommand(socket, 'downvote', 'voted', '', data, callback); + }; + + SocketPosts.unvote = function (socket, data, callback) { + helpers.postCommand(socket, 'unvote', 'voted', '', data, callback); + }; +}; \ No newline at end of file diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index c96a77265e..2fdecf550f 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -17,29 +17,20 @@ require('./topics/tools')(SocketTopics); require('./topics/infinitescroll')(SocketTopics); require('./topics/tags')(SocketTopics); -SocketTopics.post = function(socket, data, callback) { +SocketTopics.post = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } - topics.post({ - uid: socket.uid, - handle: data.handle, - title: data.title, - content: data.content, - cid: data.category_id, - thumb: data.topic_thumb, - tags: data.tags, - req: websockets.reqFromSocket(socket) - }, function(err, result) { + data.uid = socket.uid; + data.req = websockets.reqFromSocket(socket); + data.timestamp = Date.now(); + + topics.post(data, function (err, result) { if (err) { return callback(err); } - if (data.lock) { - SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [result.topicData.tid], cid: result.topicData.cid}); - } - callback(null, result.topicData); socket.emit('event:new_post', {posts: [result.postData]}); @@ -49,18 +40,18 @@ SocketTopics.post = function(socket, data, callback) { }); }; -SocketTopics.postcount = function(socket, tid, callback) { +SocketTopics.postcount = function (socket, tid, callback) { topics.getTopicField(tid, 'postcount', callback); }; -SocketTopics.bookmark = function(socket, data, callback) { +SocketTopics.bookmark = function (socket, data, callback) { if (!socket.uid || !data) { return callback(new Error('[[error:invalid-data]]')); } topics.setUserBookmark(data.tid, socket.uid, data.index, callback); }; -SocketTopics.createTopicFromPosts = function(socket, data, callback) { +SocketTopics.createTopicFromPosts = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } @@ -69,14 +60,21 @@ SocketTopics.createTopicFromPosts = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - topics.createTopicFromPosts(socket.uid, data.title, data.pids, callback); + topics.createTopicFromPosts(socket.uid, data.title, data.pids, data.fromTid, callback); }; -SocketTopics.toggleFollow = function(socket, tid, callback) { - followCommand(topics.toggleFollow, socket, tid, callback); +SocketTopics.changeWatching = function (socket, data, callback) { + if (!data.tid || !data.type) { + return callback(new Error('[[error:invalid-data]]')); + } + var commands = ['follow', 'unfollow', 'ignore']; + if (commands.indexOf(data.type) === -1) { + return callback(new Error('[[error:invalid-command]]')); + } + followCommand(topics[data.type], socket, data.tid, callback); }; -SocketTopics.follow = function(socket, tid, callback) { +SocketTopics.follow = function (socket, tid, callback) { followCommand(topics.follow, socket, tid, callback); }; @@ -88,12 +86,18 @@ function followCommand(method, socket, tid, callback) { method(tid, socket.uid, callback); } -SocketTopics.search = function(socket, data, callback) { +SocketTopics.isFollowed = function (socket, tid, callback) { + topics.isFollowing([tid], socket.uid, function (err, isFollowing) { + callback(err, Array.isArray(isFollowing) && isFollowing.length ? isFollowing[0] : false); + }); +}; + +SocketTopics.search = function (socket, data, callback) { topics.search(data.tid, data.term, callback); }; -SocketTopics.isModerator = function(socket, tid, callback) { - topics.getTopicField(tid, 'cid', function(err, cid) { +SocketTopics.isModerator = function (socket, tid, callback) { + topics.getTopicField(tid, 'cid', function (err, cid) { if (err) { return callback(err); } @@ -102,17 +106,7 @@ SocketTopics.isModerator = function(socket, tid, callback) { }; SocketTopics.getTopic = function (socket, tid, callback) { - async.waterfall([ - function (next) { - apiController.getObjectByType(socket.uid, 'topic', tid, next); - }, - function (topicData, next) { - if (parseInt(topicData.deleted, 10) === 1) { - return next(new Error('[[error:no-topic]]')); - } - next(null, topicData); - } - ], callback); + apiController.getTopicData(tid, socket.uid, callback); }; module.exports = SocketTopics; diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js index 5c45c6e4fb..45c354620a 100644 --- a/src/socket.io/topics/infinitescroll.js +++ b/src/socket.io/topics/infinitescroll.js @@ -1,31 +1,28 @@ 'use strict'; var async = require('async'); -var user = require('../../user'); + var topics = require('../../topics'); var privileges = require('../../privileges'); var meta = require('../../meta'); var utils = require('../../../public/src/utils'); var social = require('../../social'); -module.exports = function(SocketTopics) { +module.exports = function (SocketTopics) { - SocketTopics.loadMore = function(socket, data, callback) { + SocketTopics.loadMore = function (socket, data, callback) { if (!data || !data.tid || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { return callback(new Error('[[error:invalid-data]]')); } async.parallel({ - privileges: function(next) { + privileges: function (next) { privileges.topics.get(data.tid, socket.uid, next); }, - settings: function(next) { - user.getSettings(socket.uid, next); - }, - topic: function(next) { + topic: function (next) { topics.getTopicFields(data.tid, ['postcount', 'deleted'], next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -35,10 +32,10 @@ module.exports = function(SocketTopics) { } var set = 'tid:' + data.tid + ':posts'; - if (results.settings.topicPostSort === 'most_votes') { + if (data.topicPostSort === 'most_votes') { set = 'tid:' + data.tid + ':posts:votes'; } - var reverse = results.settings.topicPostSort === 'newest_to_oldest' || results.settings.topicPostSort === 'most_votes'; + var reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes'; var start = Math.max(0, parseInt(data.after, 10)); var infScrollPostsPerPage = 10; @@ -61,19 +58,19 @@ module.exports = function(SocketTopics) { stop = Math.max(0, stop); async.parallel({ - mainPost: function(next) { + mainPost: function (next) { if (start > 0) { return next(); } topics.getMainPost(data.tid, socket.uid, next); }, - posts: function(next) { + posts: function (next) { topics.getTopicPosts(data.tid, set, start, stop, socket.uid, reverse, next); }, postSharing: function (next) { social.getActivePostSharing(next); } - }, function(err, topicData) { + }, function (err, topicData) { if (err) { return callback(err); } @@ -91,18 +88,18 @@ module.exports = function(SocketTopics) { }); }; - SocketTopics.loadMoreUnreadTopics = function(socket, data, callback) { + SocketTopics.loadMoreUnreadTopics = function (socket, data, callback) { if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { return callback(new Error('[[error:invalid-data]]')); } - var start = parseInt(data.after, 10), - stop = start + 9; + var start = parseInt(data.after, 10); + var stop = start + 9; - topics.getUnreadTopics(data.cid, socket.uid, start, stop, callback); + topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback); }; - SocketTopics.loadMoreFromSet = function(socket, data, callback) { + SocketTopics.loadMoreFromSet = function (socket, data, callback) { if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0 || !data.set) { return callback(new Error('[[error:invalid-data]]')); } diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index 80fc2e3772..699c5e5d35 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -6,41 +6,41 @@ var categories = require('../../categories'); var privileges = require('../../privileges'); var socketHelpers = require('../helpers'); -module.exports = function(SocketTopics) { +module.exports = function (SocketTopics) { - SocketTopics.move = function(socket, data, callback) { + SocketTopics.move = function (socket, data, callback) { if (!data || !Array.isArray(data.tids) || !data.cid) { return callback(new Error('[[error:invalid-data]]')); } - async.eachLimit(data.tids, 10, function(tid, next) { + async.eachLimit(data.tids, 10, function (tid, next) { var topicData; async.waterfall([ - function(next) { + function (next) { privileges.topics.isAdminOrMod(tid, socket.uid, next); }, - function(canMove, next) { + function (canMove, next) { if (!canMove) { return next(new Error('[[error:no-privileges]]')); } next(); }, - function(next) { + function (next) { topics.getTopicFields(tid, ['cid', 'slug'], next); }, - function(_topicData, next) { + function (_topicData, next) { topicData = _topicData; topicData.tid = tid; topics.tools.move(tid, data.cid, socket.uid, next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData); - socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'notifications:moved_your_topic'); + socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved_your_topic'); next(); }); @@ -48,7 +48,7 @@ module.exports = function(SocketTopics) { }; - SocketTopics.moveAll = function(socket, data, callback) { + SocketTopics.moveAll = function (socket, data, callback) { if (!data || !data.cid || !data.currentCid) { return callback(new Error('[[error:invalid-data]]')); } @@ -65,7 +65,7 @@ module.exports = function(SocketTopics) { categories.getTopicIds('cid:' + data.currentCid + ':tids', true, 0, -1, next); }, function (tids, next) { - async.eachLimit(tids, 50, function(tid, next) { + async.eachLimit(tids, 50, function (tid, next) { topics.tools.move(tid, data.cid, socket.uid, next); }, next); } diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js index f55ec377fb..ab0a0221ea 100644 --- a/src/socket.io/topics/tags.js +++ b/src/socket.io/topics/tags.js @@ -3,19 +3,23 @@ var topics = require('../../topics'); var utils = require('../../../public/src/utils'); -module.exports = function(SocketTopics) { - SocketTopics.searchTags = function(socket, data, callback) { +module.exports = function (SocketTopics) { + SocketTopics.autocompleteTags = function (socket, data, callback) { + topics.autocompleteTags(data, callback); + }; + + SocketTopics.searchTags = function (socket, data, callback) { topics.searchTags(data, callback); }; - SocketTopics.searchAndLoadTags = function(socket, data, callback) { + SocketTopics.searchAndLoadTags = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } topics.searchAndLoadTags(data, callback); }; - SocketTopics.loadMoreTags = function(socket, data, callback) { + SocketTopics.loadMoreTags = function (socket, data, callback) { if (!data || !utils.isNumber(data.after)) { return callback(new Error('[[error:invalid-data]]')); } @@ -23,13 +27,11 @@ module.exports = function(SocketTopics) { var start = parseInt(data.after, 10); var stop = start + 99; - topics.getTags(start, stop, function(err, tags) { + topics.getTags(start, stop, function (err, tags) { if (err) { return callback(err); } - tags = tags.filter(function(tag) { - return tag && tag.score > 0; - }); + tags = tags.filter(Boolean); callback(null, {tags: tags, nextStart: stop + 1}); }); }; diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js index f3d9ad4688..62e159f9fd 100644 --- a/src/socket.io/topics/tools.js +++ b/src/socket.io/topics/tools.js @@ -1,15 +1,18 @@ 'use strict'; var async = require('async'); +var winston = require('winston'); +var validator = require('validator'); + var topics = require('../../topics'); var events = require('../../events'); var privileges = require('../../privileges'); var plugins = require('../../plugins'); var socketHelpers = require('../helpers'); -module.exports = function(SocketTopics) { +module.exports = function (SocketTopics) { - SocketTopics.loadTopicTools = function(socket, data, callback) { + SocketTopics.loadTopicTools = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -20,10 +23,10 @@ module.exports = function(SocketTopics) { async.waterfall([ function (next) { async.parallel({ - topic: function(next) { + topic: function (next) { topics.getTopicData(data.tid, next); }, - privileges: function(next) { + privileges: function (next) { privileges.topics.get(data.tid, socket.uid, next); } }, next); @@ -43,36 +46,36 @@ module.exports = function(SocketTopics) { ], callback); }; - SocketTopics.delete = function(socket, data, callback) { + SocketTopics.delete = function (socket, data, callback) { SocketTopics.doTopicAction('delete', 'event:topic_deleted', socket, data, callback); }; - SocketTopics.restore = function(socket, data, callback) { + SocketTopics.restore = function (socket, data, callback) { SocketTopics.doTopicAction('restore', 'event:topic_restored', socket, data, callback); }; - SocketTopics.purge = function(socket, data, callback) { + SocketTopics.purge = function (socket, data, callback) { SocketTopics.doTopicAction('purge', 'event:topic_purged', socket, data, callback); }; - SocketTopics.lock = function(socket, data, callback) { + SocketTopics.lock = function (socket, data, callback) { SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, data, callback); }; - SocketTopics.unlock = function(socket, data, callback) { + SocketTopics.unlock = function (socket, data, callback) { SocketTopics.doTopicAction('unlock', 'event:topic_unlocked', socket, data, callback); }; - SocketTopics.pin = function(socket, data, callback) { + SocketTopics.pin = function (socket, data, callback) { SocketTopics.doTopicAction('pin', 'event:topic_pinned', socket, data, callback); }; - SocketTopics.unpin = function(socket, data, callback) { + SocketTopics.unpin = function (socket, data, callback) { SocketTopics.doTopicAction('unpin', 'event:topic_unpinned', socket, data, callback); }; - SocketTopics.doTopicAction = function(action, event, socket, data, callback) { - callback = callback || function() {}; + SocketTopics.doTopicAction = function (action, event, socket, data, callback) { + callback = callback || function () {}; if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -85,8 +88,8 @@ module.exports = function(SocketTopics) { return callback(); } - async.each(data.tids, function(tid, next) { - topics.tools[action](tid, socket.uid, function(err, data) { + async.each(data.tids, function (tid, next) { + topics.tools[action](tid, socket.uid, function (err, data) { if (err) { return next(err); } @@ -94,11 +97,17 @@ module.exports = function(SocketTopics) { socketHelpers.emitToTopicAndCategory(event, data); if (action === 'delete' || action === 'restore' || action === 'purge') { - events.log({ - type: 'topic-' + action, - uid: socket.uid, - ip: socket.ip, - tid: tid + topics.getTopicField(tid, 'title', function (err, title) { + if (err) { + return winston.error(err); + } + events.log({ + type: 'topic-' + action, + uid: socket.uid, + ip: socket.ip, + tid: tid, + title: validator.escape(String(title)) + }); }); } diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js index 056b80b041..029a0c9e74 100644 --- a/src/socket.io/topics/unread.js +++ b/src/socket.io/topics/unread.js @@ -5,36 +5,35 @@ var async = require('async'); var user = require('../../user'); var topics = require('../../topics'); -module.exports = function(SocketTopics) { +module.exports = function (SocketTopics) { - SocketTopics.markAsRead = function(socket, tids, callback) { + SocketTopics.markAsRead = function (socket, tids, callback) { if (!Array.isArray(tids) || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - topics.markAsRead(tids, socket.uid, function(err) { + topics.markAsRead(tids, socket.uid, function (err) { if (err) { return callback(err); } topics.pushUnreadCount(socket.uid); - for (var i=0; i<tids.length; ++i) { - topics.markTopicNotificationsRead(tids[i], socket.uid); - } + topics.markTopicNotificationsRead(tids, socket.uid); + callback(); }); }; - SocketTopics.markTopicNotificationsRead = function(socket, tid, callback) { - if (!tid || !socket.uid) { + SocketTopics.markTopicNotificationsRead = function (socket, tids, callback) { + if (!Array.isArray(tids) || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - topics.markTopicNotificationsRead(tid, socket.uid); + topics.markTopicNotificationsRead(tids, socket.uid); }; - SocketTopics.markAllRead = function(socket, data, callback) { - topics.markAllRead(socket.uid, function(err) { + SocketTopics.markAllRead = function (socket, data, callback) { + topics.markAllRead(socket.uid, function (err) { if (err) { return callback(err); } @@ -45,8 +44,8 @@ module.exports = function(SocketTopics) { }); }; - SocketTopics.markCategoryTopicsRead = function(socket, cid, callback) { - topics.getUnreadTids(cid, socket.uid, 0, -1, function(err, tids) { + SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) { + topics.getUnreadTids(cid, socket.uid, '', function (err, tids) { if (err) { return callback(err); } @@ -55,11 +54,11 @@ module.exports = function(SocketTopics) { }); }; - SocketTopics.markUnread = function(socket, tid, callback) { + SocketTopics.markUnread = function (socket, tid, callback) { if (!tid || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } - topics.markUnread(tid, socket.uid, function(err) { + topics.markUnread(tid, socket.uid, function (err) { if (err) { return callback(err); } @@ -69,7 +68,7 @@ module.exports = function(SocketTopics) { }); }; - SocketTopics.markAsUnreadForAll = function(socket, tids, callback) { + SocketTopics.markAsUnreadForAll = function (socket, tids, callback) { if (!Array.isArray(tids)) { return callback(new Error('[[error:invalid-tid]]')); } @@ -78,36 +77,36 @@ module.exports = function(SocketTopics) { return callback(new Error('[[error:no-privileges]]')); } - user.isAdministrator(socket.uid, function(err, isAdmin) { + user.isAdministrator(socket.uid, function (err, isAdmin) { if (err) { return callback(err); } - async.each(tids, function(tid, next) { + async.each(tids, function (tid, next) { async.waterfall([ - function(next) { + function (next) { topics.exists(tid, next); }, - function(exists, next) { + function (exists, next) { if (!exists) { return next(new Error('[[error:invalid-tid]]')); } topics.getTopicField(tid, 'cid', next); }, - function(cid, next) { + function (cid, next) { user.isModerator(socket.uid, cid, next); }, - function(isMod, next) { + function (isMod, next) { if (!isAdmin && !isMod) { return next(new Error('[[error:no-privileges]]')); } topics.markAsUnreadForAll(tid, next); }, - function(next) { + function (next) { topics.updateRecent(tid, Date.now(), next); } ], next); - }, function(err) { + }, function (err) { if (err) { return callback(err); } diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 06acacd520..47d3f6f762 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -1,7 +1,7 @@ 'use strict'; var async = require('async'); - +var winston = require('winston'); var user = require('../user'); var topics = require('../topics'); @@ -22,14 +22,14 @@ require('./user/status')(SocketUser); require('./user/picture')(SocketUser); require('./user/ban')(SocketUser); -SocketUser.exists = function(socket, data, callback) { +SocketUser.exists = function (socket, data, callback) { if (!data || !data.username) { return callback(new Error('[[error:invalid-data]]')); } meta.userOrGroupExists(data.username, callback); }; -SocketUser.deleteAccount = function(socket, data, callback) { +SocketUser.deleteAccount = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -58,14 +58,14 @@ SocketUser.deleteAccount = function(socket, data, callback) { ], callback); }; -SocketUser.emailExists = function(socket, data, callback) { +SocketUser.emailExists = function (socket, data, callback) { if (!data || !data.email) { return callback(new Error('[[error:invalid-data]]')); } user.email.exists(data.email, callback); }; -SocketUser.emailConfirm = function(socket, data, callback) { +SocketUser.emailConfirm = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -73,7 +73,7 @@ SocketUser.emailConfirm = function(socket, data, callback) { if (parseInt(meta.config.requireEmailConfirmation, 10) !== 1) { callback(); } - user.getUserField(socket.uid, 'email', function(err, email) { + user.getUserField(socket.uid, 'email', function (err, email) { if (err || !email) { return callback(err); } @@ -86,15 +86,25 @@ SocketUser.emailConfirm = function(socket, data, callback) { // Password Reset SocketUser.reset = {}; -SocketUser.reset.send = function(socket, email, callback) { +SocketUser.reset.send = function (socket, email, callback) { if (!email) { return callback(new Error('[[error:invalid-data]]')); } - user.reset.send(email, callback); + user.reset.send(email, function (err) { + if (err && err.message !== '[[error:invalid-email]]') { + return callback(err); + } + if (err && err.message === '[[error:invalid-email]]') { + winston.verbose('[user/reset] Invalid email attempt: ' + email); + return setTimeout(callback, 2500); + } + + callback(); + }); }; -SocketUser.reset.commit = function(socket, data, callback) { +SocketUser.reset.commit = function (socket, data, callback) { if (!data || !data.code || !data.password) { return callback(new Error('[[error:invalid-data]]')); } @@ -102,16 +112,20 @@ SocketUser.reset.commit = function(socket, data, callback) { async.parallel({ uid: async.apply(db.getObjectField, 'reset:uid', data.code), reset: async.apply(user.reset.commit, data.code, data.password) - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var uid = results.uid; var now = new Date(); - var parsedDate = now.getFullYear() + '/' + (now.getMonth()+1) + '/' + now.getDate(); + var parsedDate = now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate(); + + user.getUserField(uid, 'username', function (err, username) { + if (err) { + return callback(err); + } - user.getUserField(uid, 'username', function(err, username) { emailer.send('reset_notify', uid, { username: username, date: parsedDate, @@ -129,7 +143,7 @@ SocketUser.reset.commit = function(socket, data, callback) { }); }; -SocketUser.isFollowing = function(socket, data, callback) { +SocketUser.isFollowing = function (socket, data, callback) { if (!socket.uid || !data.uid) { return callback(null, false); } @@ -137,7 +151,7 @@ SocketUser.isFollowing = function(socket, data, callback) { user.isFollowing(socket.uid, data.uid, callback); }; -SocketUser.follow = function(socket, data, callback) { +SocketUser.follow = function (socket, data, callback) { if (!socket.uid || !data) { return callback(new Error('[[error:invalid-data]]')); } @@ -155,7 +169,7 @@ SocketUser.follow = function(socket, data, callback) { bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]', nid: 'follow:' + data.uid + ':uid:' + socket.uid, from: socket.uid, - path: '/user/' + userData.userslug, + path: '/uid/' + socket.uid, mergeId: 'notifications:user_started_following_you' }, next); }, @@ -169,7 +183,7 @@ SocketUser.follow = function(socket, data, callback) { ], callback); }; -SocketUser.unfollow = function(socket, data, callback) { +SocketUser.unfollow = function (socket, data, callback) { if (!socket.uid || !data) { return callback(new Error('[[error:invalid-data]]')); } @@ -177,7 +191,7 @@ SocketUser.unfollow = function(socket, data, callback) { }; function toggleFollow(method, uid, theiruid, callback) { - user[method](uid, theiruid, function(err) { + user[method](uid, theiruid, function (err) { if (err) { return callback(err); } @@ -190,19 +204,19 @@ function toggleFollow(method, uid, theiruid, callback) { }); } -SocketUser.saveSettings = function(socket, data, callback) { +SocketUser.saveSettings = function (socket, data, callback) { if (!socket.uid || !data) { return callback(new Error('[[error:invalid-data]]')); } async.waterfall([ - function(next) { + function (next) { if (socket.uid === parseInt(data.uid, 10)) { return next(null, true); } user.isAdminOrGlobalMod(socket.uid, next); }, - function(allowed, next) { + function (allowed, next) { if (!allowed) { return next(new Error('[[error:no-privileges]]')); } @@ -211,91 +225,47 @@ SocketUser.saveSettings = function(socket, data, callback) { ], callback); }; -SocketUser.setTopicSort = function(socket, sort, callback) { +SocketUser.setTopicSort = function (socket, sort, callback) { if (!socket.uid) { return callback(); } user.setSetting(socket.uid, 'topicPostSort', sort, callback); }; -SocketUser.setCategorySort = function(socket, sort, callback) { +SocketUser.setCategorySort = function (socket, sort, callback) { if (!socket.uid) { return callback(); } user.setSetting(socket.uid, 'categoryTopicSort', sort, callback); }; -SocketUser.getUnreadCount = function(socket, data, callback) { +SocketUser.getUnreadCount = function (socket, data, callback) { if (!socket.uid) { return callback(null, 0); } topics.getTotalUnread(socket.uid, callback); }; -SocketUser.getUnreadChatCount = function(socket, data, callback) { +SocketUser.getUnreadChatCount = function (socket, data, callback) { if (!socket.uid) { return callback(null, 0); } messaging.getUnreadCount(socket.uid, callback); }; -SocketUser.getUnreadCounts = function(socket, data, callback) { +SocketUser.getUnreadCounts = function (socket, data, callback) { if (!socket.uid) { return callback(null, {}); } async.parallel({ unreadTopicCount: async.apply(topics.getTotalUnread, socket.uid), + unreadNewTopicCount: async.apply(topics.getTotalUnread, socket.uid, 'new'), unreadChatCount: async.apply(messaging.getUnreadCount, socket.uid), unreadNotificationCount: async.apply(user.notifications.getUnreadCount, socket.uid) }, callback); }; -SocketUser.loadMore = function(socket, data, callback) { - if (!data || !data.set || parseInt(data.after, 10) < 0) { - return callback(new Error('[[error:invalid-data]]')); - } - - if (!socket.uid && !!parseInt(meta.config.privateUserInfo, 10)) { - return callback(new Error('[[error:no-privileges]]')); - } - - var start = parseInt(data.after, 10); - var stop = start + 19; - - async.parallel({ - isAdmin: function(next) { - user.isAdministrator(socket.uid, next); - }, - isGlobalMod: function(next) { - user.isGlobalModerator(socket.uid, next); - }, - users: function(next) { - user.getUsersFromSet(data.set, socket.uid, start, stop, next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - - if (data.set === 'users:banned' && !results.isAdmin && !results.isGlobalMod) { - return callback(new Error('[[error:no-privileges]]')); - } - - if (!results.isAdmin && data.set === 'users:online') { - results.users = results.users.filter(function(user) { - return user.status !== 'offline'; - }); - } - var result = { - users: results.users, - nextStart: stop + 1, - }; - result['route_' + data.set] = true; - callback(null, result); - }); -}; - -SocketUser.invite = function(socket, email, callback) { +SocketUser.invite = function (socket, email, callback) { if (!email || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } @@ -308,7 +278,7 @@ SocketUser.invite = function(socket, email, callback) { var max = meta.config.maximumInvites; - user.isAdministrator(socket.uid, function(err, admin) { + user.isAdministrator(socket.uid, function (err, admin) { if (err) { return callback(err); } @@ -317,16 +287,16 @@ SocketUser.invite = function(socket, email, callback) { } if (max) { async.waterfall([ - function(next) { + function (next) { user.getInvitesNumber(socket.uid, next); }, - function(invites, next) { + function (invites, next) { if (!admin && invites > max) { return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]')); } next(); }, - function(next) { + function (next) { user.sendInvitationEmail(socket.uid, email, next); } ], callback); @@ -337,17 +307,38 @@ SocketUser.invite = function(socket, email, callback) { }; -SocketUser.getUserByUID = function(socket, uid, callback) { - apiController.getUserDataByUID(socket.uid, uid, callback); +SocketUser.getUserByUID = function (socket, uid, callback) { + apiController.getUserDataByField(socket.uid, 'uid', uid, callback); }; -SocketUser.getUserByUsername = function(socket, username, callback) { - apiController.getUserDataByUsername(socket.uid, username, callback); +SocketUser.getUserByUsername = function (socket, username, callback) { + apiController.getUserDataByField(socket.uid, 'username', username, callback); }; -SocketUser.getUserByEmail = function(socket, email, callback) { - apiController.getUserDataByEmail(socket.uid, email, callback); +SocketUser.getUserByEmail = function (socket, email, callback) { + apiController.getUserDataByField(socket.uid, 'email', email, callback); }; +SocketUser.setModerationNote = function (socket, data, callback) { + if (!socket.uid || !data || !data.uid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + user.isAdminOrGlobalMod(socket.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return next(new Error('[[error:no-privileges]]')); + } + if (data.note) { + user.setUserField(data.uid, 'moderationNote', data.note, next); + } else { + db.deleteObjectField('user:' + data.uid, 'moderationNote', next); + } + } + ], callback); +}; module.exports = SocketUser; diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js index 574490b74d..cdc1e20e6d 100644 --- a/src/socket.io/user/ban.js +++ b/src/socket.io/user/ban.js @@ -5,14 +5,25 @@ var user = require('../../user'); var websockets = require('../index'); var events = require('../../events'); -module.exports = function(SocketUser) { +module.exports = function (SocketUser) { - SocketUser.banUsers = function(socket, uids, callback) { - toggleBan(socket.uid, uids, SocketUser.banUser, function(err) { + SocketUser.banUsers = function (socket, data, callback) { + // Backwards compatibility + if (Array.isArray(data)) { + data = { + uids: data, + until: 0, + reason: '' + }; + } + + toggleBan(socket.uid, data.uids, function (uid, next) { + banUser(uid, data.until || 0, data.reason || '', next); + }, function (err) { if (err) { return callback(err); } - async.each(uids, function(uid, next) { + async.each(data.uids, function (uid, next) { events.log({ type: 'user-ban', uid: socket.uid, @@ -23,8 +34,21 @@ module.exports = function(SocketUser) { }); }; - SocketUser.unbanUsers = function(socket, uids, callback) { - toggleBan(socket.uid, uids, user.unban, callback); + SocketUser.unbanUsers = function (socket, uids, callback) { + toggleBan(socket.uid, uids, user.unban, function (err) { + if (err) { + return callback(err); + } + + async.each(uids, function (uid, next) { + events.log({ + type: 'user-unban', + uid: socket.uid, + targetUid: uid, + ip: socket.ip + }, next); + }, callback); + }); }; function toggleBan(uid, uids, method, callback) { @@ -45,7 +69,7 @@ module.exports = function(SocketUser) { ], callback); } - SocketUser.banUser = function(uid, callback) { + function banUser(uid, until, reason, callback) { async.waterfall([ function (next) { user.isAdministrator(uid, next); @@ -54,14 +78,13 @@ module.exports = function(SocketUser) { if (isAdmin) { return next(new Error('[[error:cant-ban-other-admins]]')); } - user.ban(uid, next); + user.ban(uid, until, reason, next); }, function (next) { websockets.in('uid_' + uid).emit('event:banned'); next(); } ], callback); - }; - + } }; diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js index cf886b1d48..7539d1687e 100644 --- a/src/socket.io/user/picture.js +++ b/src/socket.io/user/picture.js @@ -7,9 +7,9 @@ var path = require('path'); var user = require('../../user'); var plugins = require('../../plugins'); -module.exports = function(SocketUser) { +module.exports = function (SocketUser) { - SocketUser.changePicture = function(socket, data, callback) { + SocketUser.changePicture = function (socket, data, callback) { if (!socket.uid) { return callback('[[error:invalid-uid]]'); } @@ -37,7 +37,11 @@ module.exports = function(SocketUser) { uid: socket.uid, type: type, picture: undefined - }, function(err, returnData) { + }, function (err, returnData) { + if (err) { + return next(err); + } + next(null, returnData.picture || ''); }); break; @@ -49,22 +53,22 @@ module.exports = function(SocketUser) { ], callback); }; - SocketUser.uploadProfileImageFromUrl = function(socket, data, callback) { + SocketUser.uploadProfileImageFromUrl = function (socket, data, callback) { if (!socket.uid || !data.url || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } - user.isAdminOrSelf(socket.uid, data.uid, function(err) { + user.isAdminOrSelf(socket.uid, data.uid, function (err) { if (err) { return callback(err); } - user.uploadFromUrl(data.uid, data.url, function(err, uploadedImage) { + user.uploadFromUrl(data.uid, data.url, function (err, uploadedImage) { callback(err, uploadedImage ? uploadedImage.url : null); }); }); }; - SocketUser.removeUploadedPicture = function(socket, data, callback) { + SocketUser.removeUploadedPicture = function (socket, data, callback) { if (!socket.uid || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } @@ -76,9 +80,9 @@ module.exports = function(SocketUser) { function (next) { user.getUserFields(data.uid, ['uploadedpicture', 'picture'], next); }, - function(userData, next) { + function (userData, next) { if (!userData.uploadedpicture.startsWith('http')) { - require('fs').unlink(path.join(__dirname, '../../../public', userData.uploadedpicture), function(err) { + require('fs').unlink(path.join(__dirname, '../../../public', userData.uploadedpicture), function (err) { if (err) { winston.error(err); } @@ -93,7 +97,7 @@ module.exports = function(SocketUser) { ], callback); }; - SocketUser.getProfilePictures = function(socket, data, callback) { + SocketUser.getProfilePictures = function (socket, data, callback) { if (!data || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } @@ -104,7 +108,7 @@ module.exports = function(SocketUser) { pictures: [] }), uploaded: async.apply(user.getUserField, data.uid, 'uploadedpicture') - }, function(err, data) { + }, function (err, data) { if (err) { return callback(err); } diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 52c9fcd801..63726cb31d 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -6,9 +6,9 @@ var user = require('../../user'); var meta = require('../../meta'); var events = require('../../events'); -module.exports = function(SocketUser) { +module.exports = function (SocketUser) { - SocketUser.changeUsernameEmail = function(socket, data, callback) { + SocketUser.changeUsernameEmail = function (socket, data, callback) { if (!data || !data.uid || !socket.uid) { return callback(new Error('[[error:invalid-data]]')); } @@ -23,12 +23,16 @@ module.exports = function(SocketUser) { ], callback); }; - SocketUser.updateCover = function(socket, data, callback) { + SocketUser.updateCover = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } - user.isAdministrator(socket.uid, function(err, isAdmin) { + user.isAdministrator(socket.uid, function (err, isAdmin) { + if (err) { + return callback(err); + } + if (!isAdmin && data.uid !== socket.uid) { return callback(new Error('[[error:no-privileges]]')); } @@ -37,12 +41,12 @@ module.exports = function(SocketUser) { }); }; - SocketUser.removeCover = function(socket, data, callback) { + SocketUser.removeCover = function (socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:no-privileges]]')); } - user.isAdminOrSelf(socket.uid, data.uid, function(err) { + user.isAdminOrSelf(socket.uid, data.uid, function (err) { if (err) { return callback(err); } @@ -54,14 +58,14 @@ module.exports = function(SocketUser) { async.parallel({ isAdmin: async.apply(user.isAdministrator, uid), hasPassword: async.apply(user.hasPassword, data.uid), - passwordMatch: function(next) { + passwordMatch: function (next) { if (data.password) { user.isPasswordCorrect(data.uid, data.password, next); } else { next(null, false); } } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -79,7 +83,7 @@ module.exports = function(SocketUser) { }); } - SocketUser.changePassword = function(socket, data, callback) { + SocketUser.changePassword = function (socket, data, callback) { if (!data || !data.uid) { return callback(new Error('[[error:invalid-data]]')); } @@ -87,7 +91,7 @@ module.exports = function(SocketUser) { return callback('[[error:invalid-uid]]'); } - user.changePassword(socket.uid, data, function(err) { + user.changePassword(socket.uid, data, function (err) { if (err) { return callback(err); } @@ -102,7 +106,7 @@ module.exports = function(SocketUser) { }); }; - SocketUser.updateProfile = function(socket, data, callback) { + SocketUser.updateProfile = function (socket, data, callback) { if (!socket.uid) { return callback('[[error:invalid-uid]]'); } @@ -124,7 +128,7 @@ module.exports = function(SocketUser) { user.isAdminOrGlobalMod(socket.uid, next); }, - function(isAdminOrGlobalMod, next) { + function (isAdminOrGlobalMod, next) { if (!isAdminOrGlobalMod && socket.uid !== parseInt(data.uid, 10)) { return next(new Error('[[error:no-privileges]]')); } diff --git a/src/socket.io/user/search.js b/src/socket.io/user/search.js index ecef1127c2..9c3774089d 100644 --- a/src/socket.io/user/search.js +++ b/src/socket.io/user/search.js @@ -4,9 +4,9 @@ var user = require('../../user'); var meta = require('../../meta'); var pagination = require('../../pagination'); -module.exports = function(SocketUser) { +module.exports = function (SocketUser) { - SocketUser.search = function(socket, data, callback) { + SocketUser.search = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -20,8 +20,9 @@ module.exports = function(SocketUser) { sortBy: data.sortBy, onlineOnly: data.onlineOnly, bannedOnly: data.bannedOnly, + flaggedOnly: data.flaggedOnly, uid: socket.uid - }, function(err, result) { + }, function (err, result) { if (err) { return callback(err); } diff --git a/src/socket.io/user/status.js b/src/socket.io/user/status.js index c911ecf588..2ed389dacc 100644 --- a/src/socket.io/user/status.js +++ b/src/socket.io/user/status.js @@ -3,13 +3,13 @@ var user = require('../../user'); var websockets = require('../index'); -module.exports = function(SocketUser) { - SocketUser.checkStatus = function(socket, uid, callback) { +module.exports = function (SocketUser) { + SocketUser.checkStatus = function (socket, uid, callback) { if (!socket.uid) { - return callback('[[error:invalid-uid]]'); + return callback(new Error('[[error:invalid-uid]]')); } - user.getUserFields(uid, ['lastonline', 'status'], function(err, userData) { + user.getUserFields(uid, ['lastonline', 'status'], function (err, userData) { if (err) { return callback(err); } @@ -18,7 +18,7 @@ module.exports = function(SocketUser) { }); }; - SocketUser.setStatus = function(socket, status, callback) { + SocketUser.setStatus = function (socket, status, callback) { if (!socket.uid) { return callback(new Error('[[error:invalid-uid]]')); } @@ -27,7 +27,12 @@ module.exports = function(SocketUser) { if (allowedStatus.indexOf(status) === -1) { return callback(new Error('[[error:invalid-user-status]]')); } - user.setUserField(socket.uid, 'status', status, function(err) { + + var data = {status: status}; + if (status !== 'offline') { + data.lastonline = Date.now(); + } + user.setUserFields(socket.uid, data, function (err) { if (err) { return callback(err); } diff --git a/src/topics.js b/src/topics.js index 385d3ae558..d7eb658e3b 100644 --- a/src/topics.js +++ b/src/topics.js @@ -12,8 +12,7 @@ var categories = require('./categories'); var privileges = require('./privileges'); var social = require('./social'); -(function(Topics) { - +(function (Topics) { require('./topics/data')(Topics); require('./topics/create')(Topics); @@ -29,20 +28,21 @@ var social = require('./social'); require('./topics/teaser')(Topics); require('./topics/suggested')(Topics); require('./topics/tools')(Topics); + require('./topics/thumb')(Topics); - Topics.exists = function(tid, callback) { + Topics.exists = function (tid, callback) { db.isSortedSetMember('topics:tid', tid, callback); }; - Topics.getPageCount = function(tid, uid, callback) { - Topics.getTopicField(tid, 'postcount', function(err, postCount) { + Topics.getPageCount = function (tid, uid, callback) { + Topics.getTopicField(tid, 'postcount', function (err, postCount) { if (err) { return callback(err); } if (!parseInt(postCount, 10)) { return callback(null, 1); } - user.getSettings(uid, function(err, settings) { + user.getSettings(uid, function (err, settings) { if (err) { return callback(err); } @@ -52,19 +52,19 @@ var social = require('./social'); }); }; - Topics.getTidPage = function(tid, uid, callback) { + Topics.getTidPage = function (tid, uid, callback) { if(!tid) { return callback(new Error('[[error:invalid-tid]]')); } async.parallel({ - index: function(next) { + index: function (next) { categories.getTopicIndex(tid, next); }, - settings: function(next) { + settings: function (next) { user.getSettings(uid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -72,32 +72,32 @@ var social = require('./social'); }); }; - Topics.getTopicsFromSet = function(set, uid, start, stop, callback) { + Topics.getTopicsFromSet = function (set, uid, start, stop, callback) { async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange(set, start, stop, next); }, - function(tids, next) { + function (tids, next) { Topics.getTopics(tids, uid, next); }, - function(topics, next) { + function (topics, next) { next(null, {topics: topics, nextStart: stop + 1}); } ], callback); }; - Topics.getTopics = function(tids, uid, callback) { + Topics.getTopics = function (tids, uid, callback) { async.waterfall([ - function(next) { + function (next) { privileges.topics.filterTids('read', tids, uid, next); }, - function(tids, next) { + function (tids, next) { Topics.getTopicsByTids(tids, uid, next); } ], callback); }; - Topics.getTopicsByTids = function(tids, uid, callback) { + Topics.getTopicsByTids = function (tids, uid, callback) { if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } @@ -105,14 +105,14 @@ var social = require('./social'); var uids, cids, topics; async.waterfall([ - function(next) { + function (next) { Topics.getTopicsData(tids, next); }, - function(_topics, next) { + function (_topics, next) { function mapFilter(array, field) { - return array.map(function(topic) { + return array.map(function (topic) { return topic && topic[field] && topic[field].toString(); - }).filter(function(value, index, array) { + }).filter(function (value, index, array) { return utils.isNumber(value) && array.indexOf(value) === index; }); } @@ -122,31 +122,34 @@ var social = require('./social'); cids = mapFilter(topics, 'cid'); async.parallel({ - users: function(next) { + users: function (next) { user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status'], next); }, - categories: function(next) { + categories: function (next) { categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'image', 'bgColor', 'color', 'disabled'], next); }, - hasRead: function(next) { + hasRead: function (next) { Topics.hasReadTopics(tids, uid, next); }, - bookmarks: function(next) { + isIgnored: function (next) { + Topics.isIgnoring(tids, uid, next); + }, + bookmarks: function (next) { Topics.getUserBookmarks(tids, uid, next); }, - teasers: function(next) { + teasers: function (next) { Topics.getTeasers(topics, next); }, - tags: function(next) { + tags: function (next) { Topics.getTopicsTagsObjects(tids, next); } }, next); }, - function(results, next) { + function (results, next) { var users = _.object(uids, results.users); var categories = _.object(cids, results.categories); - for (var i=0; i<topics.length; ++i) { + for (var i = 0; i < topics.length; ++i) { if (topics[i]) { topics[i].category = categories[topics[i].cid]; topics[i].user = users[topics[i].uid]; @@ -157,55 +160,69 @@ var social = require('./social'); topics[i].pinned = parseInt(topics[i].pinned, 10) === 1; topics[i].locked = parseInt(topics[i].locked, 10) === 1; topics[i].deleted = parseInt(topics[i].deleted, 10) === 1; - topics[i].unread = !results.hasRead[i]; + topics[i].ignored = results.isIgnored[i]; + topics[i].unread = !results.hasRead[i] && !results.isIgnored[i]; topics[i].bookmark = results.bookmarks[i]; topics[i].unreplied = !topics[i].teaser; + + topics[i].icons = []; } } - topics = topics.filter(function(topic) { + topics = topics.filter(function (topic) { return topic && topic.category && !topic.category.disabled; }); plugins.fireHook('filter:topics.get', {topics: topics, uid: uid}, next); }, - function(data, next) { + function (data, next) { next(null, data.topics); } ], callback); }; - Topics.getTopicWithPosts = function(topicData, set, uid, start, stop, reverse, callback) { + Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse, callback) { async.waterfall([ function (next) { async.parallel({ posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse), category: async.apply(Topics.getCategoryData, topicData.tid), threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}), - tags: async.apply(Topics.getTopicTagsObjects, topicData.tid), isFollowing: async.apply(Topics.isFollowing, [topicData.tid], uid), + isIgnoring: async.apply(Topics.isIgnoring, [topicData.tid], uid), bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid), - postSharing: async.apply(social.getActivePostSharing) + postSharing: async.apply(social.getActivePostSharing), + related: function (next) { + async.waterfall([ + function (next) { + Topics.getTopicTagsObjects(topicData.tid, next); + }, + function (tags, next) { + topicData.tags = tags; + Topics.getRelatedTopics(topicData, uid, next); + } + ], next); + } }, next); }, function (results, next) { topicData.posts = results.posts; topicData.category = results.category; topicData.thread_tools = results.threadTools.tools; - topicData.tags = results.tags; topicData.isFollowing = results.isFollowing[0]; + topicData.isNotFollowing = !results.isFollowing[0] && !results.isIgnoring[0]; + topicData.isIgnoring = results.isIgnoring[0]; topicData.bookmark = results.bookmark; topicData.postSharing = results.postSharing; + topicData.related = results.related || []; topicData.unreplied = parseInt(topicData.postcount, 10) === 1; topicData.deleted = parseInt(topicData.deleted, 10) === 1; topicData.locked = parseInt(topicData.locked, 10) === 1; topicData.pinned = parseInt(topicData.pinned, 10) === 1; - Topics.getRelatedTopics(topicData, uid, next); - }, - function (related, next) { - topicData.related = related || []; + topicData.icons = []; + plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, next); }, function (data, next) { @@ -216,10 +233,10 @@ var social = require('./social'); function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) { async.waterfall([ - function(next) { + function (next) { posts.getPidsFromSet(set, start, stop, reverse, next); }, - function(pids, next) { + function (pids, next) { if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) { return callback(null, []); } @@ -229,7 +246,7 @@ var social = require('./social'); } posts.getPostsByPids(pids, uid, next); }, - function(posts, next) { + function (posts, next) { if (!posts.length) { return next(null, []); } @@ -246,31 +263,31 @@ var social = require('./social'); ], callback); } - Topics.getMainPost = function(tid, uid, callback) { - Topics.getMainPosts([tid], uid, function(err, mainPosts) { + Topics.getMainPost = function (tid, uid, callback) { + Topics.getMainPosts([tid], uid, function (err, mainPosts) { callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null); }); }; - Topics.getMainPids = function(tids, callback) { + Topics.getMainPids = function (tids, callback) { if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } - Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) { + Topics.getTopicsFields(tids, ['mainPid'], function (err, topicData) { if (err) { return callback(err); } - var mainPids = topicData.map(function(topic) { + var mainPids = topicData.map(function (topic) { return topic && topic.mainPid; }); callback(null, mainPids); }); }; - Topics.getMainPosts = function(tids, uid, callback) { - Topics.getMainPids(tids, function(err, mainPids) { + Topics.getMainPosts = function (tids, uid, callback) { + Topics.getMainPids(tids, function (err, mainPids) { if (err) { return callback(err); } @@ -279,11 +296,11 @@ var social = require('./social'); }; function getMainPosts(mainPids, uid, callback) { - posts.getPostsByPids(mainPids, uid, function(err, postData) { + posts.getPostsByPids(mainPids, uid, function (err, postData) { if (err) { return callback(err); } - postData.forEach(function(post) { + postData.forEach(function (post) { if (post) { post.index = 0; } @@ -296,28 +313,28 @@ var social = require('./social'); db.sortedSetScore('tid:' + tid + ':bookmarks', uid, callback); }; - Topics.getUserBookmarks = function(tids, uid, callback) { + Topics.getUserBookmarks = function (tids, uid, callback) { if (!parseInt(uid, 10)) { - return callback(null, tids.map(function() { + return callback(null, tids.map(function () { return null; })); } - db.sortedSetsScore(tids.map(function(tid) { + db.sortedSetsScore(tids.map(function (tid) { return 'tid:' + tid + ':bookmarks'; }), uid, callback); }; - Topics.setUserBookmark = function(tid, uid, index, callback) { + Topics.setUserBookmark = function (tid, uid, index, callback) { db.sortedSetAdd('tid:' + tid + ':bookmarks', index, uid, callback); }; - Topics.isLocked = function(tid, callback) { - Topics.getTopicField(tid, 'locked', function(err, locked) { + Topics.isLocked = function (tid, callback) { + Topics.getTopicField(tid, 'locked', function (err, locked) { callback(err, parseInt(locked, 10) === 1); }); }; - Topics.search = function(tid, term, callback) { + Topics.search = function (tid, term, callback) { if (plugins.hasListeners('filter:topic.search')) { plugins.fireHook('filter:topic.search', { tid: tid, @@ -328,4 +345,56 @@ var social = require('./social'); } }; + Topics.getTopicBookmarks = function (tid, callback) { + db.getSortedSetRangeWithScores(['tid:' + tid + ':bookmarks'], 0, -1, callback); + }; + + Topics.updateTopicBookmarks = function (tid, pids, callback) { + var maxIndex; + + async.waterfall([ + function (next) { + Topics.getPostCount(tid, next); + }, + function (postcount, next) { + maxIndex = postcount; + Topics.getTopicBookmarks(tid, next); + }, + function (bookmarks, next) { + var forkedPosts = pids.map(function (pid) { + return {pid: pid, tid: tid}; + }); + + var uidData = bookmarks.map(function (bookmark) { + return { + uid: bookmark.value, + bookmark: bookmark.score + }; + }); + + async.eachLimit(uidData, 50, function (data, next) { + posts.getPostIndices(forkedPosts, data.uid, function (err, postIndices) { + if (err) { + return next(err); + } + + var bookmark = data.bookmark; + bookmark = bookmark < maxIndex ? bookmark : maxIndex; + + for (var i = 0; i < postIndices.length && postIndices[i] < data.bookmark; ++i) { + --bookmark; + } + + if (parseInt(bookmark, 10) !== parseInt(data.bookmark, 10)) { + Topics.setUserBookmark(tid, data.uid, bookmark, next); + } else { + next(); + } + }); + }, next); + } + ], function (err){ + callback(err); + }); + }; }(exports)); diff --git a/src/topics/create.js b/src/topics/create.js index dea303709c..99d8eb1d85 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -13,18 +13,21 @@ var posts = require('../posts'); var privileges = require('../privileges'); var categories = require('../categories'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.create = function(data, callback) { + Topics.create = function (data, callback) { // This is an internal method, consider using Topics.post instead var timestamp = data.timestamp || Date.now(); var topicData; async.waterfall([ - function(next) { + function (next) { + Topics.resizeAndUploadThumb(data, next); + }, + function (next) { db.incrObjectField('global', 'nextTid', next); }, - function(tid, next) { + function (tid, next) { topicData = { 'tid': tid, 'uid': data.uid, @@ -45,100 +48,104 @@ module.exports = function(Topics) { topicData.thumb = data.thumb; } - plugins.fireHook('filter:topic.create', {topic: topicData}, next); + plugins.fireHook('filter:topic.create', {topic: topicData, data: data}, next); }, - function(data, next) { + function (data, next) { topicData = data.topic; db.setObject('topic:' + topicData.tid, topicData, next); }, - function(next) { + function (next) { async.parallel([ - function(next) { + function (next) { db.sortedSetsAdd([ 'topics:tid', 'cid:' + topicData.cid + ':tids', 'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids' ], timestamp, topicData.tid, next); }, - function(next) { + function (next) { + categories.updateRecentTid(topicData.cid, topicData.tid, next); + }, + function (next) { user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp, next); }, - function(next) { + function (next) { db.incrObjectField('category:' + topicData.cid, 'topic_count', next); }, - function(next) { + function (next) { db.incrObjectField('global', 'topicCount', next); }, - function(next) { + function (next) { Topics.createTags(data.tags, topicData.tid, timestamp, next); } ], next); }, - function(results, next) { + function (results, next) { plugins.fireHook('action:topic.save', topicData); next(null, topicData.tid); } ], callback); }; - Topics.post = function(data, callback) { + Topics.post = function (data, callback) { var uid = data.uid; var title = data.title ? data.title.trim() : data.title; data.tags = data.tags || []; async.waterfall([ - function(next) { + function (next) { check(title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next); }, - function(next) { + function (next) { check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags', next); }, - function(next) { + function (next) { if (data.content) { data.content = data.content.rtrim(); } check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); }, - function(next) { + function (next) { categories.exists(data.cid, next); }, - function(categoryExists, next) { + function (categoryExists, next) { if (!categoryExists) { return next(new Error('[[error:no-category]]')); } privileges.categories.can('topics:create', data.cid, data.uid, next); }, - function(canCreate, next) { + function (canCreate, next) { if (!canCreate) { return next(new Error('[[error:no-privileges]]')); } - - if (!guestHandleValid(data)) { - return next(new Error('[[error:guest-handle-invalid]]')); - } - + guestHandleValid(data, next); + }, + function (next) { user.isReadyToPost(data.uid, data.cid, next); }, - function(next) { + function (next) { plugins.fireHook('filter:topic.post', data, next); }, - function(filteredData, next) { + function (filteredData, next) { data = filteredData; - Topics.create({uid: data.uid, title: data.title, cid: data.cid, thumb: data.thumb, tags: data.tags, timestamp: data.timestamp}, next); + Topics.create(data, next); }, - function(tid, next) { - posts.create({uid: data.uid, tid: tid, handle: data.handle, content: data.content, timestamp: data.timestamp, ip: data.req ? data.req.ip : null}, next); + function (tid, next) { + var postData = data; + postData.tid = tid; + postData.ip = data.req ? data.req.ip : null; + posts.create(postData, next); }, - function(postData, next) { + function (postData, next) { onNewPost(postData, data, next); }, - function(postData, next) { + function (postData, next) { async.parallel({ - postData: function(next) { + postData: function (next) { next(null, postData); }, - settings: function(next) { - user.getSettings(uid, function(err, settings) { + settings: function (next) { + user.getSettings(uid, function (err, settings) { if (err) { return next(err); } @@ -149,12 +156,12 @@ module.exports = function(Topics) { } }); }, - topicData: function(next) { + topicData: function (next) { Topics.getTopicsByTids([postData.tid], uid, next); } }, next); }, - function(data, next) { + function (data, next) { if (!Array.isArray(data.topicData) || !data.topicData.length) { return next(new Error('[[error:no-topic]]')); } @@ -179,7 +186,7 @@ module.exports = function(Topics) { ], callback); }; - Topics.reply = function(data, callback) { + Topics.reply = function (data, callback) { var tid = data.tid; var uid = data.uid; var content = data.content; @@ -187,42 +194,43 @@ module.exports = function(Topics) { var cid; async.waterfall([ - function(next) { + function (next) { Topics.getTopicField(tid, 'cid', next); }, - function(_cid, next) { + function (_cid, next) { cid = _cid; async.parallel({ - exists: async.apply(Topics.exists, tid), - locked: async.apply(Topics.isLocked, tid), + topicData: async.apply(Topics.getTopicData, tid), canReply: async.apply(privileges.topics.can, 'topics:reply', tid, uid), - isAdmin: async.apply(user.isAdministrator, uid), - isModerator: async.apply(user.isModerator, uid, cid) + isAdminOrMod: async.apply(privileges.categories.isAdminOrMod, cid, uid), }, next); }, - function(results, next) { - if (!results.exists) { + function (results, next) { + if (!results.topicData) { return next(new Error('[[error:no-topic]]')); } - if (results.locked && !results.isAdmin && !results.isModerator) { + if (parseInt(results.topicData.locked, 10) === 1 && !results.isAdminOrMod) { return next(new Error('[[error:topic-locked]]')); } - if (!results.canReply) { - return next(new Error('[[error:no-privileges]]')); + if (parseInt(results.topicData.deleted, 10) === 1 && !results.isAdminOrMod) { + return next(new Error('[[error:topic-deleted]]')); } - if (!guestHandleValid(data)) { - return next(new Error('[[error:guest-handle-invalid]]')); + if (!results.canReply) { + return next(new Error('[[error:no-privileges]]')); } + guestHandleValid(data, next); + }, + function (next) { user.isReadyToPost(uid, cid, next); }, - function(next) { + function (next) { plugins.fireHook('filter:topic.reply', data, next); }, - function(filteredData, next) { + function (filteredData, next) { content = filteredData.content || data.content; if (content) { content = content.rtrim(); @@ -230,17 +238,25 @@ module.exports = function(Topics) { check(content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); }, - function(next) { - posts.create({uid: uid, tid: tid, handle: data.handle, content: content, toPid: data.toPid, timestamp: data.timestamp, ip: data.req ? data.req.ip : null}, next); + function (next) { + posts.create({ + uid: uid, + tid: tid, + handle: data.handle, + content: content, + toPid: data.toPid, + timestamp: data.timestamp, + ip: data.req ? data.req.ip : null + }, next); }, - function(_postData, next) { + function (_postData, next) { postData = _postData; onNewPost(postData, data, next); }, - function(postData, next) { + function (postData, next) { user.getSettings(uid, next); }, - function(settings, next) { + function (settings, next) { if (settings.followTopicsOnReply) { Topics.follow(postData.tid, uid); } @@ -270,16 +286,16 @@ module.exports = function(Topics) { }, function (markedRead, next) { async.parallel({ - userInfo: function(next) { + userInfo: function (next) { posts.getUserInfoForPosts([postData.uid], uid, next); }, - topicInfo: function(next) { - Topics.getTopicFields(tid, ['tid', 'title', 'slug', 'cid', 'postcount'], next); + topicInfo: function (next) { + Topics.getTopicFields(tid, ['tid', 'title', 'slug', 'cid', 'postcount', 'mainPid'], next); }, - parents: function(next) { + parents: function (next) { Topics.addParentPosts([postData], next); }, - content: function(next) { + content: function (next) { posts.parsePost(postData, next); } }, next); @@ -291,16 +307,18 @@ module.exports = function(Topics) { // Username override for guests, if enabled if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postData.uid, 10) === 0 && data.handle) { - postData.user.username = validator.escape(data.handle); + postData.user.username = validator.escape(String(data.handle)); } - postData.favourited = false; postData.votes = 0; + postData.bookmarked = false; + postData.display_edit_tools = true; + postData.display_delete_tools = true; postData.display_moderator_tools = true; postData.display_move_tools = true; postData.selfPost = false; postData.timestampISO = utils.toISOString(postData.timestamp); - postData.topic.title = validator.escape(postData.topic.title); + postData.topic.title = validator.escape(String(postData.topic.title)); next(null, postData); } @@ -309,19 +327,27 @@ module.exports = function(Topics) { function check(item, min, max, minError, maxError, callback) { if (!item || item.length < parseInt(min, 10)) { - return callback(new Error('[[error:'+ minError + ', ' + min + ']]')); + return callback(new Error('[[error:' + minError + ', ' + min + ']]')); } else if (item.length > parseInt(max, 10)) { - return callback(new Error('[[error:'+ maxError + ', ' + max + ']]')); + return callback(new Error('[[error:' + maxError + ', ' + max + ']]')); } callback(); } - function guestHandleValid(data) { - if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(data.uid, 10) === 0 && - data.handle && data.handle.length > meta.config.maximumUsernameLength) { - return false; + function guestHandleValid(data, callback) { + if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(data.uid, 10) === 0 && data.handle) { + if (data.handle.length > meta.config.maximumUsernameLength) { + return callback(new Error('[[error:guest-handle-invalid]]')); + } + user.existsBySlug(utils.slugify(data.handle), function (err, exists) { + if (err || exists) { + return callback(err || new Error('[[error:username-taken]]')); + } + callback(); + }); + return; } - return true; + callback(); } }; diff --git a/src/topics/data.js b/src/topics/data.js index 9d153b5b78..a0196e1508 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -6,28 +6,28 @@ var db = require('../database'); var categories = require('../categories'); var utils = require('../../public/src/utils'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.getTopicField = function(tid, field, callback) { + Topics.getTopicField = function (tid, field, callback) { db.getObjectField('topic:' + tid, field, callback); }; - Topics.getTopicFields = function(tid, fields, callback) { + Topics.getTopicFields = function (tid, fields, callback) { db.getObjectFields('topic:' + tid, fields, callback); }; - Topics.getTopicsFields = function(tids, fields, callback) { + Topics.getTopicsFields = function (tids, fields, callback) { if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } - var keys = tids.map(function(tid) { + var keys = tids.map(function (tid) { return 'topic:' + tid; }); db.getObjectsFields(keys, fields, callback); }; - Topics.getTopicData = function(tid, callback) { - db.getObject('topic:' + tid, function(err, topic) { + Topics.getTopicData = function (tid, callback) { + db.getObject('topic:' + tid, function (err, topic) { if (err || !topic) { return callback(err); } @@ -37,14 +37,14 @@ module.exports = function(Topics) { }); }; - Topics.getTopicsData = function(tids, callback) { + Topics.getTopicsData = function (tids, callback) { var keys = []; - for (var i=0; i<tids.length; ++i) { + for (var i = 0; i < tids.length; ++i) { keys.push('topic:' + tids[i]); } - db.getObjects(keys, function(err, topics) { + db.getObjects(keys, function (err, topics) { if (err) { return callback(err); } @@ -64,8 +64,8 @@ module.exports = function(Topics) { topic.lastposttimeISO = utils.toISOString(topic.lastposttime); } - Topics.getCategoryData = function(tid, callback) { - Topics.getTopicField(tid, 'cid', function(err, cid) { + Topics.getCategoryData = function (tid, callback) { + Topics.getTopicField(tid, 'cid', function (err, cid) { if (err) { return callback(err); } @@ -74,11 +74,17 @@ module.exports = function(Topics) { }); }; - Topics.setTopicField = function(tid, field, value, callback) { + Topics.setTopicField = function (tid, field, value, callback) { db.setObjectField('topic:' + tid, field, value, callback); }; - Topics.deleteTopicField = function(tid, field, callback) { + + Topics.setTopicFields = function (tid, data, callback) { + callback = callback || function () {}; + db.setObject('topic:' + tid, data, callback); + }; + + Topics.deleteTopicField = function (tid, field, callback) { db.deleteObjectField('topic:' + tid, field, callback); }; diff --git a/src/topics/delete.js b/src/topics/delete.js index 97d617d878..df63ebe3bc 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -1,77 +1,77 @@ 'use strict'; -var async = require('async'), - db = require('../database'), +var async = require('async'); +var db = require('../database'); - user = require('../user'), - posts = require('../posts'), - plugins = require('../plugins'), - batch = require('../batch'); +var user = require('../user'); +var posts = require('../posts'); +var plugins = require('../plugins'); +var batch = require('../batch'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.delete = function(tid, uid, callback) { - Topics.getTopicFields(tid, ['cid'], function(err, topicData) { + Topics.delete = function (tid, uid, callback) { + Topics.getTopicFields(tid, ['cid'], function (err, topicData) { if (err) { return callback(err); } async.parallel([ - function(next) { + function (next) { Topics.setTopicField(tid, 'deleted', 1, next); }, - function(next) { + function (next) { db.sortedSetsRemove(['topics:recent', 'topics:posts', 'topics:views'], tid, next); }, - function(next) { - Topics.getPids(tid, function(err, pids) { + function (next) { + Topics.getPids(tid, function (err, pids) { if (err) { return next(err); } db.sortedSetRemove('cid:' + topicData.cid + ':pids', pids, next); }); } - ], function(err, results) { + ], function (err) { callback(err); }); }); }; - Topics.restore = function(tid, uid, callback) { - Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount', 'viewcount'], function(err, topicData) { + Topics.restore = function (tid, uid, callback) { + Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount', 'viewcount'], function (err, topicData) { if (err) { return callback(err); } async.parallel([ - function(next) { + function (next) { Topics.setTopicField(tid, 'deleted', 0, next); }, - function(next) { + function (next) { Topics.updateRecent(tid, topicData.lastposttime, next); }, - function(next) { + function (next) { db.sortedSetAdd('topics:posts', topicData.postcount, tid, next); }, - function(next) { + function (next) { db.sortedSetAdd('topics:views', topicData.viewcount, tid, next); }, - function(next) { - Topics.getPids(tid, function(err, pids) { + function (next) { + Topics.getPids(tid, function (err, pids) { if (err) { return callback(err); } - posts.getPostsFields(pids, ['pid', 'timestamp', 'deleted'], function(err, postData) { + posts.getPostsFields(pids, ['pid', 'timestamp', 'deleted'], function (err, postData) { if (err) { return next(err); } - postData = postData.filter(function(post) { + postData = postData.filter(function (post) { return post && parseInt(post.deleted, 10) !== 1; }); var pidsToAdd = [], scores = []; - postData.forEach(function(post) { + postData.forEach(function (post) { pidsToAdd.push(post.pid); scores.push(post.timestamp); }); @@ -79,13 +79,13 @@ module.exports = function(Topics) { }); }); } - ], function(err, results) { + ], function (err) { callback(err); }); }); }; - Topics.purgePostsAndTopic = function(tid, uid, callback) { + Topics.purgePostsAndTopic = function (tid, uid, callback) { var mainPid; async.waterfall([ function (next) { @@ -93,8 +93,8 @@ module.exports = function(Topics) { }, function (_mainPid, next) { mainPid = _mainPid; - batch.processSortedSet('tid:' + tid + ':posts', function(pids, next) { - async.eachLimit(pids, 10, function(pid, next) { + batch.processSortedSet('tid:' + tid + ':posts', function (pids, next) { + async.eachLimit(pids, 10, function (pid, next) { posts.purge(pid, uid, next); }, next); }, {alwaysStartAt: 0}, next); @@ -108,29 +108,38 @@ module.exports = function(Topics) { ], callback); }; - Topics.purge = function(tid, uid, callback) { - async.parallel([ - function(next) { - db.deleteAll([ - 'tid:' + tid + ':followers', - 'tid:' + tid + ':posts', - 'tid:' + tid + ':posts:votes', - 'tid:' + tid + ':bookmarks' - ], next); - }, - function(next) { - db.sortedSetsRemove(['topics:tid', 'topics:recent', 'topics:posts', 'topics:views'], tid, next); - }, - function(next) { - deleteTopicFromCategoryAndUser(tid, next); - }, - function(next) { - Topics.deleteTopicTags(tid, next); + Topics.purge = function (tid, uid, callback) { + async.waterfall([ + function (next) { + deleteFromFollowersIgnorers(tid, next); }, - function(next) { - reduceCounters(tid, next); + function (next) { + async.parallel([ + function (next) { + db.deleteAll([ + 'tid:' + tid + ':followers', + 'tid:' + tid + ':ignorers', + 'tid:' + tid + ':posts', + 'tid:' + tid + ':posts:votes', + 'tid:' + tid + ':bookmarks', + 'tid:' + tid + ':posters' + ], next); + }, + function (next) { + db.sortedSetsRemove(['topics:tid', 'topics:recent', 'topics:posts', 'topics:views'], tid, next); + }, + function (next) { + deleteTopicFromCategoryAndUser(tid, next); + }, + function (next) { + Topics.deleteTopicTags(tid, next); + }, + function (next) { + reduceCounters(tid, next); + } + ], next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -139,13 +148,33 @@ module.exports = function(Topics) { }); }; + function deleteFromFollowersIgnorers(tid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + followers: async.apply(db.getSetMembers, 'tid:' + tid + ':followers'), + ignorers: async.apply(db.getSetMembers, 'tid:' + tid + ':ignorers') + }, next); + }, + function (results, next) { + var followerKeys = results.followers.map(function (uid) { + return 'uid:' + uid + ':followed_tids'; + }); + var ignorerKeys = results.ignorers.map(function (uid) { + return 'uid:' + uid + 'ignored_tids'; + }); + db.sortedSetsRemove(followerKeys.concat(ignorerKeys), tid, next); + } + ], callback); + } + function deleteTopicFromCategoryAndUser(tid, callback) { - Topics.getTopicFields(tid, ['cid', 'uid'], function(err, topicData) { + Topics.getTopicFields(tid, ['cid', 'uid'], function (err, topicData) { if (err) { return callback(err); } async.parallel([ - function(next) { + function (next) { db.sortedSetsRemove([ 'cid:' + topicData.cid + ':tids', 'cid:' + topicData.cid + ':tids:posts', @@ -153,7 +182,7 @@ module.exports = function(Topics) { 'uid:' + topicData.uid + ':topics' ], tid, next); }, - function(next) { + function (next) { user.decrementUserFieldBy(topicData.uid, 'topiccount', 1, next); } ], callback); @@ -163,11 +192,11 @@ module.exports = function(Topics) { function reduceCounters(tid, callback) { var incr = -1; async.parallel([ - function(next) { + function (next) { db.incrObjectFieldBy('global', 'topicCount', incr, next); }, - function(next) { - Topics.getTopicFields(tid, ['cid', 'postcount'], function(err, topicData) { + function (next) { + Topics.getTopicFields(tid, ['cid', 'postcount'], function (err, topicData) { if (err) { return next(err); } @@ -176,13 +205,13 @@ module.exports = function(Topics) { var postCountChange = incr * topicData.postcount; async.parallel([ - function(next) { + function (next) { db.incrObjectFieldBy('global', 'postCount', postCountChange, next); }, - function(next) { + function (next) { db.incrObjectFieldBy('category:' + topicData.cid, 'post_count', postCountChange, next); }, - function(next) { + function (next) { db.incrObjectFieldBy('category:' + topicData.cid, 'topic_count', incr, next); } ], next); diff --git a/src/topics/follow.js b/src/topics/follow.js index c05a796741..b68e7beb26 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -8,15 +8,17 @@ var winston = require('winston'); var db = require('../database'); var user = require('../user'); +var posts = require('../posts'); var notifications = require('../notifications'); var privileges = require('../privileges'); var meta = require('../meta'); var emailer = require('../emailer'); +var plugins = require('../plugins'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.toggleFollow = function(tid, uid, callback) { - callback = callback || function() {}; + Topics.toggleFollow = function (tid, uid, callback) { + callback = callback || function () {}; var isFollowing; async.waterfall([ function (next) { @@ -36,14 +38,26 @@ module.exports = function(Topics) { Topics.follow(tid, uid, next); } }, - function(next) { + function (next) { next(null, !isFollowing); } ], callback); }; - Topics.follow = function(tid, uid, callback) { - callback = callback || function() {}; + Topics.follow = function (tid, uid, callback) { + setWatching(follow, unignore, 'action:topic.follow', tid, uid, callback); + }; + + Topics.unfollow = function (tid, uid, callback) { + setWatching(unfollow, unignore, 'action:topic.unfollow', tid, uid, callback); + }; + + Topics.ignore = function (tid, uid, callback) { + setWatching(ignore, unfollow, 'action:topic.ignore', tid, uid, callback); + }; + + function setWatching(method1, method2, hook, tid, uid, callback) { + callback = callback || function () {}; if (!parseInt(uid, 10)) { return callback(); } @@ -55,51 +69,98 @@ module.exports = function(Topics) { if (!exists) { return next(new Error('[[error:no-topic]]')); } - db.setAdd('tid:' + tid + ':followers', uid, next); + method1(tid, uid, next); }, - function(next) { - db.sortedSetAdd('uid:' + uid + ':followed_tids', Date.now(), tid, next); - } + function (next) { + method2(tid, uid, next); + }, + async.apply(plugins.fireHook, hook, {uid: uid, tid: tid}) ], callback); - }; + } - Topics.unfollow = function(tid, uid, callback) { - callback = callback || function() {}; + function follow(tid, uid, callback) { + addToSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid, callback); + } + + function unfollow(tid, uid, callback) { + removeFromSets('tid:' + tid + ':followers', 'uid:' + uid + ':followed_tids', tid, uid, callback); + } + + function ignore(tid, uid, callback) { + addToSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid, callback); + } + + function unignore(tid, uid, callback) { + removeFromSets('tid:' + tid + ':ignorers', 'uid:' + uid + ':ignored_tids', tid, uid, callback); + } + + function addToSets(set1, set2, tid, uid, callback) { async.waterfall([ function (next) { - Topics.exists(tid, next); + db.setAdd(set1, uid, next); }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-topic]]')); - } - db.setRemove('tid:' + tid + ':followers', uid, next); + function (next) { + db.sortedSetAdd(set2, Date.now(), tid, next); + } + ], callback); + } + + function removeFromSets(set1, set2, tid, uid, callback) { + async.waterfall([ + function (next) { + db.setRemove(set1, uid, next); }, - function(next) { - db.sortedSetRemove('uid:' + uid + ':followed_tids', tid, next); + function (next) { + db.sortedSetRemove(set2, tid, next); } ], callback); + } + + Topics.isFollowing = function (tids, uid, callback) { + isIgnoringOrFollowing('followers', tids, uid, callback); + }; + + Topics.isIgnoring = function (tids, uid, callback) { + isIgnoringOrFollowing('ignorers', tids, uid, callback); }; - Topics.isFollowing = function(tids, uid, callback) { + function isIgnoringOrFollowing(set, tids, uid, callback) { if (!Array.isArray(tids)) { return callback(); } if (!parseInt(uid, 10)) { - return callback(null, tids.map(function() { return false; })); + return callback(null, tids.map(function () { return false; })); } - var keys = tids.map(function(tid) { - return 'tid:' + tid + ':followers'; + var keys = tids.map(function (tid) { + return 'tid:' + tid + ':' + set; }); db.isMemberOfSets(keys, uid, callback); - }; + } - Topics.getFollowers = function(tid, callback) { + Topics.getFollowers = function (tid, callback) { db.getSetMembers('tid:' + tid + ':followers', callback); }; - Topics.notifyFollowers = function(postData, exceptUid, callback) { - callback = callback || function() {}; + Topics.getIgnorers = function (tid, callback) { + db.getSetMembers('tid:' + tid + ':ignorers', callback); + }; + + Topics.filterIgnoringUids = function (tid, uids, callback) { + async.waterfall([ + function (next){ + db.isSetMembers('tid:' + tid + ':ignorers', uids, next); + }, + function (isMembers, next){ + var readingUids = uids.filter(function (uid, index) { + return uid && isMembers[index]; + }); + next(null, readingUids); + } + ], callback); + }; + + Topics.notifyFollowers = function (postData, exceptUid, callback) { + callback = callback || function () {}; var followers; var title; var titleEscaped; @@ -134,10 +195,13 @@ module.exports = function(Topics) { titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); } + postData.content = posts.relativeToAbsolute(postData.content); + notifications.create({ bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', bodyLong: postData.content, pid: postData.pid, + path: '/post/' + postData.pid, nid: 'new_post:tid:' + postData.topic.tid + ':pid:' + postData.pid + ':uid:' + exceptUid, tid: postData.topic.tid, from: exceptUid, @@ -154,24 +218,27 @@ module.exports = function(Topics) { return next(); } - async.eachLimit(followers, 3, function(toUid, next) { + async.eachLimit(followers, 3, function (toUid, next) { async.parallel({ userData: async.apply(user.getUserFields, toUid, ['username', 'userslug']), userSettings: async.apply(user.getSettings, toUid) - }, function(err, data) { + }, function (err, data) { if (err) { return next(err); } + if (data.userSettings.sendPostNotifications) { emailer.send('notif_post', toUid, { pid: postData.pid, subject: '[' + (meta.config.title || 'NodeBB') + '] ' + title, intro: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', - postBody: postData.content.replace(/"\/\//g, '"http://'), + postBody: postData.content.replace(/"\/\//g, '"https://'), site_title: meta.config.title || 'NodeBB', username: data.userData.username, userslug: data.userData.userslug, url: nconf.get('url') + '/topic/' + postData.topic.tid, + topicSlug: postData.topic.slug, + postCount: postData.topic.postcount, base_url: nconf.get('url') }, next); } else { diff --git a/src/topics/fork.js b/src/topics/fork.js index 948cb3207e..c1cd019a90 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -11,9 +11,9 @@ var plugins = require('../plugins'); var meta = require('../meta'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.createTopicFromPosts = function(uid, title, pids, callback) { + Topics.createTopicFromPosts = function (uid, title, pids, fromTid, callback) { if (title) { title = title.trim(); } @@ -28,38 +28,41 @@ module.exports = function(Topics) { return callback(new Error('[[error:invalid-pid]]')); } - pids.sort(function(a, b) { + pids.sort(function (a, b) { return a - b; }); var mainPid = pids[0]; var cid; var tid; async.waterfall([ - function(next) { + function (next) { posts.getCidByPid(mainPid, next); }, - function(_cid, next) { + function (_cid, next) { cid = _cid; async.parallel({ - postData: function(next) { + postData: function (next) { posts.getPostData(mainPid, next); }, - isAdminOrMod: function(next) { + isAdminOrMod: function (next) { privileges.categories.isAdminOrMod(cid, uid, next); } }, next); }, - function(results, next) { + function (results, next) { if (!results.isAdminOrMod) { return next(new Error('[[error:no-privileges]]')); } Topics.create({uid: results.postData.uid, title: title, cid: cid}, next); }, - function(_tid, next) { + function (results, next) { + Topics.updateTopicBookmarks(fromTid, pids, function (){ next( null, results );} ); + }, + function (_tid, next) { function move(pid, next) { - privileges.posts.canEdit(pid, uid, function(err, canEdit) { - if(err || !canEdit) { - return next(err); + privileges.posts.canEdit(pid, uid, function (err, canEdit) { + if (err || !canEdit.flag) { + return next(err || new Error(canEdit.message)); } Topics.movePostToTopic(pid, tid, next); @@ -68,28 +71,28 @@ module.exports = function(Topics) { tid = _tid; async.eachSeries(pids, move, next); }, - function(next) { + function (next) { Topics.updateTimestamp(tid, Date.now(), next); }, - function(next) { + function (next) { Topics.getTopicData(tid, next); } ], callback); }; - Topics.movePostToTopic = function(pid, tid, callback) { + Topics.movePostToTopic = function (pid, tid, callback) { var postData; async.waterfall([ - function(next) { + function (next) { Topics.exists(tid, next); }, - function(exists, next) { + function (exists, next) { if (!exists) { return next(new Error('[[error:no-topic]]')); } - posts.getPostFields(pid, ['tid', 'timestamp', 'votes'], next); + posts.getPostFields(pid, ['tid', 'uid', 'timestamp', 'upvotes', 'downvotes'], next); }, - function(post, next) { + function (post, next) { if (!post || !post.tid) { return next(new Error('[[error:no-post]]')); } @@ -101,34 +104,34 @@ module.exports = function(Topics) { postData = post; postData.pid = pid; - Topics.removePostFromTopic(postData.tid, pid, next); + Topics.removePostFromTopic(postData.tid, postData, next); }, - function(next) { + function (next) { async.parallel([ - function(next) { + function (next) { updateCategoryPostCount(postData.tid, tid, next); }, - function(next) { + function (next) { Topics.decreasePostCount(postData.tid, next); }, - function(next) { + function (next) { Topics.increasePostCount(tid, next); }, - function(next) { + function (next) { posts.setPostField(pid, 'tid', tid, next); }, - function(next) { - Topics.addPostToTopic(tid, pid, postData.timestamp, postData.votes, next); + function (next) { + Topics.addPostToTopic(tid, postData, next); } ], next); }, - function(results, next) { + function (results, next) { async.parallel([ async.apply(updateRecentTopic, tid), async.apply(updateRecentTopic, postData.tid) ], next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -138,7 +141,7 @@ module.exports = function(Topics) { }; function updateCategoryPostCount(oldTid, tid, callback) { - Topics.getTopicsFields([oldTid, tid], ['cid'], function(err, topicData) { + Topics.getTopicsFields([oldTid, tid], ['cid'], function (err, topicData) { if (err) { return callback(err); } @@ -157,16 +160,16 @@ module.exports = function(Topics) { function updateRecentTopic(tid, callback) { async.waterfall([ - function(next) { + function (next) { Topics.getLatestUndeletedPid(tid, next); }, - function(pid, next) { + function (pid, next) { if (!pid) { return callback(); } posts.getPostField(pid, 'timestamp', next); }, - function(timestamp, next) { + function (timestamp, next) { Topics.updateTimestamp(tid, timestamp, next); } ], callback); diff --git a/src/topics/popular.js b/src/topics/popular.js index 88d5a6819a..452f897581 100644 --- a/src/topics/popular.js +++ b/src/topics/popular.js @@ -1,14 +1,12 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - privileges = require('../privileges'); +var async = require('async'); +var privileges = require('../privileges'); +module.exports = function (Topics) { -module.exports = function(Topics) { - - Topics.getPopular = function(term, uid, count, callback) { + Topics.getPopular = function (term, uid, count, callback) { count = parseInt(count, 10) || 20; if (term === 'alltime') { @@ -16,37 +14,37 @@ module.exports = function(Topics) { } async.waterfall([ - function(next) { + function (next) { Topics.getLatestTidsFromSet('topics:tid', 0, -1, term, next); }, - function(tids, next) { + function (tids, next) { getTopics(tids, uid, count, next); } ], callback); }; function getAllTimePopular(uid, count, callback) { - Topics.getTopicsFromSet('topics:posts', uid, 0, count - 1, function(err, data) { + Topics.getTopicsFromSet('topics:posts', uid, 0, count - 1, function (err, data) { callback(err, data ? data.topics : null); }); } function getTopics(tids, uid, count, callback) { async.waterfall([ - function(next) { + function (next) { Topics.getTopicsFields(tids, ['tid', 'postcount', 'deleted'], next); }, - function(topics, next) { - tids = topics.filter(function(topic) { + function (topics, next) { + tids = topics.filter(function (topic) { return topic && parseInt(topic.deleted, 10) !== 1; - }).sort(function(a, b) { + }).sort(function (a, b) { return b.postcount - a.postcount; - }).slice(0, count).map(function(topic) { + }).slice(0, count).map(function (topic) { return topic.tid; }); privileges.topics.filterTids('read', tids, uid, next); }, - function(tids, next) { + function (tids, next) { Topics.getTopicsByTids(tids, uid, next); } ], callback); diff --git a/src/topics/posts.js b/src/topics/posts.js index 445eab4637..6e15c62b7c 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -7,36 +7,35 @@ var validator = require('validator'); var db = require('../database'); var user = require('../user'); -var favourites = require('../favourites'); var posts = require('../posts'); var meta = require('../meta'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.onNewPostMade = function(postData, callback) { + Topics.onNewPostMade = function (postData, callback) { async.series([ - function(next) { + function (next) { Topics.increasePostCount(postData.tid, next); }, - function(next) { + function (next) { Topics.updateTimestamp(postData.tid, postData.timestamp, next); }, - function(next) { - Topics.addPostToTopic(postData.tid, postData.pid, postData.timestamp, 0, next); + function (next) { + Topics.addPostToTopic(postData.tid, postData, next); } ], callback); }; - Topics.getTopicPosts = function(tid, set, start, stop, uid, reverse, callback) { - callback = callback || function() {}; + Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) { + callback = callback || function () {}; async.parallel({ - posts: function(next) { + posts: function (next) { posts.getPostsFromSet(set, start, stop, uid, reverse, next); }, - postCount: function(next) { + postCount: function (next) { Topics.getTopicField(tid, 'postcount', next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -47,11 +46,11 @@ module.exports = function(Topics) { }); }; - Topics.addPostData = function(postData, uid, callback) { + Topics.addPostData = function (postData, uid, callback) { if (!Array.isArray(postData) || !postData.length) { return callback(null, []); } - var pids = postData.map(function(post) { + var pids = postData.map(function (post) { return post && post.pid; }); @@ -60,67 +59,67 @@ module.exports = function(Topics) { } async.parallel({ - favourites: function(next) { - favourites.getFavouritesByPostIDs(pids, uid, next); + bookmarks: function (next) { + posts.hasBookmarked(pids, uid, next); }, - voteData: function(next) { - favourites.getVoteStatusByPostIDs(pids, uid, next); + voteData: function (next) { + posts.getVoteStatusByPostIDs(pids, uid, next); }, - userData: function(next) { + userData: function (next) { var uids = []; - for(var i=0; i<postData.length; ++i) { + for(var i = 0; i < postData.length; ++i) { if (postData[i] && uids.indexOf(postData[i].uid) === -1) { uids.push(postData[i].uid); } } - posts.getUserInfoForPosts(uids, uid, function(err, users) { + posts.getUserInfoForPosts(uids, uid, function (err, users) { if (err) { return next(err); } var userData = {}; - users.forEach(function(user, index) { + users.forEach(function (user, index) { userData[uids[index]] = user; }); next(null, userData); }); }, - editors: function(next) { + editors: function (next) { var editors = []; - for(var i=0; i<postData.length; ++i) { + for(var i = 0; i < postData.length; ++i) { if (postData[i] && postData[i].editor && editors.indexOf(postData[i].editor) === -1) { editors.push(postData[i].editor); } } - user.getUsersFields(editors, ['uid', 'username', 'userslug'], function(err, editors) { + user.getUsersFields(editors, ['uid', 'username', 'userslug'], function (err, editors) { if (err) { return next(err); } var editorData = {}; - editors.forEach(function(editor) { + editors.forEach(function (editor) { editorData[editor.uid] = editor; }); next(null, editorData); }); }, - parents: function(next) { + parents: function (next) { Topics.addParentPosts(postData, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - postData.forEach(function(postObj, i) { + postData.forEach(function (postObj, i) { if (postObj) { postObj.deleted = parseInt(postObj.deleted, 10) === 1; postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]); postObj.editor = postObj.editor ? results.editors[postObj.editor] : null; - postObj.favourited = results.favourites[i]; + postObj.bookmarked = results.bookmarks[i]; postObj.upvoted = results.voteData.upvotes[i]; postObj.downvoted = results.voteData.downvotes[i]; postObj.votes = postObj.votes || 0; @@ -128,7 +127,7 @@ module.exports = function(Topics) { // Username override for guests, if enabled if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) { - postObj.user.username = validator.escape(postObj.handle); + postObj.user.username = validator.escape(String(postObj.handle)); } } }); @@ -137,13 +136,15 @@ module.exports = function(Topics) { }); }; - Topics.modifyPostsByPrivilege = function(topicData, topicPrivileges) { + Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) { var loggedIn = !!parseInt(topicPrivileges.uid, 10); - topicData.posts.forEach(function(post) { + topicData.posts.forEach(function (post) { if (post) { - post.display_moderator_tools = topicPrivileges.isAdminOrMod || post.selfPost; + post.display_edit_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:edit']); + post.display_delete_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:delete']); + post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools; post.display_move_tools = topicPrivileges.isAdminOrMod && post.index !== 0; - post.display_post_menu = topicPrivileges.isAdminOrMod || post.selfPost || ((loggedIn || topicData.postSharing.length) && !post.deleted); + post.display_post_menu = topicPrivileges.isAdminOrMod || (post.selfPost && !topicData.locked) || ((loggedIn || topicData.postSharing.length) && !post.deleted); post.ip = topicPrivileges.isAdminOrMod ? post.ip : undefined; if (post.deleted && !(topicPrivileges.isAdminOrMod || post.selfPost)) { @@ -156,8 +157,8 @@ module.exports = function(Topics) { }); }; - Topics.addParentPosts = function(postData, callback) { - var parentPids = postData.map(function(postObj) { + Topics.addParentPosts = function (postData, callback) { + var parentPids = postData.map(function (postObj) { return postObj && postObj.hasOwnProperty('toPid') ? parseInt(postObj.toPid, 10) : null; }).filter(Boolean); @@ -168,9 +169,9 @@ module.exports = function(Topics) { var parentPosts; async.waterfall([ async.apply(posts.getPostsFields, parentPids, ['uid']), - function(_parentPosts, next) { + function (_parentPosts, next) { parentPosts = _parentPosts; - var parentUids = parentPosts.map(function(postObj) { return parseInt(postObj.uid, 10); }).filter(function(uid, idx, users) { + var parentUids = parentPosts.map(function (postObj) { return parseInt(postObj.uid, 10); }).filter(function (uid, idx, users) { return users.indexOf(uid) === idx; }); @@ -178,15 +179,15 @@ module.exports = function(Topics) { }, function (userData, next) { var usersMap = {}; - userData.forEach(function(user) { + userData.forEach(function (user) { usersMap[user.uid] = user.username; }); var parents = {}; - parentPosts.forEach(function(post, i) { + parentPosts.forEach(function (post, i) { parents[parentPids[i]] = {username: usersMap[post.uid]}; }); - postData.forEach(function(post) { + postData.forEach(function (post) { post.parent = parents[post.toPid]; }); next(); @@ -194,8 +195,8 @@ module.exports = function(Topics) { ], callback); }; - Topics.calculatePostIndices = function(posts, start, stop, postCount, reverse) { - posts.forEach(function(post, index) { + Topics.calculatePostIndices = function (posts, start, stop, postCount, reverse) { + posts.forEach(function (post, index) { if (reverse) { post.index = postCount - (start + index + 1); } else { @@ -204,34 +205,34 @@ module.exports = function(Topics) { }); }; - Topics.getLatestUndeletedPid = function(tid, callback) { + Topics.getLatestUndeletedPid = function (tid, callback) { async.waterfall([ - function(next) { + function (next) { Topics.getLatestUndeletedReply(tid, next); }, - function(pid, next) { + function (pid, next) { if (parseInt(pid, 10)) { return callback(null, pid.toString()); } Topics.getTopicField(tid, 'mainPid', next); }, - function(mainPid, next) { + function (mainPid, next) { posts.getPostFields(mainPid, ['pid', 'deleted'], next); }, - function(mainPost, next) { + function (mainPost, next) { next(null, parseInt(mainPost.pid, 10) && parseInt(mainPost.deleted, 10) !== 1 ? mainPost.pid.toString() : null); } ], callback); }; - Topics.getLatestUndeletedReply = function(tid, callback) { + Topics.getLatestUndeletedReply = function (tid, callback) { var isDeleted = false; var done = false; var latestPid = null; var index = 0; async.doWhilst( - function(next) { - db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, function(err, pids) { + function (next) { + db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, function (err, pids) { if (err) { return next(err); } @@ -241,7 +242,7 @@ module.exports = function(Topics) { return next(); } - posts.getPostField(pids[0], 'deleted', function(err, deleted) { + posts.getPostField(pids[0], 'deleted', function (err, deleted) { if (err) { return next(err); } @@ -255,58 +256,74 @@ module.exports = function(Topics) { }); }); }, - function() { + function () { return isDeleted && !done; }, - function(err) { + function (err) { callback(err, latestPid); } ); }; - Topics.addPostToTopic = function(tid, pid, timestamp, votes, callback) { - Topics.getTopicField(tid, 'mainPid', function(err, mainPid) { - if (err) { - return callback(err); - } - if (!parseInt(mainPid, 10)) { - Topics.setTopicField(tid, 'mainPid', pid, callback); - } else { - async.parallel([ - function(next) { - db.sortedSetAdd('tid:' + tid + ':posts', timestamp, pid, next); - }, - function(next) { - db.sortedSetAdd('tid:' + tid + ':posts:votes', votes, pid, next); - } - ], function(err) { - if (err) { - return callback(err); - } - Topics.updateTeaser(tid, callback); - }); + Topics.addPostToTopic = function (tid, postData, callback) { + async.waterfall([ + function (next) { + Topics.getTopicField(tid, 'mainPid', next); + }, + function (mainPid, next) { + if (!parseInt(mainPid, 10)) { + Topics.setTopicField(tid, 'mainPid', postData.pid, next); + } else { + async.parallel([ + function (next) { + db.sortedSetAdd('tid:' + tid + ':posts', postData.timestamp, postData.pid, next); + }, + function (next) { + var upvotes = parseInt(postData.upvotes, 10) || 0; + var downvotes = parseInt(postData.downvotes, 10) || 0; + var votes = upvotes - downvotes; + db.sortedSetAdd('tid:' + tid + ':posts:votes', votes, postData.pid, next); + } + ], function (err) { + next(err); + }); + } + }, + function (next) { + db.sortedSetIncrBy('tid:' + tid + ':posters', 1, postData.uid, next); + }, + function (count, next) { + Topics.updateTeaser(tid, next); } - }); + ], callback); }; - Topics.removePostFromTopic = function(tid, pid, callback) { - db.sortedSetsRemove(['tid:' + tid + ':posts', 'tid:' + tid + ':posts:votes'], pid, function(err) { - if (err) { - return callback(err); + Topics.removePostFromTopic = function (tid, postData, callback) { + async.waterfall([ + function (next) { + db.sortedSetsRemove([ + 'tid:' + tid + ':posts', + 'tid:' + tid + ':posts:votes' + ], postData.pid, next); + }, + function (next) { + db.sortedSetIncrBy('tid:' + tid + ':posters', -1, postData.uid, next); + }, + function (count, next) { + Topics.updateTeaser(tid, next); } - Topics.updateTeaser(tid, callback); - }); + ], callback); }; - Topics.getPids = function(tid, callback) { + Topics.getPids = function (tid, callback) { async.parallel({ - mainPid: function(next) { + mainPid: function (next) { Topics.getTopicField(tid, 'mainPid', next); }, - pids: function(next) { + pids: function (next) { db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -317,21 +334,21 @@ module.exports = function(Topics) { }); }; - Topics.increasePostCount = function(tid, callback) { + Topics.increasePostCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts', callback); }; - Topics.decreasePostCount = function(tid, callback) { + Topics.decreasePostCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts', callback); }; - Topics.increaseViewCount = function(tid, callback) { + Topics.increaseViewCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, 'topics:views', callback); }; function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) { - callback = callback || function() {}; - db.incrObjectFieldBy('topic:' + tid, field, by, function(err, value) { + callback = callback || function () {}; + db.incrObjectFieldBy('topic:' + tid, field, by, function (err, value) { if (err) { return callback(err); } @@ -339,12 +356,12 @@ module.exports = function(Topics) { }); } - Topics.getTitleByPid = function(pid, callback) { + Topics.getTitleByPid = function (pid, callback) { Topics.getTopicFieldByPid('title', pid, callback); }; - Topics.getTopicFieldByPid = function(field, pid, callback) { - posts.getPostField(pid, 'tid', function(err, tid) { + Topics.getTopicFieldByPid = function (field, pid, callback) { + posts.getPostField(pid, 'tid', function (err, tid) { if (err) { return callback(err); } @@ -352,8 +369,8 @@ module.exports = function(Topics) { }); }; - Topics.getTopicDataByPid = function(pid, callback) { - posts.getPostField(pid, 'tid', function(err, tid) { + Topics.getTopicDataByPid = function (pid, callback) { + posts.getPostField(pid, 'tid', function (err, tid) { if (err) { return callback(err); } @@ -361,7 +378,7 @@ module.exports = function(Topics) { }); }; - Topics.getPostCount = function(tid, callback) { + Topics.getPostCount = function (tid, callback) { db.getObjectField('topic:' + tid, 'postcount', callback); }; diff --git a/src/topics/recent.js b/src/topics/recent.js index 7139f58991..653ea372a7 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -4,8 +4,9 @@ var async = require('async'); var db = require('../database'); +var plugins = require('../plugins'); -module.exports = function(Topics) { +module.exports = function (Topics) { var terms = { day: 86400000, week: 604800000, @@ -13,21 +14,21 @@ module.exports = function(Topics) { year: 31104000000 }; - Topics.getLatestTopics = function(uid, start, stop, term, callback) { + Topics.getLatestTopics = function (uid, start, stop, term, callback) { async.waterfall([ function (next) { Topics.getLatestTidsFromSet('topics:recent', start, stop, term, next); }, - function(tids, next) { + function (tids, next) { Topics.getTopics(tids, uid, next); }, - function(topics, next) { + function (topics, next) { next(null, {topics: topics, nextStart: stop + 1}); } ], callback); }; - Topics.getLatestTidsFromSet = function(set, start, stop, term, callback) { + Topics.getLatestTidsFromSet = function (set, start, stop, term, callback) { var since = terms.day; if (terms[term]) { since = terms[term]; @@ -38,9 +39,9 @@ module.exports = function(Topics) { db.getSortedSetRevRangeByScore(set, start, count, '+inf', Date.now() - since, callback); }; - Topics.updateTimestamp = function(tid, timestamp, callback) { + Topics.updateTimestamp = function (tid, timestamp, callback) { async.parallel([ - function(next) { + function (next) { async.waterfall([ function (next) { Topics.getTopicField(tid, 'deleted', next); @@ -53,16 +54,29 @@ module.exports = function(Topics) { } ], next); }, - function(next) { + function (next) { Topics.setTopicField(tid, 'lastposttime', timestamp, next); } - ], function(err) { + ], function (err) { callback(err); }); }; - Topics.updateRecent = function(tid, timestamp, callback) { - callback = callback || function() {}; - db.sortedSetAdd('topics:recent', timestamp, tid, callback); + Topics.updateRecent = function (tid, timestamp, callback) { + callback = callback || function () {}; + if (plugins.hasListeners('filter:topics.updateRecent')) { + plugins.fireHook('filter:topics.updateRecent', {tid: tid, timestamp: timestamp}, function (err, data) { + if (err) { + return callback(err); + } + if (data && data.tid && data.timestamp) { + db.sortedSetAdd('topics:recent', data.timestamp, data.tid, callback); + } else { + callback(); + } + }); + } else { + db.sortedSetAdd('topics:recent', timestamp, tid, callback); + } }; }; diff --git a/src/topics/suggested.js b/src/topics/suggested.js index 0f087ceb4c..a7b1190bf1 100644 --- a/src/topics/suggested.js +++ b/src/topics/suggested.js @@ -9,25 +9,25 @@ var async = require('async'), db = require('../database'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.getSuggestedTopics = function(tid, uid, start, stop, callback) { + Topics.getSuggestedTopics = function (tid, uid, start, stop, callback) { async.parallel({ - tagTids: function(next) { + tagTids: function (next) { getTidsWithSameTags(tid, next); }, - searchTids: function(next) { + searchTids: function (next) { getSearchTids(tid, next); }, - categoryTids: function(next) { + categoryTids: function (next) { getCategoryTids(tid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var tids = results.tagTids.concat(results.searchTids).concat(results.categoryTids); - tids = tids.filter(function(_tid, index, array) { + tids = tids.filter(function (_tid, index, array) { return parseInt(_tid, 10) !== parseInt(tid, 10) && array.indexOf(_tid) === index; }).slice(start, stop + 1); @@ -37,15 +37,15 @@ module.exports = function(Topics) { function getTidsWithSameTags(tid, callback) { async.waterfall([ - function(next) { + function (next) { Topics.getTopicTags(tid, next); }, - function(tags, next) { - async.map(tags, function(tag, next) { + function (tags, next) { + async.map(tags, function (tag, next) { Topics.getTagTids(tag, 0, -1, next); }, next); }, - function(data, next) { + function (data, next) { next(null, _.unique(_.flatten(data))); } ], callback); @@ -53,17 +53,17 @@ module.exports = function(Topics) { function getSearchTids(tid, callback) { async.waterfall([ - function(next) { + function (next) { Topics.getTopicField(tid, 'title', next); }, - function(title, next) { + function (title, next) { search.searchQuery('topic', title, [], [], next); } ], callback); } function getCategoryTids(tid, callback) { - Topics.getTopicField(tid, 'cid', function(err, cid) { + Topics.getTopicField(tid, 'cid', function (err, cid) { if (err || !cid) { return callback(err, []); } diff --git a/src/topics/tags.js b/src/topics/tags.js index 83702331b7..ee8892b1cf 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -10,9 +10,9 @@ var plugins = require('../plugins'); var utils = require('../../public/src/utils'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.createTags = function(tags, tid, timestamp, callback) { + Topics.createTags = function (tags, tid, timestamp, callback) { callback = callback || function () {}; if (!Array.isArray(tags) || !tags.length) { @@ -25,20 +25,20 @@ module.exports = function(Topics) { }, function (data, next) { tags = data.tags.slice(0, meta.config.maximumTagsPerTopic || 5); - tags = tags.map(function(tag) { + tags = tags.map(function (tag) { return utils.cleanUpTag(tag, meta.config.maximumTagLength); - }).filter(function(tag, index, array) { + }).filter(function (tag, index, array) { return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index; }); - var keys = tags.map(function(tag) { + var keys = tags.map(function (tag) { return 'tag:' + tag + ':topics'; }); async.parallel([ async.apply(db.setAdd, 'topic:' + tid + ':tags', tags), async.apply(db.sortedSetsAdd, keys, timestamp, tid) - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -48,13 +48,36 @@ module.exports = function(Topics) { ], callback); }; - Topics.updateTag = function(tag, data, callback) { + Topics.createEmptyTag = function (tag, callback) { + if (!tag) { + return callback(new Error('[[error:invalid-tag]]')); + } + + tag = utils.cleanUpTag(tag, meta.config.maximumTagLength); + if (tag.length < (meta.config.minimumTagLength || 3)) { + return callback(new Error('[[error:tag-too-short]]')); + } + + async.waterfall([ + function (next) { + db.isSortedSetMember('tags:topic:count', tag, next); + }, + function (isMember, next) { + if (isMember) { + return next(); + } + db.sortedSetAdd('tags:topic:count', 0, tag, next); + } + ], callback); + }; + + Topics.updateTag = function (tag, data, callback) { db.setObject('tag:' + tag, data, callback); }; function updateTagCount(tag, callback) { - callback = callback || function() {}; - Topics.getTagTopicCount(tag, function(err, count) { + callback = callback || function () {}; + Topics.getTagTopicCount(tag, function (err, count) { if (err) { return callback(err); } @@ -64,42 +87,42 @@ module.exports = function(Topics) { }); } - Topics.getTagTids = function(tag, start, stop, callback) { + Topics.getTagTids = function (tag, start, stop, callback) { db.getSortedSetRevRange('tag:' + tag + ':topics', start, stop, callback); }; - Topics.getTagTopicCount = function(tag, callback) { + Topics.getTagTopicCount = function (tag, callback) { db.sortedSetCard('tag:' + tag + ':topics', callback); }; - Topics.deleteTags = function(tags, callback) { + Topics.deleteTags = function (tags, callback) { if (!Array.isArray(tags) || !tags.length) { return callback(); } async.series([ - function(next) { + function (next) { removeTagsFromTopics(tags, next); }, - function(next) { - var keys = tags.map(function(tag) { + function (next) { + var keys = tags.map(function (tag) { return 'tag:' + tag + ':topics'; }); db.deleteAll(keys, next); }, - function(next) { + function (next) { db.sortedSetRemove('tags:topic:count', tags, next); } ], callback); }; function removeTagsFromTopics(tags, callback) { - async.eachLimit(tags, 50, function(tag, next) { - db.getSortedSetRange('tag:' + tag + ':topics', 0, -1, function(err, tids) { + async.eachLimit(tags, 50, function (tag, next) { + db.getSortedSetRange('tag:' + tag + ':topics', 0, -1, function (err, tids) { if (err || !tids.length) { return next(err); } - var keys = tids.map(function(tid) { + var keys = tids.map(function (tid) { return 'topic:' + tid + ':tags'; }); @@ -108,13 +131,13 @@ module.exports = function(Topics) { }, callback); } - Topics.deleteTag = function(tag) { + Topics.deleteTag = function (tag) { db.delete('tag:' + tag + ':topics'); db.sortedSetRemove('tags:topic:count', tag); }; - Topics.getTags = function(start, stop, callback) { - db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop, function(err, tags) { + Topics.getTags = function (start, stop, callback) { + db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop, function (err, tags) { if (err) { return callback(err); } @@ -123,17 +146,17 @@ module.exports = function(Topics) { }); }; - Topics.getTagData = function(tags, callback) { - var keys = tags.map(function(tag) { + Topics.getTagData = function (tags, callback) { + var keys = tags.map(function (tag) { return 'tag:' + tag.value; }); - db.getObjects(keys, function(err, tagData) { + db.getObjects(keys, function (err, tagData) { if (err) { return callback(err); } - tags.forEach(function(tag, index) { + tags.forEach(function (tag, index) { tag.color = tagData[index] ? tagData[index].color : ''; tag.bgColor = tagData[index] ? tagData[index].bgColor : ''; }); @@ -141,53 +164,53 @@ module.exports = function(Topics) { }); }; - Topics.getTopicTags = function(tid, callback) { + Topics.getTopicTags = function (tid, callback) { db.getSetMembers('topic:' + tid + ':tags', callback); }; - Topics.getTopicTagsObjects = function(tid, callback) { - Topics.getTopicsTagsObjects([tid], function(err, data) { + Topics.getTopicTagsObjects = function (tid, callback) { + Topics.getTopicsTagsObjects([tid], function (err, data) { callback(err, Array.isArray(data) && data.length ? data[0] : []); }); }; - Topics.getTopicsTagsObjects = function(tids, callback) { - var sets = tids.map(function(tid) { + Topics.getTopicsTagsObjects = function (tids, callback) { + var sets = tids.map(function (tid) { return 'topic:' + tid + ':tags'; }); - db.getSetsMembers(sets, function(err, topicTags) { + db.getSetsMembers(sets, function (err, topicTags) { if (err) { return callback(err); } var uniqueTopicTags = _.uniq(_.flatten(topicTags)); - var tags = uniqueTopicTags.map(function(tag) { + var tags = uniqueTopicTags.map(function (tag) { return {value: tag}; }); async.parallel({ - tagData: function(next) { + tagData: function (next) { Topics.getTagData(tags, next); }, - counts: function(next) { + counts: function (next) { db.sortedSetScores('tags:topic:count', uniqueTopicTags, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.tagData.forEach(function(tag, index) { + results.tagData.forEach(function (tag, index) { tag.score = results.counts[index] ? results.counts[index] : 0; }); var tagData = _.object(uniqueTopicTags, results.tagData); - topicTags.forEach(function(tags, index) { + topicTags.forEach(function (tags, index) { if (Array.isArray(tags)) { - topicTags[index] = tags.map(function(tag) {return tagData[tag];}); + topicTags[index] = tags.map(function (tag) {return tagData[tag];}); } }); @@ -196,79 +219,121 @@ module.exports = function(Topics) { }); }; - Topics.updateTags = function(tid, tags, callback) { - callback = callback || function() {}; + Topics.updateTags = function (tid, tags, callback) { + callback = callback || function () {}; async.waterfall([ - function(next) { + function (next) { Topics.deleteTopicTags(tid, next); }, - function(next) { + function (next) { Topics.getTopicField(tid, 'timestamp', next); }, - function(timestamp, next) { + function (timestamp, next) { Topics.createTags(tags, tid, timestamp, next); } ], callback); }; - Topics.deleteTopicTags = function(tid, callback) { - Topics.getTopicTags(tid, function(err, tags) { + Topics.deleteTopicTags = function (tid, callback) { + Topics.getTopicTags(tid, function (err, tags) { if (err) { return callback(err); } async.series([ - function(next) { + function (next) { db.delete('topic:' + tid + ':tags', next); }, - function(next) { - var sets = tags.map(function(tag) { + function (next) { + var sets = tags.map(function (tag) { return 'tag:' + tag + ':topics'; }); db.sortedSetsRemove(sets, tid, next); }, - function(next) { - async.each(tags, function(tag, next) { + function (next) { + async.each(tags, function (tag, next) { updateTagCount(tag, next); }, next); } - ], function(err, results) { + ], function (err) { callback(err); }); }); }; - Topics.searchTags = function(data, callback) { + Topics.searchTags = function (data, callback) { + function done(matches) { + plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function (err, data) { + callback(err, data ? data.matches : []); + }); + } + + if (!data || !data.query) { return callback(null, []); } - db.getSortedSetRevRange('tags:topic:count', 0, -1, function(err, tags) { + if (plugins.hasListeners('filter:topics.searchTags')) { + return plugins.fireHook('filter:topics.searchTags', {data: data}, function (err, data) { + if (err) { + return callback(err); + } + done(data.matches); + }); + } + + findMatches(data.query, function (err, matches) { if (err) { - return callback(null, []); + return callback(err); } + done(matches); + }); + }; + + Topics.autocompleteTags = function (data, callback) { + if (!data || !data.query) { + return callback(null, []); + } - data.query = data.query.toLowerCase(); + if (plugins.hasListeners('filter:topics.autocompleteTags')) { + return plugins.fireHook('filter:topics.autocompleteTags', {data: data}, function (err, data) { + if (err) { + return callback(err); + } + callback(null, data.matches); + }); + } + + findMatches(data.query, callback); + }; + + function findMatches(query, callback) { + db.getSortedSetRevRange('tags:topic:count', 0, -1, function (err, tags) { + if (err) { + return callback(err); + } + + query = query.toLowerCase(); var matches = []; - for(var i=0; i<tags.length; ++i) { - if (tags[i].toLowerCase().startsWith(data.query)) { + for(var i = 0; i < tags.length; ++i) { + if (tags[i].toLowerCase().startsWith(query)) { matches.push(tags[i]); + if (matches.length > 19) { + break; + } } } - matches = matches.slice(0, 20).sort(function(a, b) { + matches = matches.sort(function (a, b) { return a > b; }); - - plugins.fireHook('filter:tags.search', {data: data, matches: matches}, function(err, data) { - callback(err, data ? data.matches : []); - }); + callback(null, matches); }); - }; + } - Topics.searchAndLoadTags = function(data, callback) { + Topics.searchAndLoadTags = function (data, callback) { var searchResult = { tags: [], matchCount: 0, @@ -278,29 +343,29 @@ module.exports = function(Topics) { if (!data.query || !data.query.length) { return callback(null, searchResult); } - Topics.searchTags(data, function(err, tags) { + Topics.searchTags(data, function (err, tags) { if (err) { return callback(err); } async.parallel({ - counts: function(next) { + counts: function (next) { db.sortedSetScores('tags:topic:count', tags, next); }, - tagData: function(next) { - tags = tags.map(function(tag) { + tagData: function (next) { + tags = tags.map(function (tag) { return {value: tag}; }); Topics.getTagData(tags, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - results.tagData.forEach(function(tag, index) { + results.tagData.forEach(function (tag, index) { tag.score = results.counts[index]; }); - results.tagData.sort(function(a, b) { + results.tagData.sort(function (a, b) { return b.score - a.score; }); searchResult.tags = results.tagData; @@ -311,13 +376,13 @@ module.exports = function(Topics) { }); }; - Topics.getRelatedTopics = function(topicData, uid, callback) { + Topics.getRelatedTopics = function (topicData, uid, callback) { if (plugins.hasListeners('filter:topic.getRelatedTopics')) { return plugins.fireHook('filter:topic.getRelatedTopics', {topic: topicData, uid: uid}, callback); } - var maximumTopics = parseInt(meta.config.maximumRelatedTopics, 10); - if (maximumTopics === 0 || !topicData.tags.length) { + var maximumTopics = parseInt(meta.config.maximumRelatedTopics, 10) || 0; + if (maximumTopics === 0 || !topicData.tags || !topicData.tags.length) { return callback(null, []); } @@ -334,7 +399,7 @@ module.exports = function(Topics) { Topics.getTopics(tids, uid, next); }, function (topics, next) { - topics = topics.filter(function(topic) { + topics = topics.filter(function (topic) { return topic && !topic.deleted && parseInt(topic.uid, 10) !== parseInt(uid, 10); }); next(null, topics); diff --git a/src/topics/teaser.js b/src/topics/teaser.js index 5d730de74d..1467dd194f 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -2,20 +2,18 @@ 'use strict'; -var async = require('async'), - S = require('string'), +var async = require('async'); +var S = require('string'); - meta = require('../meta'), - db = require('../database'), - user = require('../user'), - posts = require('../posts'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'); +var meta = require('../meta'); +var user = require('../user'); +var posts = require('../posts'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); +module.exports = function (Topics) { -module.exports = function(Topics) { - - Topics.getTeasers = function(topics, callback) { + Topics.getTeasers = function (topics, callback) { if (!Array.isArray(topics) || !topics.length) { return callback(null, []); } @@ -25,35 +23,52 @@ module.exports = function(Topics) { var postData; var tidToPost = {}; - topics.forEach(function(topic) { + topics.forEach(function (topic) { counts.push(topic && (parseInt(topic.postcount, 10) || 0)); if (topic) { - teaserPids.push(meta.config.teaserPost === 'first' ? topic.mainPid : topic.teaserPid); + if (topic.teaserPid === 'null') { + delete topic.teaserPid; + } + + switch(meta.config.teaserPost) { + case 'first': + teaserPids.push(topic.mainPid); + break; + + case 'last-post': + teaserPids.push(topic.teaserPid || topic.mainPid); + break; + + case 'last-reply': // intentional fall-through + default: + teaserPids.push(topic.teaserPid); + break; + } } }); async.waterfall([ - function(next) { + function (next) { posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content'], next); }, - function(_postData, next) { + function (_postData, next) { postData = _postData; - var uids = postData.map(function(post) { + var uids = postData.map(function (post) { return post.uid; - }).filter(function(uid, index, array) { + }).filter(function (uid, index, array) { return array.indexOf(uid) === index; }); user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture'], next); }, - function(usersData, next) { + function (usersData, next) { var users = {}; - usersData.forEach(function(user) { + usersData.forEach(function (user) { users[user.uid] = user; }); - async.each(postData, function(post, next) { + async.each(postData, function (post, next) { // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. if (!users.hasOwnProperty(post.uid)) { post.uid = 0; @@ -65,8 +80,8 @@ module.exports = function(Topics) { posts.parsePost(post, next); }, next); }, - function(next) { - var teasers = topics.map(function(topic, index) { + function (next) { + var teasers = topics.map(function (topic, index) { if (!topic) { return null; } @@ -74,7 +89,7 @@ module.exports = function(Topics) { tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index]; if (tidToPost[topic.tid].content) { var s = S(tidToPost[topic.tid].content); - tidToPost[topic.tid].content = s.stripTags.apply(s, utils.stripTags.concat(['img'])).s; + tidToPost[topic.tid].content = s.stripTags.apply(s, utils.stripTags).s; } } return tidToPost[topic.tid]; @@ -82,40 +97,44 @@ module.exports = function(Topics) { plugins.fireHook('filter:teasers.get', {teasers: teasers}, next); }, - function(data, next) { + function (data, next) { next(null, data.teasers); } ], callback); }; - Topics.getTeasersByTids = function(tids, callback) { + Topics.getTeasersByTids = function (tids, callback) { if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } async.waterfall([ - function(next) { + function (next) { Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid'], next); }, - function(topics, next) { + function (topics, next) { Topics.getTeasers(topics, next); } ], callback); }; - Topics.getTeaser = function(tid, callback) { - Topics.getTeasersByTids([tid], function(err, teasers) { + Topics.getTeaser = function (tid, callback) { + Topics.getTeasersByTids([tid], function (err, teasers) { callback(err, Array.isArray(teasers) && teasers.length ? teasers[0] : null); }); }; - Topics.updateTeaser = function(tid, callback) { - Topics.getLatestUndeletedReply(tid, function(err, pid) { + Topics.updateTeaser = function (tid, callback) { + Topics.getLatestUndeletedReply(tid, function (err, pid) { if (err) { return callback(err); } pid = pid || null; - Topics.setTopicField(tid, 'teaserPid', pid, callback); + if (pid) { + Topics.setTopicField(tid, 'teaserPid', pid, callback); + } else { + Topics.deleteTopicField(tid, 'teaserPid', callback); + } }); }; }; \ No newline at end of file diff --git a/src/topics/thumb.js b/src/topics/thumb.js new file mode 100644 index 0000000000..7d22365293 --- /dev/null +++ b/src/topics/thumb.js @@ -0,0 +1,91 @@ + +'use strict'; + +var async = require('async'); +var nconf = require('nconf'); +var winston = require('winston'); +var path = require('path'); +var fs = require('fs'); +var request = require('request'); +var mime = require('mime'); +var validator = require('validator'); + +var meta = require('../meta'); +var image = require('../image'); +var file = require('../file'); +var plugins = require('../plugins'); + +module.exports = function (Topics) { + + Topics.resizeAndUploadThumb = function (data, callback) { + if (!data.thumb || !validator.isURL(data.thumb)) { + return callback(); + } + + var pathToUpload; + var filename; + + async.waterfall([ + function (next) { + request.head(data.thumb, next); + }, + function (res, body, next) { + + var type = res.headers['content-type']; + if (!type.match(/image./)) { + return next(new Error('[[error:invalid-file]]')); + } + + var extension = path.extname(data.thumb); + if (!extension) { + extension = '.' + mime.extension(type); + } + filename = Date.now() + '-topic-thumb' + extension; + pathToUpload = path.join(nconf.get('base_dir'), nconf.get('upload_path'), 'files', filename); + + request(data.thumb).pipe(fs.createWriteStream(pathToUpload)).on('close', next); + }, + function (next) { + file.isFileTypeAllowed(pathToUpload, next); + }, + function (next) { + var size = parseInt(meta.config.topicThumbSize, 10) || 120; + image.resizeImage({ + path: pathToUpload, + extension: path.extname(pathToUpload), + width: size, + height: size + }, next); + }, + function (next) { + if (!plugins.hasListeners('filter:uploadImage')) { + data.thumb = path.join(nconf.get('upload_url'), 'files', filename); + return callback(); + } + + plugins.fireHook('filter:uploadImage', {image: {path: pathToUpload, name: ''}, uid: data.uid}, next); + }, + function (uploadedFile, next) { + deleteFile(pathToUpload); + data.thumb = uploadedFile.url; + next(); + } + ], function (err) { + if (err) { + deleteFile(pathToUpload); + } + callback(err); + }); + }; + + function deleteFile(path) { + if (path) { + fs.unlink(path, function (err) { + if (err) { + winston.error(err); + } + }); + } + } + +}; diff --git a/src/topics/tools.js b/src/topics/tools.js index 1cb02ae514..6fce256f27 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -1,24 +1,25 @@ 'use strict'; -var async = require('async'), +var async = require('async'); - db = require('../database'), - categories = require('../categories'), - plugins = require('../plugins'), - privileges = require('../privileges'); +var db = require('../database'); +var categories = require('../categories'); +var meta = require('../meta'); +var plugins = require('../plugins'); +var privileges = require('../privileges'); -module.exports = function(Topics) { +module.exports = function (Topics) { var topicTools = {}; Topics.tools = topicTools; - topicTools.delete = function(tid, uid, callback) { + topicTools.delete = function (tid, uid, callback) { toggleDelete(tid, uid, true, callback); }; - topicTools.restore = function(tid, uid, callback) { + topicTools.restore = function (tid, uid, callback) { toggleDelete(tid, uid, false, callback); }; @@ -32,10 +33,10 @@ module.exports = function(Topics) { if (!exists) { return next(new Error('[[error:no-topic]]')); } - privileges.topics.isOwnerOrAdminOrMod(tid, uid, next); + privileges.topics.canDelete(tid, uid, next); }, - function (isOwnerOrAdminOrMod, next) { - if (!isOwnerOrAdminOrMod) { + function (canDelete, next) { + if (!canDelete) { return next(new Error('[[error:no-privileges]]')); } Topics.getTopicFields(tid, ['tid', 'cid', 'uid', 'deleted', 'title', 'mainPid'], next); @@ -72,13 +73,13 @@ module.exports = function(Topics) { ], callback); } - topicTools.purge = function(tid, uid, callback) { + topicTools.purge = function (tid, uid, callback) { var cid; async.waterfall([ - function(next) { + function (next) { Topics.exists(tid, next); }, - function(exists, next) { + function (exists, next) { if (!exists) { return callback(); } @@ -102,16 +103,16 @@ module.exports = function(Topics) { ], callback); }; - topicTools.lock = function(tid, uid, callback) { + topicTools.lock = function (tid, uid, callback) { toggleLock(tid, uid, true, callback); }; - topicTools.unlock = function(tid, uid, callback) { + topicTools.unlock = function (tid, uid, callback) { toggleLock(tid, uid, false, callback); }; function toggleLock(tid, uid, lock, callback) { - callback = callback || function() {}; + callback = callback || function () {}; var cid; @@ -148,11 +149,11 @@ module.exports = function(Topics) { ], callback); } - topicTools.pin = function(tid, uid, callback) { + topicTools.pin = function (tid, uid, callback) { togglePin(tid, uid, true, callback); }; - topicTools.unpin = function(tid, uid, callback) { + topicTools.unpin = function (tid, uid, callback) { togglePin(tid, uid, false, callback); }; @@ -172,7 +173,7 @@ module.exports = function(Topics) { topicData = _topicData; privileges.categories.isAdminOrMod(_topicData.cid, uid, next); }, - function(isAdminOrMod, next) { + function (isAdminOrMod, next) { if (!isAdminOrMod) { return next(new Error('[[error:no-privileges]]')); } @@ -181,7 +182,7 @@ module.exports = function(Topics) { async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', pin ? Math.pow(2, 53) : topicData.lastposttime, tid) ], next); }, - function(results, next) { + function (results, next) { var data = { tid: tid, isPinned: pin, @@ -196,7 +197,7 @@ module.exports = function(Topics) { ], callback); } - topicTools.move = function(tid, cid, uid, callback) { + topicTools.move = function (tid, cid, uid, callback) { var topic; async.waterfall([ function (next) { @@ -218,16 +219,16 @@ module.exports = function(Topics) { function (next) { var timestamp = parseInt(topic.pinned, 10) ? Math.pow(2, 53) : topic.lastposttime; async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('cid:' + cid + ':tids', timestamp, tid, next); }, - function(next) { + function (next) { topic.postcount = topic.postcount || 0; db.sortedSetAdd('cid:' + cid + ':tids:posts', topic.postcount, tid, next); } ], next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } @@ -242,9 +243,12 @@ module.exports = function(Topics) { categories.incrementCategoryFieldBy(cid, 'topic_count', 1, next); }, function (next) { - Topics.setTopicField(tid, 'cid', cid, next); + Topics.setTopicFields(tid, { + cid: cid, + oldCid: oldCid + }, next); } - ], function(err) { + ], function (err) { if (err) { return callback(err); } diff --git a/src/topics/unread.js b/src/topics/unread.js index 28846640b7..b4a5d07514 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -12,15 +12,20 @@ var privileges = require('../privileges'); var meta = require('../meta'); var utils = require('../../public/src/utils'); -module.exports = function(Topics) { +module.exports = function (Topics) { - Topics.getTotalUnread = function(uid, callback) { - Topics.getUnreadTids(0, uid, 0, 99, function(err, tids) { - callback(err, tids ? tids.length : 0); + Topics.getTotalUnread = function (uid, filter, callback) { + if (!callback) { + callback = filter; + filter = ''; + } + Topics.getUnreadTids(0, uid, filter, function (err, tids) { + callback(err, Array.isArray(tids) ? tids.length : 0); }); }; - Topics.getUnreadTopics = function(cid, uid, start, stop, callback) { + + Topics.getUnreadTopics = function (cid, uid, start, stop, filter, callback) { var unreadTopics = { showSelect: true, @@ -29,16 +34,25 @@ module.exports = function(Topics) { }; async.waterfall([ - function(next) { - Topics.getUnreadTids(cid, uid, start, stop, next); + function (next) { + Topics.getUnreadTids(cid, uid, filter, next); }, - function(tids, next) { + function (tids, next) { + unreadTopics.topicCount = tids.length; + if (!tids.length) { return next(null, []); } + + if (stop === -1) { + tids = tids.slice(start); + } else { + tids = tids.slice(start, stop + 1); + } + Topics.getTopicsByTids(tids, uid, next); }, - function(topicData, next) { + function (topicData, next) { if (!Array.isArray(topicData) || !topicData.length) { return next(null, unreadTopics); } @@ -50,11 +64,11 @@ module.exports = function(Topics) { ], callback); }; - Topics.unreadCutoff = function() { + Topics.unreadCutoff = function () { return Date.now() - (parseInt(meta.config.unreadCutoff, 10) || 2) * 86400000; }; - Topics.getUnreadTids = function(cid, uid, start, stop, callback) { + Topics.getUnreadTids = function (cid, uid, filter, callback) { uid = parseInt(uid, 10); if (uid === 0) { return callback(null, []); @@ -62,80 +76,120 @@ module.exports = function(Topics) { var cutoff = Topics.unreadCutoff(); - async.parallel({ - ignoredCids: function(next) { - user.getIgnoredCategories(uid, next); - }, - recentTids: function(next) { - db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next); - }, - userScores: function(next) { - db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff, next); - }, - tids_unread: function(next) { - db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1, next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - - if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) { - return callback(null, []); - } + var ignoredCids; - var userRead = {}; - results.userScores.forEach(function(userItem) { - userRead[userItem.value] = userItem.score; - }); + async.waterfall([ + function (next) { + async.parallel({ + ignoredCids: function (next) { + if (filter === 'watched') { + return next(null, []); + } + user.getIgnoredCategories(uid, next); + }, + ignoredTids: function (next) { + user.getIgnoredTids(uid, 0, -1, next); + }, + recentTids: function (next) { + db.getSortedSetRevRangeByScoreWithScores('topics:recent', 0, -1, '+inf', cutoff, next); + }, + userScores: function (next) { + db.getSortedSetRevRangeByScoreWithScores('uid:' + uid + ':tids_read', 0, -1, '+inf', cutoff, next); + }, + tids_unread: function (next) { + db.getSortedSetRevRangeWithScores('uid:' + uid + ':tids_unread', 0, -1, next); + } + }, next); + }, + function (results, next) { + if (results.recentTids && !results.recentTids.length && !results.tids_unread.length) { + return callback(null, []); + } - results.recentTids = results.recentTids.concat(results.tids_unread); - results.recentTids.sort(function(a, b) { - return b.score - a.score; - }); + ignoredCids = results.ignoredCids; - var tids = results.recentTids.filter(function(recentTopic) { - return !userRead[recentTopic.value] || recentTopic.score > userRead[recentTopic.value]; - }).map(function(topic) { - return topic.value; - }).filter(function(tid, index, array) { - return array.indexOf(tid) === index; - }); + var userRead = {}; + results.userScores.forEach(function (userItem) { + userRead[userItem.value] = userItem.score; + }); - tids = tids.slice(0, 100); + results.recentTids = results.recentTids.concat(results.tids_unread); + results.recentTids.sort(function (a, b) { + return b.score - a.score; + }); - filterTopics(uid, tids, cid, results.ignoredCids, function(err, tids) { - if (err) { - return callback(err); - } + var tids = results.recentTids.filter(function (recentTopic) { + if (results.ignoredTids.indexOf(recentTopic.value.toString()) !== -1) { + return false; + } + switch (filter) { + case 'new': + return !userRead[recentTopic.value]; + default: + return !userRead[recentTopic.value] || recentTopic.score > userRead[recentTopic.value]; + } + }).map(function (topic) { + return topic.value; + }).filter(function (tid, index, array) { + return array.indexOf(tid) === index; + }); - if (stop === -1) { - tids = tids.slice(start); + if (filter === 'watched') { + filterWatchedTids(uid, tids, next); } else { - tids = tids.slice(start, stop + 1); + next(null, tids); } + }, + function (tids, next) { + + tids = tids.slice(0, 200); + + filterTopics(uid, tids, cid, ignoredCids, filter, next); + } + ], callback); + }; - callback(null, tids); + function filterWatchedTids(uid, tids, callback) { + db.sortedSetScores('uid:' + uid + ':followed_tids', tids, function (err, scores) { + if (err) { + return callback(err); + } + tids = tids.filter(function (tid, index) { + return tid && !!scores[index]; }); + callback(null, tids); }); - }; + } - function filterTopics(uid, tids, cid, ignoredCids, callback) { + function filterTopics(uid, tids, cid, ignoredCids, filter, callback) { if (!Array.isArray(ignoredCids) || !tids.length) { return callback(null, tids); } async.waterfall([ - function(next) { + function (next) { privileges.topics.filterTids('read', tids, uid, next); }, - function(tids, next) { - Topics.getTopicsFields(tids, ['tid', 'cid'], next); + function (tids, next) { + async.parallel({ + topics: function (next) { + Topics.getTopicsFields(tids, ['tid', 'cid'], next); + }, + isTopicsFollowed: function (next) { + if (filter === 'watched' || filter === 'new') { + return next(null, []); + } + db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next); + } + }, next); }, - function(topics, next) { - tids = topics.filter(function(topic) { - return topic && topic.cid && ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10)); - }).map(function(topic) { + function (results, next) { + var topics = results.topics; + tids = topics.filter(function (topic, index) { + return topic && topic.cid && + (!!results.isTopicsFollowed[index] || ignoredCids.indexOf(topic.cid.toString()) === -1) && + (!cid || parseInt(cid, 10) === parseInt(topic.cid, 10)); + }).map(function (topic) { return topic.tid; }); next(null, tids); @@ -143,13 +197,13 @@ module.exports = function(Topics) { ], callback); } - Topics.pushUnreadCount = function(uid, callback) { - callback = callback || function() {}; + Topics.pushUnreadCount = function (uid, callback) { + callback = callback || function () {}; if (!uid || parseInt(uid, 10) === 0) { return callback(); } - Topics.getTotalUnread(uid, function(err, count) { + Topics.getTotalUnread(uid, function (err, count) { if (err) { return callback(err); } @@ -159,17 +213,17 @@ module.exports = function(Topics) { }); }; - Topics.markAsUnreadForAll = function(tid, callback) { + Topics.markAsUnreadForAll = function (tid, callback) { Topics.markCategoryUnreadForAll(tid, callback); }; - Topics.markAsRead = function(tids, uid, callback) { - callback = callback || function() {}; + Topics.markAsRead = function (tids, uid, callback) { + callback = callback || function () {}; if (!Array.isArray(tids) || !tids.length) { return callback(); } - tids = tids.filter(function(tid, index, array) { + tids = tids.filter(function (tid, index, array) { return tid && utils.isNumber(tid) && array.indexOf(tid) === index; }); @@ -185,7 +239,7 @@ module.exports = function(Topics) { }, next); }, function (results, next) { - tids = tids.filter(function(tid, index) { + tids = tids.filter(function (tid, index) { return results.topicScores[index] && (!results.userScores[index] || results.userScores[index] < results.topicScores[index]); }); @@ -194,20 +248,20 @@ module.exports = function(Topics) { } var now = Date.now(); - var scores = tids.map(function() { + var scores = tids.map(function () { return now; }); async.parallel({ markRead: async.apply(db.sortedSetAdd, 'uid:' + uid + ':tids_read', scores, tids), markUnread: async.apply(db.sortedSetRemove, 'uid:' + uid + ':tids_unread', tids), - topicData: async.apply( Topics.getTopicsFields, tids, ['cid']) + topicData: async.apply(Topics.getTopicsFields, tids, ['cid']) }, next); }, function (results, next) { - var cids = results.topicData.map(function(topic) { + var cids = results.topicData.map(function (topic) { return topic && topic.cid; - }).filter(function(topic, index, array) { + }).filter(function (topic, index, array) { return topic && array.indexOf(topic) === index; }); @@ -219,15 +273,13 @@ module.exports = function(Topics) { ], callback); }; - Topics.markAllRead = function(uid, callback) { + Topics.markAllRead = function (uid, callback) { async.waterfall([ function (next) { db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', Topics.unreadCutoff(), next); }, function (tids, next) { - for (var i=0; i<tids.length; ++i) { - Topics.markTopicNotificationsRead(tids[i], uid); - } + Topics.markTopicNotificationsRead(tids, uid); Topics.markAsRead(tids, uid, next); }, function (markedRead, next) { @@ -236,22 +288,28 @@ module.exports = function(Topics) { ], callback); }; - Topics.markTopicNotificationsRead = function(tid, uid) { - if (!tid) { + Topics.markTopicNotificationsRead = function (tids, uid) { + if (!Array.isArray(tids) || !tids.length) { return; } - user.notifications.getUnreadByField(uid, 'tid', tid, function(err, nids) { + + async.waterfall([ + function (next) { + user.notifications.getUnreadByField(uid, 'tid', tids, next); + }, + function (nids, next) { + notifications.markReadMultiple(nids, uid, next); + } + ], function (err) { if (err) { - return winston.error(err.stack); + return winston.error(err); } - notifications.markReadMultiple(nids, uid, function() { - user.notifications.pushCount(uid); - }); + user.notifications.pushCount(uid); }); }; - Topics.markCategoryUnreadForAll = function(tid, callback) { - Topics.getTopicField(tid, 'cid', function(err, cid) { + Topics.markCategoryUnreadForAll = function (tid, callback) { + Topics.getTopicField(tid, 'cid', function (err, cid) { if(err) { return callback(err); } @@ -260,30 +318,30 @@ module.exports = function(Topics) { }); }; - Topics.hasReadTopics = function(tids, uid, callback) { + Topics.hasReadTopics = function (tids, uid, callback) { if (!parseInt(uid, 10)) { - return callback(null, tids.map(function() { + return callback(null, tids.map(function () { return false; })); } async.parallel({ - recentScores: function(next) { + recentScores: function (next) { db.sortedSetScores('topics:recent', tids, next); }, - userScores: function(next) { + userScores: function (next) { db.sortedSetScores('uid:' + uid + ':tids_read', tids, next); }, tids_unread: function (next) { db.sortedSetScores('uid:' + uid + ':tids_unread', tids, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } var cutoff = Topics.unreadCutoff(); - var result = tids.map(function(tid, index) { + var result = tids.map(function (tid, index) { return !results.tids_unread[index] && (results.recentScores[index] < cutoff || !!(results.userScores[index] && results.userScores[index] >= results.recentScores[index])); @@ -293,13 +351,13 @@ module.exports = function(Topics) { }); }; - Topics.hasReadTopic = function(tid, uid, callback) { - Topics.hasReadTopics([tid], uid, function(err, hasRead) { + Topics.hasReadTopic = function (tid, uid, callback) { + Topics.hasReadTopics([tid], uid, function (err, hasRead) { callback(err, Array.isArray(hasRead) && hasRead.length ? hasRead[0] : false); }); }; - Topics.markUnread = function(tid, uid, callback) { + Topics.markUnread = function (tid, uid, callback) { async.waterfall([ function (next) { Topics.exists(tid, next); diff --git a/src/topics/user.js b/src/topics/user.js index 4db380efdf..ab56a5f778 100644 --- a/src/topics/user.js +++ b/src/topics/user.js @@ -2,40 +2,23 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - posts = require('../posts'); +var async = require('async'); +var db = require('../database'); +var posts = require('../posts'); +module.exports = function (Topics) { -module.exports = function(Topics) { - - Topics.isOwner = function(tid, uid, callback) { + Topics.isOwner = function (tid, uid, callback) { uid = parseInt(uid, 10); if (!uid) { return callback(null, false); } - Topics.getTopicField(tid, 'uid', function(err, author) { + Topics.getTopicField(tid, 'uid', function (err, author) { callback(err, parseInt(author, 10) === uid); }); }; - Topics.getUids = function(tid, callback) { - async.waterfall([ - function(next) { - Topics.getPids(tid, next); - }, - function(pids, next) { - posts.getPostsFields(pids, ['uid'], next); - }, - function(postData, next) { - var uids = postData.map(function(post) { - return post && post.uid; - }).filter(function(uid, index, array) { - return uid && array.indexOf(uid) === index; - }); - - next(null, uids); - } - ], callback); + Topics.getUids = function (tid, callback) { + db.getSortedSetRevRangeByScore('tid:' + tid + ':posters', 0, -1, '+inf', 1, callback); }; }; \ No newline at end of file diff --git a/src/upgrade.js b/src/upgrade.js index f6f1ce0093..03c776dce3 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -6,20 +6,20 @@ var db = require('./database'), Upgrade = {}, - minSchemaDate = Date.UTC(2015, 7, 18), // This value gets updated every new MINOR version + minSchemaDate = Date.UTC(2015, 10, 6), // This value gets updated every new MAJOR version schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - latestSchema = Date.UTC(2016, 1, 25); + latestSchema = Date.UTC(2016, 9, 8); -Upgrade.check = function(callback) { - db.get('schemaDate', function(err, value) { +Upgrade.check = function (callback) { + db.get('schemaDate', function (err, value) { if (err) { return callback(err); } if (!value) { - db.set('schemaDate', latestSchema, function(err) { + db.set('schemaDate', latestSchema, function (err) { if (err) { return callback(err); } @@ -33,21 +33,25 @@ Upgrade.check = function(callback) { }); }; -Upgrade.update = function(schemaDate, callback) { +Upgrade.update = function (schemaDate, callback) { db.set('schemaDate', schemaDate, callback); }; -Upgrade.upgrade = function(callback) { +Upgrade.upgrade = function (callback) { var updatesMade = false; winston.info('Beginning database schema update'); async.series([ - function(next) { + function (next) { // Prepare for upgrade & check to make sure the upgrade is possible - db.get('schemaDate', function(err, value) { + db.get('schemaDate', function (err, value) { + if (err) { + return next(err); + } + if(!value) { - db.set('schemaDate', latestSchema, function() { + db.set('schemaDate', latestSchema, function () { next(); }); schemaDate = latestSchema; @@ -62,96 +66,14 @@ Upgrade.upgrade = function(callback) { } }); }, - function(next) { - thisSchemaDate = Date.UTC(2015, 8, 30); - if (schemaDate < thisSchemaDate) { - updatesMade = true; - winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar'); - - async.waterfall([ - async.apply(db.isObjectField, 'config', 'customGravatarDefaultImage'), - function(keyExists, _next) { - if (keyExists) { - _next(); - } else { - winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped'); - Upgrade.update(thisSchemaDate, next); - next(); - } - }, - async.apply(db.getObjectField, 'config', 'customGravatarDefaultImage'), - async.apply(db.setObjectField, 'config', 'defaultAvatar'), - async.apply(db.deleteObjectField, 'config', 'customGravatarDefaultImage') - ], function(err) { - if (err) { - return next(err); - } - - winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar done'); - Upgrade.update(thisSchemaDate, next); - }); - } else { - winston.info('[2015/09/30] Converting default Gravatar image to default User Avatar skipped'); - next(); - } - }, - function(next) { - thisSchemaDate = Date.UTC(2015, 10, 6); - if (schemaDate < thisSchemaDate) { - updatesMade = true; - winston.info('[2015/11/06] Removing gravatar'); - - db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) { - if (err) { - return next(err); - } - - async.eachLimit(uids, 500, function(uid, next) { - db.getObjectFields('user:' + uid, ['picture', 'gravatarpicture'], function(err, userData) { - if (err) { - return next(err); - } - - if (!userData.picture || !userData.gravatarpicture) { - return next(); - } - - if (userData.gravatarpicture === userData.picture) { - async.series([ - function (next) { - db.setObjectField('user:' + uid, 'picture', '', next); - }, - function (next) { - db.deleteObjectField('user:' + uid, 'gravatarpicture', next); - } - ], next); - } else { - db.deleteObjectField('user:' + uid, 'gravatarpicture', next); - } - }); - }, function(err) { - if (err) { - return next(err); - } - - winston.info('[2015/11/06] Gravatar pictures removed!'); - Upgrade.update(thisSchemaDate, next); - }); - }); - - } else { - winston.info('[2015/11/06] Gravatar removal skipped'); - next(); - } - }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2015, 11, 15); if (schemaDate < thisSchemaDate) { updatesMade = true; winston.info('[2015/12/15] Upgrading chats'); - db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function(err, globalData) { + db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function (err, globalData) { if (err) { return next(err); } @@ -160,16 +82,16 @@ Upgrade.upgrade = function(callback) { var roomId = globalData.nextChatRoomId || 1; var currentMid = 1; - async.whilst(function() { + async.whilst(function () { return currentMid <= globalData.nextMid; - }, function(next) { - db.getObject('message:' + currentMid, function(err, message) { + }, function (next) { + db.getObject('message:' + currentMid, function (err, message) { function addMessageToUids(roomId, callback) { async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('uid:' + message.fromuid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next); }, - function(next) { + function (next) { db.sortedSetAdd('uid:' + message.touid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next); } ], callback); @@ -186,7 +108,7 @@ Upgrade.upgrade = function(callback) { if (rooms[pairID]) { winston.info('adding message ' + currentMid + ' to existing roomID ' + roomId); - addMessageToUids(rooms[pairID], function(err) { + addMessageToUids(rooms[pairID], function (err) { if (err) { return next(err); } @@ -196,19 +118,19 @@ Upgrade.upgrade = function(callback) { } else { winston.info('adding message ' + currentMid + ' to new roomID ' + roomId); async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('uid:' + message.fromuid + ':chat:rooms', msgTime, roomId, next); }, - function(next) { + function (next) { db.sortedSetAdd('uid:' + message.touid + ':chat:rooms', msgTime, roomId, next); }, - function(next) { + function (next) { db.sortedSetAdd('chat:room:' + roomId + ':uids', [msgTime, msgTime + 1], [message.fromuid, message.touid], next); }, - function(next) { + function (next) { addMessageToUids(roomId, next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -219,7 +141,7 @@ Upgrade.upgrade = function(callback) { }); } }); - }, function(err) { + }, function (err) { if (err) { return next(err); } @@ -233,22 +155,22 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2015, 11, 23); if (schemaDate < thisSchemaDate) { updatesMade = true; winston.info('[2015/12/23] Upgrading chat room hashes'); - db.getObjectField('global', 'nextChatRoomId', function(err, nextChatRoomId) { + db.getObjectField('global', 'nextChatRoomId', function (err, nextChatRoomId) { if (err) { return next(err); } var currentChatRoomId = 1; - async.whilst(function() { + async.whilst(function () { return currentChatRoomId <= nextChatRoomId; - }, function(next) { - db.getSortedSetRange('chat:room:' + currentChatRoomId + ':uids', 0, 0, function(err, uids) { + }, function (next) { + db.getSortedSetRange('chat:room:' + currentChatRoomId + ':uids', 0, 0, function (err, uids) { if (err) { return next(err); } @@ -257,7 +179,7 @@ Upgrade.upgrade = function(callback) { return next(); } - db.setObject('chat:room:' + currentChatRoomId, {owner: uids[0], roomId: currentChatRoomId}, function(err) { + db.setObject('chat:room:' + currentChatRoomId, {owner: uids[0], roomId: currentChatRoomId}, function (err) { if (err) { return next(err); } @@ -265,7 +187,7 @@ Upgrade.upgrade = function(callback) { next(); }); }); - }, function(err) { + }, function (err) { if (err) { return next(err); } @@ -279,7 +201,7 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2016, 0, 11); if (schemaDate < thisSchemaDate) { @@ -289,7 +211,7 @@ Upgrade.upgrade = function(callback) { async.waterfall([ async.apply(db.getObjectField, 'config', 'theme:id'), async.apply(db.sortedSetAdd, 'plugins:active', 0) - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -302,7 +224,7 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2016, 0, 14); if (schemaDate < thisSchemaDate) { @@ -311,9 +233,9 @@ Upgrade.upgrade = function(callback) { var batch = require('./batch'); - batch.processSortedSet('posts:pid', function(ids, next) { - async.eachSeries(ids, function(id, next) { - db.getObjectFields('post:' + id, ['pid', 'uid', 'votes'], function(err, postData) { + batch.processSortedSet('posts:pid', function (ids, next) { + async.eachSeries(ids, function (id, next) { + db.getObjectFields('post:' + id, ['pid', 'uid', 'votes'], function (err, postData) { if (err) { return next(err); } @@ -324,7 +246,7 @@ Upgrade.upgrade = function(callback) { db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next); }); }, next); - }, {}, function(err) { + }, {}, function (err) { if (err) { return next(err); } @@ -336,7 +258,7 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2016, 0, 20); if (schemaDate < thisSchemaDate) { @@ -345,9 +267,9 @@ Upgrade.upgrade = function(callback) { var batch = require('./batch'); var now = Date.now(); - batch.processSortedSet('users:joindate', function(ids, next) { - async.eachSeries(ids, function(id, next) { - db.getObjectFields('user:' + id, ['uid', 'email:confirmed'], function(err, userData) { + batch.processSortedSet('users:joindate', function (ids, next) { + async.eachSeries(ids, function (id, next) { + db.getObjectFields('user:' + id, ['uid', 'email:confirmed'], function (err, userData) { if (err) { return next(err); } @@ -358,7 +280,7 @@ Upgrade.upgrade = function(callback) { db.sortedSetAdd('users:notvalidated', now, userData.uid, next); }); }, next); - }, {}, function(err) { + }, {}, function (err) { if (err) { return next(err); } @@ -370,7 +292,7 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2016, 0, 23); if (schemaDate < thisSchemaDate) { @@ -398,7 +320,7 @@ Upgrade.upgrade = function(callback) { function (groupData, next) { groups.show('Global Moderators', next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -411,7 +333,7 @@ Upgrade.upgrade = function(callback) { next(); } }, - function(next) { + function (next) { thisSchemaDate = Date.UTC(2016, 1, 25); if (schemaDate < thisSchemaDate) { @@ -426,7 +348,7 @@ Upgrade.upgrade = function(callback) { function (next) { db.deleteObjectField('config', 'disableSocialButtons', next); } - ], function(err) { + ], function (err) { if (err) { return next(err); } @@ -438,10 +360,557 @@ Upgrade.upgrade = function(callback) { winston.info('[2016/02/25] Social: Post Sharing skipped!'); next(); } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 3, 14); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/04/14] Group title from settings to user profile'); + + var user = require('./user'); + var batch = require('./batch'); + var count = 0; + batch.processSortedSet('users:joindate', function (uids, next) { + winston.info('upgraded ' + count + ' users'); + user.getMultipleUserSettings(uids, function (err, settings) { + if (err) { + return next(err); + } + count += uids.length; + settings = settings.filter(function (setting) { + return setting && setting.groupTitle; + }); + + async.each(settings, function (setting, next) { + db.setObjectField('user:' + setting.uid, 'groupTitle', setting.groupTitle, next); + }, next); + }); + }, {}, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/04/14] Group title from settings to user profile done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/04/14] Group title from settings to user profile skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 3, 18); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/04/19] Users post count per tid'); + + var batch = require('./batch'); + var topics = require('./topics'); + var count = 0; + batch.processSortedSet('topics:tid', function (tids, next) { + winston.info('upgraded ' + count + ' topics'); + count += tids.length; + async.each(tids, function (tid, next) { + db.delete('tid:' + tid + ':posters', function (err) { + if (err) { + return next(err); + } + topics.getPids(tid, function (err, pids) { + if (err) { + return next(err); + } + + if (!pids.length) { + return next(); + } + + async.eachSeries(pids, function (pid, next) { + db.getObjectField('post:' + pid, 'uid', function (err, uid) { + if (err) { + return next(err); + } + if (!parseInt(uid, 10)) { + return next(); + } + db.sortedSetIncrBy('tid:' + tid + ':posters', 1, uid, next); + }); + }, next); + }); + }); + }, next); + }, {}, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/04/19] Users post count per tid done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/04/19] Users post count per tid skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 3, 29); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/04/29] Dismiss flags from deleted topics'); + + var posts = require('./posts'), + topics = require('./topics'); + + var pids, tids; + + async.waterfall([ + async.apply(db.getSortedSetRange, 'posts:flagged', 0, -1), + function (_pids, next) { + pids = _pids; + posts.getPostsFields(pids, ['tid'], next); + }, + function (_tids, next) { + tids = _tids.map(function (a) { + return a.tid; + }); + + topics.getTopicsFields(tids, ['deleted'], next); + }, + function (state, next) { + var toDismiss = state.map(function (a, idx) { + return parseInt(a.deleted, 10) === 1 ? pids[idx] : null; + }).filter(Boolean); + + winston.info('[2016/04/29] ' + toDismiss.length + ' dismissable flags found'); + async.each(toDismiss, posts.dismissFlag, next); + } + ], function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/04/29] Dismiss flags from deleted topics done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/04/29] Dismiss flags from deleted topics skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 4, 28); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category'); + + var groupsAPI = require('./groups'); + var privilegesAPI = require('./privileges'); + + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return next(err); + } + + async.eachSeries(cids, function (cid, next) { + privilegesAPI.categories.list(cid, function (err, data) { + if (err) { + return next(err); + } + + var groups = data.groups; + var users = data.users; + + async.waterfall([ + function (next) { + async.eachSeries(groups, function (group, next) { + if (group.privileges['groups:read']) { + return groupsAPI.join('cid:' + cid + ':privileges:groups:topics:read', group.name, function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:groups:topics:read granted to gid: ' + group.name); + } + + return next(err); + }); + } + + next(null); + }, next); + }, + function (next) { + async.eachSeries(users, function (user, next) { + if (user.privileges.read) { + return groupsAPI.join('cid:' + cid + ':privileges:topics:read', user.uid, function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:topics:read granted to uid: ' + user.uid); + } + + return next(err); + }); + } + + next(null); + }, next); + } + ], function (err) { + if (!err) { + winston.info('-- cid ' + cid + ' upgraded'); + } + + next(err); + }); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category - done'); + Upgrade.update(thisSchemaDate, next); + }); + }); + } else { + winston.info('[2016/05/28] Giving topics:read privs to any group that was previously allowed to Find & Access Category - skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 5, 13); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/06/13] Store upvotes/downvotes separately'); + + var batch = require('./batch'); + var posts = require('./posts'); + var count = 0; + batch.processSortedSet('posts:pid', function (pids, next) { + winston.info('upgraded ' + count + ' posts'); + count += pids.length; + async.each(pids, function (pid, next) { + async.parallel({ + upvotes: function (next) { + db.setCount('pid:' + pid + ':upvote', next); + }, + downvotes: function (next) { + db.setCount('pid:' + pid + ':downvote', next); + } + }, function (err, results) { + if (err) { + return next(err); + } + var data = {}; + + if (parseInt(results.upvotes, 10) > 0) { + data.upvotes = results.upvotes; + } + if (parseInt(results.downvotes, 10) > 0) { + data.downvotes = results.downvotes; + } + + if (Object.keys(data).length) { + posts.setPostFields(pid, data, next); + } else { + next(); + } + }, next); + }, next); + }, {}, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/06/13] Store upvotes/downvotes separately done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/06/13] Store upvotes/downvotes separately skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 6, 12); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/07/12] Giving upload privileges'); + var privilegesAPI = require('./privileges'); + var meta = require('./meta'); + + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return next(err); + } + + async.eachSeries(cids, function (cid, next) { + privilegesAPI.categories.list(cid, function (err, data) { + if (err) { + return next(err); + } + async.eachSeries(data.groups, function (group, next) { + if (group.name === 'guests' && parseInt(meta.config.allowGuestUploads, 10) !== 1) { + return next(); + } + if (group.privileges['groups:read']) { + privilegesAPI.categories.give(['upload:post:image'], cid, group.name, next); + } else { + next(); + } + }, next); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/07/12] Upload privileges done'); + Upgrade.update(thisSchemaDate, next); + }); + }); + } else { + winston.info('[2016/07/12] Upload privileges skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 7, 5); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/08/05] Removing best posts with negative scores'); + var batch = require('./batch'); + batch.processSortedSet('users:joindate', function (ids, next) { + async.each(ids, function (id, next) { + console.log('processing uid ' + id); + db.sortedSetsRemoveRangeByScore(['uid:' + id + ':posts:votes'], '-inf', 0, next); + }, next); + }, {}, function (err) { + if (err) { + return next(err); + } + winston.info('[2016/08/05] Removing best posts with negative scores done!'); + Upgrade.update(thisSchemaDate, next); + }); + + } else { + winston.info('[2016/08/05] Removing best posts with negative scores skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 8, 7); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories'); + + var groupsAPI = require('./groups'); + var privilegesAPI = require('./privileges'); + + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return next(err); + } + + async.eachSeries(cids, function (cid, next) { + privilegesAPI.categories.list(cid, function (err, data) { + if (err) { + return next(err); + } + + var groups = data.groups; + var users = data.users; + + async.waterfall([ + function (next) { + async.eachSeries(groups, function (group, next) { + if (group.privileges['groups:topics:reply']) { + return async.parallel([ + async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:groups:posts:edit', group.name), + async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:groups:posts:delete', group.name) + ], function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:groups:posts:edit, cid:' + cid + ':privileges:groups:posts:delete granted to gid: ' + group.name); + } + + return next(err); + }); + } + + next(null); + }, next); + }, + function (next) { + async.eachSeries(groups, function (group, next) { + if (group.privileges['groups:topics:create']) { + return groupsAPI.join('cid:' + cid + ':privileges:groups:topics:delete', group.name, function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:groups:topics:delete granted to gid: ' + group.name); + } + + return next(err); + }); + } + + next(null); + }, next); + }, + function (next) { + async.eachSeries(users, function (user, next) { + if (user.privileges['topics:reply']) { + return async.parallel([ + async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:posts:edit', user.uid), + async.apply(groupsAPI.join, 'cid:' + cid + ':privileges:posts:delete', user.uid) + ], function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:posts:edit, cid:' + cid + ':privileges:posts:delete granted to uid: ' + user.uid); + } + + return next(err); + }); + } + + next(null); + }, next); + }, + function (next) { + async.eachSeries(users, function (user, next) { + if (user.privileges['topics:create']) { + return groupsAPI.join('cid:' + cid + ':privileges:topics:delete', user.uid, function (err) { + if (!err) { + winston.info('cid:' + cid + ':privileges:topics:delete granted to uid: ' + user.uid); + } + + return next(err); + }); + } + + next(null); + }, next); + } + ], function (err) { + if (!err) { + winston.info('-- cid ' + cid + ' upgraded'); + } + + next(err); + }); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories - done'); + Upgrade.update(thisSchemaDate, next); + }); + }); + } else { + winston.info('[2016/08/07] Granting edit/delete/delete topic on existing categories - skipped!'); + next(); + } + }, + function (next) { + thisSchemaDate = Date.UTC(2016, 8, 22); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/09/22] Setting category recent tids'); + + + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return next(err); + } + + async.eachSeries(cids, function (cid, next) { + db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0, function (err, pid) { + if (err || !pid) { + return next(err); + } + db.getObjectFields('post:' + pid, ['tid', 'timestamp'], function (err, postData) { + if (err || !postData || !postData.tid) { + return next(err); + } + db.sortedSetAdd('cid:' + cid + ':recent_tids', postData.timestamp, postData.tid, next); + }); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/09/22] Setting category recent tids - done'); + Upgrade.update(thisSchemaDate, next); + }); + }); + } else { + winston.info('[2016/09/22] Setting category recent tids - skipped!'); + next(); + } + }, + function (next) { + function upgradePosts(next) { + var batch = require('./batch'); + + batch.processSortedSet('posts:pid', function (ids, next) { + async.each(ids, function (id, next) { + console.log('processing pid ' + id); + async.waterfall([ + function (next) { + db.rename('pid:' + id + ':users_favourited', 'pid:' + id + ':users_bookmarked', next); + }, + function (next) { + db.getObjectField('post:' + id, 'reputation', next); + }, + function (reputation, next) { + if (parseInt(reputation, 10)) { + db.setObjectField('post:' + id, 'bookmarks', reputation, next); + } else { + next(); + } + }, + function (next) { + db.deleteObjectField('post:' + id, 'reputation', next); + } + ], next); + }, next); + }, {}, next); + } + + function upgradeUsers(next) { + var batch = require('./batch'); + + batch.processSortedSet('users:joindate', function (ids, next) { + async.each(ids, function (id, next) { + console.log('processing uid ' + id); + db.rename('uid:' + id + ':favourites', 'uid:' + id + ':bookmarks', next); + }, next); + }, {}, next); + } + + thisSchemaDate = Date.UTC(2016, 9, 8); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/10/8] favourite -> bookmark refactor'); + async.series([upgradePosts, upgradeUsers], function (err) { + if (err) { + return next(err); + } + winston.info('[2016/08/05] favourite- bookmark refactor done!'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/10/8] favourite -> bookmark refactor - skipped!'); + next(); + } } // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! - ], function(err) { + ], function (err) { if (!err) { if(updatesMade) { winston.info('[upgrade] Schema update complete!'); diff --git a/src/user.js b/src/user.js index 19c60fc1fd..7cc01c89f3 100644 --- a/src/user.js +++ b/src/user.js @@ -1,14 +1,16 @@ 'use strict'; -var async = require('async'), +var async = require('async'); +var _ = require('underscore'); - plugins = require('./plugins'), - db = require('./database'), - topics = require('./topics'), - privileges = require('./privileges'), - utils = require('../public/src/utils'); +var groups = require('./groups'); +var plugins = require('./plugins'); +var db = require('./database'); +var topics = require('./topics'); +var privileges = require('./privileges'); +var meta = require('./meta'); -(function(User) { +(function (User) { User.email = require('./user/email'); User.notifications = require('./user/notifications'); @@ -19,6 +21,7 @@ var async = require('async'), require('./user/auth')(User); require('./user/create')(User); require('./user/posts')(User); + require('./user/topics')(User); require('./user/categories')(User); require('./user/follow')(User); require('./user/profile')(User); @@ -31,10 +34,11 @@ var async = require('async'), require('./user/approval')(User); require('./user/invite')(User); require('./user/password')(User); + require('./user/info')(User); - User.updateLastOnlineTime = function(uid, callback) { - callback = callback || function() {}; - User.getUserFields(uid, ['status', 'lastonline'], function(err, userData) { + User.updateLastOnlineTime = function (uid, callback) { + callback = callback || function () {}; + db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) { var now = Date.now(); if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) { return callback(err); @@ -43,21 +47,21 @@ var async = require('async'), }); }; - User.updateOnlineUsers = function(uid, callback) { - callback = callback || function() {}; + User.updateOnlineUsers = function (uid, callback) { + callback = callback || function () {}; var now = Date.now(); async.waterfall([ - function(next) { + function (next) { db.sortedSetScore('users:online', uid, next); }, - function(userOnlineTime, next) { + function (userOnlineTime, next) { if (now - parseInt(userOnlineTime, 10) < 300000) { return callback(); } db.sortedSetAdd('users:online', now, uid, next); }, - function(next) { + function (next) { topics.pushUnreadCount(uid); plugins.fireHook('action:user.online', {uid: uid, timestamp: now}); next(); @@ -65,7 +69,7 @@ var async = require('async'), ], callback); }; - User.getUidsFromSet = function(set, start, stop, callback) { + User.getUidsFromSet = function (set, start, stop, callback) { if (set === 'users:online') { var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; var now = Date.now(); @@ -75,47 +79,45 @@ var async = require('async'), } }; - User.getUsersFromSet = function(set, uid, start, stop, callback) { + User.getUsersFromSet = function (set, uid, start, stop, callback) { async.waterfall([ - function(next) { + function (next) { User.getUidsFromSet(set, start, stop, next); }, - function(uids, next) { + function (uids, next) { User.getUsers(uids, uid, next); } ], callback); }; - User.getUsers = function(uids, uid, callback) { - var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline']; - + User.getUsersWithFields = function (uids, fields, uid, callback) { async.waterfall([ function (next) { plugins.fireHook('filter:users.addFields', {fields: fields}, next); }, function (data, next) { - data.fields = data.fields.filter(function(field, index, array) { + data.fields = data.fields.filter(function (field, index, array) { return array.indexOf(field) === index; }); async.parallel({ - userData: function(next) { + userData: function (next) { User.getUsersFields(uids, data.fields, next); }, - isAdmin: function(next) { + isAdmin: function (next) { User.isAdministrator(uids, next); } }, next); }, function (results, next) { - results.userData.forEach(function(user, index) { + results.userData.forEach(function (user, index) { if (user) { user.status = User.getStatus(user); - user.joindateISO = utils.toISOString(user.joindate); user.administrator = results.isAdmin[index]; user.banned = parseInt(user.banned, 10) === 1; + user.banned_until = parseInt(user['banned:expire'], 10) || 0; + user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned'; user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1; - user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO; } }); plugins.fireHook('filter:userlist.get', {users: results.userData, uid: uid}, next); @@ -126,25 +128,32 @@ var async = require('async'), ], callback); }; - User.getStatus = function(userData) { - var isOnline = Date.now() - parseInt(userData.lastonline, 10) < 300000; + User.getUsers = function (uids, uid, callback) { + var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags', + 'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline']; + + User.getUsersWithFields(uids, fields, uid, callback); + }; + + User.getStatus = function (userData) { + var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000; return isOnline ? (userData.status || 'online') : 'offline'; }; - User.isOnline = function(uid, callback) { + User.isOnline = function (uid, callback) { if (Array.isArray(uid)) { - db.sortedSetScores('users:online', uid, function(err, lastonline) { + db.sortedSetScores('users:online', uid, function (err, lastonline) { if (err) { return callback(err); } var now = Date.now(); - var isOnline = uid.map(function(uid, index) { + var isOnline = uid.map(function (uid, index) { return now - lastonline[index] < 300000; }); callback(null, isOnline); }); } else { - db.sortedSetScore('users:online', uid, function(err, lastonline) { + db.sortedSetScore('users:online', uid, function (err, lastonline) { if (err) { return callback(err); } @@ -155,41 +164,41 @@ var async = require('async'), }; - User.exists = function(uid, callback) { + User.exists = function (uid, callback) { db.isSortedSetMember('users:joindate', uid, callback); }; - User.existsBySlug = function(userslug, callback) { - User.getUidByUserslug(userslug, function(err, exists) { + User.existsBySlug = function (userslug, callback) { + User.getUidByUserslug(userslug, function (err, exists) { callback(err, !! exists); }); }; - User.getUidByUsername = function(username, callback) { + User.getUidByUsername = function (username, callback) { if (!username) { return callback(null, 0); } db.sortedSetScore('username:uid', username, callback); }; - User.getUidsByUsernames = function(usernames, callback) { + User.getUidsByUsernames = function (usernames, callback) { db.sortedSetScores('username:uid', usernames, callback); }; - User.getUidByUserslug = function(userslug, callback) { + User.getUidByUserslug = function (userslug, callback) { if (!userslug) { return callback(null, 0); } db.sortedSetScore('userslug:uid', userslug, callback); }; - User.getUsernamesByUids = function(uids, callback) { - User.getUsersFields(uids, ['username'], function(err, users) { + User.getUsernamesByUids = function (uids, callback) { + User.getUsersFields(uids, ['username'], function (err, users) { if (err) { return callback(err); } - users = users.map(function(user) { + users = users.map(function (user) { return user.username; }); @@ -197,23 +206,23 @@ var async = require('async'), }); }; - User.getUsernameByUserslug = function(slug, callback) { + User.getUsernameByUserslug = function (slug, callback) { async.waterfall([ - function(next) { + function (next) { User.getUidByUserslug(slug, next); }, - function(uid, next) { + function (uid, next) { User.getUserField(uid, 'username', next); } ], callback); }; - User.getUidByEmail = function(email, callback) { + User.getUidByEmail = function (email, callback) { db.sortedSetScore('email:uid', email.toLowerCase(), callback); }; - User.getUsernameByEmail = function(email, callback) { - db.sortedSetScore('email:uid', email.toLowerCase(), function(err, uid) { + User.getUsernameByEmail = function (email, callback) { + db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) { if (err) { return callback(err); } @@ -221,32 +230,38 @@ var async = require('async'), }); }; - User.isModerator = function(uid, cid, callback) { + User.isModerator = function (uid, cid, callback) { privileges.users.isModerator(uid, cid, callback); }; - User.isAdministrator = function(uid, callback) { + User.isModeratorOfAnyCategory = function (uid, callback) { + User.getModeratedCids(uid, function (err, cids) { + callback(err, Array.isArray(cids) ? !!cids.length : false); + }); + }; + + User.isAdministrator = function (uid, callback) { privileges.users.isAdministrator(uid, callback); }; - User.isGlobalModerator = function(uid, callback) { + User.isGlobalModerator = function (uid, callback) { privileges.users.isGlobalModerator(uid, callback); }; - User.isAdminOrGlobalMod = function(uid, callback) { + User.isAdminOrGlobalMod = function (uid, callback) { async.parallel({ isAdmin: async.apply(User.isAdministrator, uid), isGlobalMod: async.apply(User.isGlobalModerator, uid) - }, function(err, results) { + }, function (err, results) { callback(err, results ? (results.isAdmin || results.isGlobalMod) : false); }); }; - User.isAdminOrSelf = function(callerUid, uid, callback) { + User.isAdminOrSelf = function (callerUid, uid, callback) { if (parseInt(callerUid, 10) === parseInt(uid, 10)) { return callback(); } - User.isAdministrator(callerUid, function(err, isAdmin) { + User.isAdministrator(callerUid, function (err, isAdmin) { if (err || !isAdmin) { return callback(err || new Error('[[error:no-privileges]]')); } @@ -254,5 +269,99 @@ var async = require('async'), }); }; + User.getAdminsandGlobalMods = function (callback) { + async.parallel({ + admins: async.apply(groups.getMembers, 'administrators', 0, -1), + mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1) + }, function (err, results) { + if (err) { + return callback(err); + } + var uids = results.admins.concat(results.mods).filter(function (uid, index, array) { + return uid && array.indexOf(uid) === index; + }); + User.getUsersData(uids, callback); + }); + }; + + User.getAdminsandGlobalModsandModerators = function (callback) { + async.parallel([ + async.apply(groups.getMembers, 'administrators', 0, -1), + async.apply(groups.getMembers, 'Global Moderators', 0, -1), + async.apply(User.getModeratorUids) + ], function (err, results) { + if (err) { + return callback(err); + } + + User.getUsersData(_.union.apply(_, results), callback); + }); + }; + + User.getModeratorUids = function (callback) { + async.waterfall([ + async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + function (cids, next) { + var groupNames = cids.map(function (cid) { + return 'cid:' + cid + ':privileges:mods'; + }); + + groups.getMembersOfGroups(groupNames, function (err, memberSets) { + if (err) { + return next(err); + } + + next(null, _.union.apply(_, memberSets)); + }); + } + ], callback); + }; + + User.getModeratedCids = function (uid, callback) { + var cids; + async.waterfall([ + function (next) { + db.getSortedSetRange('categories:cid', 0, -1, next); + }, + function (_cids, next) { + cids = _cids; + User.isModerator(uid, cids, next); + }, + function (isMods, next) { + cids = cids.filter(function (cid, index) { + return cid && isMods[index]; + }); + next(null, cids); + } + ], callback); + }; + + User.addInterstitials = function (callback) { + plugins.registerHook('core', { + hook: 'filter:register.interstitial', + method: function (data, callback) { + if (meta.config.termsOfUse && !data.userData.acceptTos) { + data.interstitials.push({ + template: 'partials/acceptTos', + data: { + termsOfUse: meta.config.termsOfUse + }, + callback: function (userData, formData, next) { + if (formData['agree-terms'] === 'on') { + userData.acceptTos = true; + } + + next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); + } + }); + } + + callback(null, data); + } + }); + + callback(); + }; + }(exports)); diff --git a/src/user/admin.js b/src/user/admin.js index 004e048c93..6a1eb5f458 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -3,11 +3,13 @@ var async = require('async'); var db = require('../database'); +var posts = require('../posts'); var plugins = require('../plugins'); +var winston = require('winston'); -module.exports = function(User) { +module.exports = function (User) { - User.logIP = function(uid, ip) { + User.logIP = function (uid, ip) { var now = Date.now(); db.sortedSetAdd('uid:' + uid + ':ip', now, ip || 'Unknown'); if (ip) { @@ -15,19 +17,18 @@ module.exports = function(User) { } }; - User.getIPs = function(uid, stop, callback) { - db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function(err, ips) { + User.getIPs = function (uid, stop, callback) { + db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function (err, ips) { if (err) { return callback(err); } - callback(null, ips.map(function(ip) { - return {ip:ip}; - })); + callback(null, ips); }); }; - User.getUsersCSV = function(callback) { + User.getUsersCSV = function (callback) { + winston.info('[user/getUsersCSV] Compiling User CSV data'); var csvContent = ''; async.waterfall([ @@ -35,13 +36,13 @@ module.exports = function(User) { db.getSortedSetRangeWithScores('username:uid', 0, -1, next); }, function (users, next) { - var uids = users.map(function(user) { + var uids = users.map(function (user) { return user.score; }); User.getUsersFields(uids, ['uid', 'email', 'username'], next); }, function (usersData, next) { - usersData.forEach(function(user) { + usersData.forEach(function (user) { if (user) { csvContent += user.email + ',' + user.username + ',' + user.uid + '\n'; } @@ -52,44 +53,103 @@ module.exports = function(User) { ], callback); }; - User.ban = function(uid, callback) { + User.ban = function (uid, until, reason, callback) { + // "until" (optional) is unix timestamp in milliseconds + // "reason" (optional) is a string + if (!callback && typeof until === 'function') { + callback = until; + until = 0; + reason = ''; + } else if (!callback && typeof reason === 'function') { + callback = reason; + reason = ''; + } + + var now = Date.now(); + + until = parseInt(until, 10); + if (isNaN(until)) { + return callback(new Error('[[error:ban-expiry-missing]]')); + } + + var tasks = [ + async.apply(User.setUserField, uid, 'banned', 1), + async.apply(db.sortedSetAdd, 'users:banned', now, uid), + async.apply(db.sortedSetAdd, 'uid:' + uid + ':bans', now, until) + ]; + + if (until > 0 && now < until) { + tasks.push(async.apply(db.sortedSetAdd, 'users:banned:expire', until, uid)); + tasks.push(async.apply(User.setUserField, uid, 'banned:expire', until)); + } else { + until = 0; + } + + if (reason) { + tasks.push(async.apply(db.sortedSetAdd, 'banned:' + uid + ':reasons', now, reason)); + } + + async.series(tasks, function (err) { + if (err) { + return callback(err); + } + + plugins.fireHook('action:user.banned', { + uid: uid, + until: until > 0 ? until : undefined + }); + callback(); + }); + }; + + User.unban = function (uid, callback) { async.waterfall([ function (next) { - User.setUserField(uid, 'banned', 1, next); + User.setUserFields(uid, {banned: 0, 'banned:expire': 0}, next); }, function (next) { - db.sortedSetAdd('users:banned', Date.now(), uid, next); + db.sortedSetsRemove(['users:banned', 'users:banned:expire'], uid, next); }, function (next) { - plugins.fireHook('action:user.banned', {uid: uid}); + plugins.fireHook('action:user.unbanned', {uid: uid}); next(); } ], callback); }; - User.unban = function(uid, callback) { - db.delete('uid:' + uid + ':flagged_by'); + User.isBanned = function (uid, callback) { async.waterfall([ - function (next) { - User.setUserField(uid, 'banned', 0, next); - }, - function (next) { - db.sortedSetRemove('users:banned', uid, next); - }, - function (next) { - plugins.fireHook('action:user.unbanned', {uid: uid}); - next(); + async.apply(User.getUserFields, uid, ['banned', 'banned:expire']), + function (userData, next) { + var banned = parseInt(userData.banned, 10) === 1; + if (!banned) { + return next(null, banned); + } + + // If they are banned, see if the ban has expired + var stillBanned = !userData['banned:expire'] || Date.now() < userData['banned:expire']; + + if (stillBanned) { + return next(null, true); + } + async.parallel([ + async.apply(db.sortedSetRemove.bind(db), 'users:banned:expire', uid), + async.apply(db.sortedSetRemove.bind(db), 'users:banned', uid), + async.apply(User.setUserFields, uid, {banned:0, 'banned:expire': 0}) + ], function (err) { + next(err, false); + }); } ], callback); }; - User.resetFlags = function(uids, callback) { + User.resetFlags = function (uids, callback) { if (!Array.isArray(uids) || !uids.length) { return callback(); } - var keys = uids.map(function(uid) { - return 'uid:' + uid + ':flagged_by'; - }); - db.deleteAll(keys, callback); + + async.eachSeries(uids, function (uid, next) { + posts.dismissUserFlags(uid, next); + }, callback); }; }; diff --git a/src/user/approval.js b/src/user/approval.js index 94e0f097e5..ac54bcfc67 100644 --- a/src/user/approval.js +++ b/src/user/approval.js @@ -11,33 +11,35 @@ var notifications = require('../notifications'); var groups = require('../groups'); var translator = require('../../public/src/modules/translator'); var utils = require('../../public/src/utils'); +var plugins = require('../plugins'); +module.exports = function (User) { -module.exports = function(User) { - - User.addToApprovalQueue = function(userData, callback) { + User.addToApprovalQueue = function (userData, callback) { userData.userslug = utils.slugify(userData.username); async.waterfall([ - function(next) { + function (next) { User.isDataValid(userData, next); }, - function(next) { + function (next) { User.hashPassword(userData.password, next); }, - function(hashedPassword, next) { + function (hashedPassword, next) { var data = { username: userData.username, email: userData.email, ip: userData.ip, hashedPassword: hashedPassword }; - - db.setObject('registration:queue:name:' + userData.username, data, next); + plugins.fireHook('filter:user.addToApprovalQueue', {data: data, userData: userData}, next); }, - function(next) { + function (results, next) { + db.setObject('registration:queue:name:' + userData.username, results.data, next); + }, + function (next) { db.sortedSetAdd('registration:queue', Date.now(), userData.username, next); }, - function(next) { + function (next) { sendNotificationToAdmins(userData.username, next); } ], callback); @@ -49,7 +51,7 @@ module.exports = function(User) { nid: 'new_register:' + username, path: '/admin/manage/registration', mergeId: 'new_register' - }, function(err, notification) { + }, function (err, notification) { if (err || !notification) { return callback(err); } @@ -58,27 +60,27 @@ module.exports = function(User) { }); } - User.acceptRegistration = function(username, callback) { + User.acceptRegistration = function (username, callback) { var uid; var userData; async.waterfall([ - function(next) { + function (next) { db.getObject('registration:queue:name:' + username, next); }, - function(_userData, next) { + function (_userData, next) { if (!_userData) { return callback(new Error('[[error:invalid-data]]')); } userData = _userData; User.create(userData, next); }, - function(_uid, next) { + function (_uid, next) { uid = _uid; User.setUserField(uid, 'password', userData.hashedPassword, next); }, - function(next) { + function (next) { var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; - translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function(subject) { + translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function (subject) { var data = { site_title: title, username: username, @@ -90,10 +92,10 @@ module.exports = function(User) { emailer.send('registration_accepted', uid, data, next); }); }, - function(next) { + function (next) { removeFromQueue(username, next); }, - function(next) { + function (next) { markNotificationRead(username, next); } ], callback); @@ -106,14 +108,14 @@ module.exports = function(User) { groups.getMembers('administrators', 0, -1, next); }, function (uids, next) { - async.each(uids, function(uid, next) { + async.each(uids, function (uid, next) { notifications.markRead(nid, uid, next); }, next); } ], callback); } - User.rejectRegistration = function(username, callback) { + User.rejectRegistration = function (username, callback) { async.waterfall([ function (next) { removeFromQueue(username, next); @@ -128,26 +130,26 @@ module.exports = function(User) { async.parallel([ async.apply(db.sortedSetRemove, 'registration:queue', username), async.apply(db.delete, 'registration:queue:name:' + username) - ], function(err, results) { + ], function (err) { callback(err); }); } - User.getRegistrationQueue = function(start, stop, callback) { + User.getRegistrationQueue = function (start, stop, callback) { var data; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRangeWithScores('registration:queue', start, stop, next); }, - function(_data, next) { + function (_data, next) { data = _data; - var keys = data.filter(Boolean).map(function(user) { + var keys = data.filter(Boolean).map(function (user) { return 'registration:queue:name:' + user.value; }); db.getObjects(keys, next); }, - function(users, next) { - users = users.map(function(user, index) { + function (users, next) { + users = users.map(function (user, index) { if (!user) { return null; } @@ -158,7 +160,7 @@ module.exports = function(User) { return user; }).filter(Boolean); - async.map(users, function(user, next) { + async.map(users, function (user, next) { if (!user) { return next(null, user); } @@ -166,28 +168,52 @@ module.exports = function(User) { // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 user.ip = user.ip.replace('::ffff:', ''); - request({ - method: 'get', - url: 'http://api.stopforumspam.org/api' + - '?ip=' + encodeURIComponent(user.ip) + - '&email=' + encodeURIComponent(user.email) + - '&username=' + encodeURIComponent(user.username) + - '&f=json', - json: true - }, function (err, response, body) { - if (err) { - return next(null, user); + async.parallel([ + function (next) { + User.getUidsFromSet('ip:' + user.ip + ':uid', 0, -1, function (err, uids) { + if (err) { + return next(err); + } + + User.getUsersFields(uids, ['uid', 'username', 'picture'], function (err, ipMatch) { + user.ipMatch = ipMatch; + next(err); + }); + }); + }, + function (next) { + request({ + method: 'get', + url: 'http://api.stopforumspam.org/api' + + '?ip=' + encodeURIComponent(user.ip) + + '&email=' + encodeURIComponent(user.email) + + '&username=' + encodeURIComponent(user.username) + + '&f=json', + json: true + }, function (err, response, body) { + if (err) { + return next(); + } + if (response.statusCode === 200 && body) { + user.spamData = body; + user.usernameSpam = body.username ? (body.username.frequency > 0 || body.username.appears > 0) : true; + user.emailSpam = body.email ? (body.email.frequency > 0 || body.email.appears > 0) : true; + user.ipSpam = body.ip ? (body.ip.frequency > 0 || body.ip.appears > 0) : true; + } + + next(); + }); } - if (response.statusCode === 200) { - user.spamData = body; - user.usernameSpam = body.username.frequency > 0 || body.username.appears > 0; - user.emailSpam = body.email.frequency > 0 || body.email.appears > 0; - user.ipSpam = body.ip.frequency > 0 || body.ip.appears > 0; - } - - next(null, user); + ], function (err) { + next(err, user); }); }, next); + }, + function (users, next) { + plugins.fireHook('filter:user.getRegistrationQueue', {users: users}, next); + }, + function (results, next) { + next(null, results.users); } ], callback); }; diff --git a/src/user/auth.js b/src/user/auth.js index 99bdfb8c94..a60f59fea4 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -1,16 +1,16 @@ 'use strict'; -var async = require('async'), - winston = require('winston'), - db = require('../database'), - meta = require('../meta'), - events = require('../events'); +var async = require('async'); +var winston = require('winston'); +var db = require('../database'); +var meta = require('../meta'); +var events = require('../events'); -module.exports = function(User) { +module.exports = function (User) { User.auth = {}; - User.auth.logAttempt = function(uid, ip, callback) { - db.exists('lockout:' + uid, function(err, exists) { + User.auth.logAttempt = function (uid, ip, callback) { + db.exists('lockout:' + uid, function (err, exists) { if (err) { return callback(err); } @@ -19,14 +19,14 @@ module.exports = function(User) { return callback(new Error('[[error:account-locked]]')); } - db.increment('loginAttempts:' + uid, function(err, attempts) { + db.increment('loginAttempts:' + uid, function (err, attempts) { if (err) { return callback(err); } if ((meta.config.loginAttempts || 5) < attempts) { // Lock out the account - db.set('lockout:' + uid, '', function(err) { + db.set('lockout:' + uid, '', function (err) { if (err) { return callback(err); } @@ -49,18 +49,18 @@ module.exports = function(User) { }); }; - User.auth.clearLoginAttempts = function(uid) { + User.auth.clearLoginAttempts = function (uid) { db.delete('loginAttempts:' + uid); }; - User.auth.resetLockout = function(uid, callback) { + User.auth.resetLockout = function (uid, callback) { async.parallel([ async.apply(db.delete, 'loginAttempts:' + uid), async.apply(db.delete, 'lockout:' + uid) ], callback); }; - User.auth.getSessions = function(uid, curSessionId, callback) { + User.auth.getSessions = function (uid, curSessionId, callback) { var _sids; // curSessionId is optional @@ -76,7 +76,7 @@ module.exports = function(User) { async.map(sids, db.sessionStore.get.bind(db.sessionStore), next); }, function (sessions, next) { - sessions.forEach(function(sessionObj, idx) { + sessions.forEach(function (sessionObj, idx) { if (sessionObj && sessionObj.meta) { sessionObj.meta.current = curSessionId === _sids[idx]; } @@ -86,7 +86,7 @@ module.exports = function(User) { var expiredSids = [], expired; - sessions = sessions.filter(function(sessionObj, idx) { + sessions = sessions.filter(function (sessionObj, idx) { expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || !sessionObj.passport.hasOwnProperty('user') || parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); @@ -98,29 +98,29 @@ module.exports = function(User) { return !expired; }); - async.each(expiredSids, function(sid, next) { + async.each(expiredSids, function (sid, next) { User.auth.revokeSession(sid, uid, next); - }, function(err) { - next(null, sessions); + }, function (err) { + next(err, sessions); }); } ], function (err, sessions) { - callback(err, sessions ? sessions.map(function(sessObj) { + callback(err, sessions ? sessions.map(function (sessObj) { sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); return sessObj.meta; }) : undefined); }); }; - User.auth.addSession = function(uid, sessionId, callback) { - callback = callback || function() {}; + User.auth.addSession = function (uid, sessionId, callback) { + callback = callback || function () {}; db.sortedSetAdd('uid:' + uid + ':sessions', Date.now(), sessionId, callback); }; - User.auth.revokeSession = function(sessionId, uid, callback) { + User.auth.revokeSession = function (sessionId, uid, callback) { winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid); - db.sessionStore.get(sessionId, function(err, sessionObj) { + db.sessionStore.get(sessionId, function (err, sessionObj) { if (err) { return callback(err); } @@ -138,11 +138,11 @@ module.exports = function(User) { }); }; - User.auth.revokeAllSessions = function(uid, callback) { + User.auth.revokeAllSessions = function (uid, callback) { async.waterfall([ async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1), function (sids, next) { - async.each(sids, function(sid, next) { + async.each(sids, function (sid, next) { User.auth.revokeSession(sid, uid, next); }, next); } diff --git a/src/user/categories.js b/src/user/categories.js index 612b5ec14e..fee8fc8bb0 100644 --- a/src/user/categories.js +++ b/src/user/categories.js @@ -5,33 +5,33 @@ var async = require('async'); var db = require('../database'); var categories = require('../categories'); -module.exports = function(User) { +module.exports = function (User) { - User.getIgnoredCategories = function(uid, callback) { + User.getIgnoredCategories = function (uid, callback) { db.getSortedSetRange('uid:' + uid + ':ignored:cids', 0, -1, callback); }; - User.getWatchedCategories = function(uid, callback) { + User.getWatchedCategories = function (uid, callback) { async.parallel({ - ignored: function(next) { + ignored: function (next) { User.getIgnoredCategories(uid, next); }, - all: function(next) { + all: function (next) { db.getSortedSetRange('categories:cid', 0, -1, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } - var watched = results.all.filter(function(cid) { + var watched = results.all.filter(function (cid) { return cid && results.ignored.indexOf(cid) === -1; }); callback(null, watched); }); }; - User.ignoreCategory = function(uid, cid, callback) { + User.ignoreCategory = function (uid, cid, callback) { if (!uid) { return callback(); } @@ -45,11 +45,14 @@ module.exports = function(User) { return next(new Error('[[error:no-category]]')); } db.sortedSetAdd('uid:' + uid + ':ignored:cids', Date.now(), cid, next); + }, + function (next) { + db.sortedSetAdd('cid:' + cid + ':ignorers', Date.now(), uid, next); } ], callback); }; - User.watchCategory = function(uid, cid, callback) { + User.watchCategory = function (uid, cid, callback) { if (!uid) { return callback(); } @@ -63,6 +66,9 @@ module.exports = function(User) { return next(new Error('[[error:no-category]]')); } db.sortedSetRemove('uid:' + uid + ':ignored:cids', cid, next); + }, + function (next) { + db.sortedSetRemove('cid:' + cid + ':ignorers', uid, next); } ], callback); }; diff --git a/src/user/create.js b/src/user/create.js index ae69f1ad5d..46508d1d0b 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -8,18 +8,16 @@ var plugins = require('../plugins'); var groups = require('../groups'); var meta = require('../meta'); +module.exports = function (User) { -module.exports = function(User) { - - User.create = function(data, callback) { - + User.create = function (data, callback) { data.username = data.username.trim(); data.userslug = utils.slugify(data.username); if (data.email !== undefined) { - data.email = validator.escape(data.email.trim()); + data.email = validator.escape(String(data.email).trim()); } - User.isDataValid(data, function(err) { + User.isDataValid(data, function (err) { if (err) { return callback(err); } @@ -28,7 +26,7 @@ module.exports = function(User) { var userData = { 'username': data.username, 'userslug': data.userslug, - 'email': data.email, + 'email': data.email || '', 'joindate': timestamp, 'lastonline': timestamp, 'picture': '', @@ -48,13 +46,13 @@ module.exports = function(User) { }; async.parallel({ - renamedUsername: function(next) { + renamedUsername: function (next) { renameUsername(userData, next); }, - userData: function(next) { + userData: function (next) { plugins.fireHook('filter:user.create', {user: userData, data: data}, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -67,44 +65,44 @@ module.exports = function(User) { } async.waterfall([ - function(next) { + function (next) { db.incrObjectField('global', 'nextUid', next); }, - function(uid, next) { + function (uid, next) { userData.uid = uid; db.setObject('user:' + uid, userData, next); }, - function(next) { + function (next) { async.parallel([ - function(next) { + function (next) { db.incrObjectField('global', 'userCount', next); }, - function(next) { + function (next) { db.sortedSetAdd('username:uid', userData.uid, userData.username, next); }, - function(next) { + function (next) { db.sortedSetAdd('username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid, next); }, - function(next) { + function (next) { db.sortedSetAdd('userslug:uid', userData.uid, userData.userslug, next); }, - function(next) { + function (next) { var sets = ['users:joindate', 'users:online']; if (parseInt(userData.uid) !== 1) { sets.push('users:notvalidated'); } db.sortedSetsAdd(sets, timestamp, userData.uid, next); }, - function(next) { + function (next) { db.sortedSetsAdd(['users:postcount', 'users:reputation'], 0, userData.uid, next); }, - function(next) { + function (next) { groups.join('registered-users', userData.uid, next); }, - function(next) { + function (next) { User.notifications.sendWelcomeNotification(userData.uid, next); }, - function(next) { + function (next) { if (userData.email) { async.parallel([ async.apply(db.sortedSetAdd, 'email:uid', userData.uid, userData.email.toLowerCase()), @@ -118,12 +116,12 @@ module.exports = function(User) { next(); } }, - function(next) { + function (next) { if (!data.password) { return next(); } - User.hashPassword(data.password, function(err, hash) { + User.hashPassword(data.password, function (err, hash) { if (err) { return next(err); } @@ -133,10 +131,13 @@ module.exports = function(User) { async.apply(User.reset.updateExpiry, userData.uid) ], next); }); + }, + function (next) { + User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq, next); } ], next); }, - function(results, next) { + function (results, next) { if (userNameChanged) { User.notifications.sendNameChangeNotification(userData.uid, userData.username); } @@ -148,28 +149,28 @@ module.exports = function(User) { }); }; - User.isDataValid = function(userData, callback) { + User.isDataValid = function (userData, callback) { async.parallel({ - emailValid: function(next) { + emailValid: function (next) { if (userData.email) { next(!utils.isEmailValid(userData.email) ? new Error('[[error:invalid-email]]') : null); } else { next(); } }, - userNameValid: function(next) { + userNameValid: function (next) { next((!utils.isUserNameValid(userData.username) || !userData.userslug) ? new Error('[[error:invalid-username, ' + userData.username + ']]') : null); }, - passwordValid: function(next) { + passwordValid: function (next) { if (userData.password) { User.isPasswordValid(userData.password, next); } else { next(); } }, - emailAvailable: function(next) { + emailAvailable: function (next) { if (userData.email) { - User.email.available(userData.email, function(err, available) { + User.email.available(userData.email, function (err, available) { if (err) { return next(err); } @@ -179,12 +180,12 @@ module.exports = function(User) { next(); } } - }, function(err) { + }, function (err) { callback(err); }); }; - User.isPasswordValid = function(password, callback) { + User.isPasswordValid = function (password, callback) { if (!password || !utils.isPasswordValid(password)) { return callback(new Error('[[error:invalid-password]]')); } @@ -201,15 +202,15 @@ module.exports = function(User) { }; function renameUsername(userData, callback) { - meta.userOrGroupExists(userData.userslug, function(err, exists) { + meta.userOrGroupExists(userData.userslug, function (err, exists) { if (err || !exists) { return callback(err); } var newUsername = ''; - async.forever(function(next) { + async.forever(function (next) { newUsername = userData.username + (Math.floor(Math.random() * 255) + 1); - User.existsBySlug(newUsername, function(err, exists) { + User.existsBySlug(newUsername, function (err, exists) { if (err) { return callback(err); } @@ -219,7 +220,7 @@ module.exports = function(User) { next(); } }); - }, function(username) { + }, function (username) { callback(null, username); }); }); diff --git a/src/user/data.js b/src/user/data.js index 66fedbefcd..cbaf066ded 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -6,25 +6,26 @@ var winston = require('winston'); var db = require('../database'); var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); -module.exports = function(User) { +module.exports = function (User) { var iconBackgrounds = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722', '#795548', '#607d8b']; - User.getUserField = function(uid, field, callback) { - User.getUserFields(uid, [field], function(err, user) { + User.getUserField = function (uid, field, callback) { + User.getUserFields(uid, [field], function (err, user) { callback(err, user ? user[field] : null); }); }; - User.getUserFields = function(uid, fields, callback) { - User.getUsersFields([uid], fields, function(err, users) { + User.getUserFields = function (uid, fields, callback) { + User.getUsersFields([uid], fields, function (err, users) { callback(err, users ? users[0] : null); }); }; - User.getUsersFields = function(uids, fields, callback) { + User.getUsersFields = function (uids, fields, callback) { var fieldsToRemove = []; function addField(field) { if (fields.indexOf(field) === -1) { @@ -37,7 +38,7 @@ module.exports = function(User) { return callback(null, []); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'user:' + uid; }); @@ -50,7 +51,11 @@ module.exports = function(User) { addField('uploadedpicture'); } - db.getObjectsFields(keys, fields, function(err, users) { + if (fields.indexOf('status') !== -1) { + addField('lastonline'); + } + + db.getObjectsFields(keys, fields, function (err, users) { if (err) { return callback(err); } @@ -59,27 +64,27 @@ module.exports = function(User) { }); }; - User.getMultipleUserFields = function(uids, fields, callback) { + User.getMultipleUserFields = function (uids, fields, callback) { winston.warn('[deprecated] User.getMultipleUserFields is deprecated please use User.getUsersFields'); User.getUsersFields(uids, fields, callback); }; - User.getUserData = function(uid, callback) { - User.getUsersData([uid], function(err, users) { + User.getUserData = function (uid, callback) { + User.getUsersData([uid], function (err, users) { callback(err, users ? users[0] : null); }); }; - User.getUsersData = function(uids, callback) { + User.getUsersData = function (uids, callback) { if (!Array.isArray(uids) || !uids.length) { return callback(null, []); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'user:' + uid; }); - db.getObjects(keys, function(err, users) { + db.getObjects(keys, function (err, users) { if (err) { return callback(err); } @@ -89,12 +94,14 @@ module.exports = function(User) { }; function modifyUserData(users, fieldsToRemove, callback) { - users.forEach(function(user) { + users.forEach(function (user) { if (!user) { return; } - user.username = validator.escape(user.username ? user.username.toString() : ''); + if (user.hasOwnProperty('username')) { + user.username = validator.escape(user.username ? user.username.toString() : ''); + } if (user.password) { user.password = undefined; @@ -115,25 +122,37 @@ module.exports = function(User) { user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : nconf.get('relative_path') + user.uploadedpicture; } - for(var i=0; i<fieldsToRemove.length; ++i) { + if (user.hasOwnProperty('status') && parseInt(user.lastonline, 10)) { + user.status = User.getStatus(user); + } + + for(var i = 0; i < fieldsToRemove.length; ++i) { user[fieldsToRemove[i]] = undefined; } // User Icons if (user.hasOwnProperty('picture') && user.username && parseInt(user.uid, 10)) { user['icon:text'] = (user.username[0] || '').toUpperCase(); - user['icon:bgColor'] = iconBackgrounds[Array.prototype.reduce.call(user.username, function(cur, next) { + user['icon:bgColor'] = iconBackgrounds[Array.prototype.reduce.call(user.username, function (cur, next) { return cur + next.charCodeAt(); }, 0) % iconBackgrounds.length]; } + + if (user.hasOwnProperty('joindate')) { + user.joindateISO = utils.toISOString(user.joindate); + } + + if (user.hasOwnProperty('lastonline')) { + user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO; + } }); plugins.fireHook('filter:users.get', users, callback); } - User.setUserField = function(uid, field, value, callback) { - callback = callback || function() {}; - db.setObjectField('user:' + uid, field, value, function(err) { + User.setUserField = function (uid, field, value, callback) { + callback = callback || function () {}; + db.setObjectField('user:' + uid, field, value, function (err) { if (err) { return callback(err); } @@ -142,9 +161,9 @@ module.exports = function(User) { }); }; - User.setUserFields = function(uid, data, callback) { - callback = callback || function() {}; - db.setObject('user:' + uid, data, function(err) { + User.setUserFields = function (uid, data, callback) { + callback = callback || function () {}; + db.setObject('user:' + uid, data, function (err) { if (err) { return callback(err); } @@ -157,9 +176,9 @@ module.exports = function(User) { }); }; - User.incrementUserFieldBy = function(uid, field, value, callback) { - callback = callback || function() {}; - db.incrObjectFieldBy('user:' + uid, field, value, function(err, value) { + User.incrementUserFieldBy = function (uid, field, value, callback) { + callback = callback || function () {}; + db.incrObjectFieldBy('user:' + uid, field, value, function (err, value) { if (err) { return callback(err); } @@ -169,9 +188,9 @@ module.exports = function(User) { }); }; - User.decrementUserFieldBy = function(uid, field, value, callback) { - callback = callback || function() {}; - db.incrObjectFieldBy('user:' + uid, field, -value, function(err, value) { + User.decrementUserFieldBy = function (uid, field, value, callback) { + callback = callback || function () {}; + db.incrObjectFieldBy('user:' + uid, field, -value, function (err, value) { if (err) { return callback(err); } @@ -181,4 +200,4 @@ module.exports = function(User) { }); }; -}; \ No newline at end of file +}; diff --git a/src/user/delete.js b/src/user/delete.js index 3ab5176725..e03333d7c1 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -1,50 +1,50 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - posts = require('../posts'), - topics = require('../topics'), - favourites = require('../favourites'), - groups = require('../groups'), - plugins = require('../plugins'), - batch = require('../batch'); - -module.exports = function(User) { - - User.delete = function(callerUid, uid, callback) { +var async = require('async'); + +var db = require('../database'); +var posts = require('../posts'); +var topics = require('../topics'); +var groups = require('../groups'); +var plugins = require('../plugins'); +var batch = require('../batch'); + +module.exports = function (User) { + + User.delete = function (callerUid, uid, callback) { if (!parseInt(uid, 10)) { return callback(new Error('[[error:invalid-uid]]')); } async.waterfall([ - function(next) { + function (next) { deletePosts(callerUid, uid, next); }, - function(next) { + function (next) { deleteTopics(callerUid, uid, next); }, - function(next) { + function (next) { User.deleteAccount(uid, next); } ], callback); }; function deletePosts(callerUid, uid, callback) { - batch.processSortedSet('uid:' + uid + ':posts', function(ids, next) { - async.eachSeries(ids, function(pid, netx) { + batch.processSortedSet('uid:' + uid + ':posts', function (ids, next) { + async.eachSeries(ids, function (pid, next) { posts.purge(pid, callerUid, next); }, next); }, {alwaysStartAt: 0}, callback); } function deleteTopics(callerUid, uid, callback) { - batch.processSortedSet('uid:' + uid + ':topics', function(ids, next) { - async.eachSeries(ids, function(tid, next) { + batch.processSortedSet('uid:' + uid + ':topics', function (ids, next) { + async.eachSeries(ids, function (tid, next) { topics.purge(tid, callerUid, next); }, next); }, {alwaysStartAt: 0}, callback); } - User.deleteAccount = function(uid, callback) { + User.deleteAccount = function (uid, callback) { var userData; async.waterfall([ function (next) { @@ -65,19 +65,19 @@ module.exports = function(User) { }, function (next) { async.parallel([ - function(next) { + function (next) { db.sortedSetRemove('username:uid', userData.username, next); }, - function(next) { + function (next) { db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid, next); }, - function(next) { + function (next) { db.sortedSetRemove('userslug:uid', userData.userslug, next); }, - function(next) { + function (next) { db.sortedSetRemove('fullname:uid', userData.fullname, next); }, - function(next) { + function (next) { if (userData.email) { async.parallel([ async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()), @@ -87,23 +87,30 @@ module.exports = function(User) { next(); } }, - function(next) { + function (next) { db.sortedSetsRemove([ 'users:joindate', 'users:postcount', 'users:reputation', 'users:banned', 'users:online', - 'users:notvalidated' + 'users:notvalidated', + 'digest:day:uids', + 'digest:week:uids', + 'digest:month:uids' ], uid, next); }, - function(next) { + function (next) { db.decrObjectField('global', 'userCount', next); }, - function(next) { + function (next) { var keys = [ - 'uid:' + uid + ':notifications:read', 'uid:' + uid + ':notifications:unread', - 'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings', + 'uid:' + uid + ':notifications:read', + 'uid:' + uid + ':notifications:unread', + 'uid:' + uid + ':bookmarks', + 'uid:' + uid + ':followed_tids', + 'uid:' + uid + ':ignored_tids', + 'user:' + uid + ':settings', 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', 'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread', @@ -113,18 +120,14 @@ module.exports = function(User) { ]; db.deleteAll(keys, next); }, - function(next) { + function (next) { deleteUserIps(uid, next); }, - function(next) { + function (next) { deleteUserFromFollowers(uid, next); }, - function(next) { + function (next) { groups.leaveAllGroups(uid, next); - }, - function(next) { - // Deprecated as of v0.7.4, remove in v1.0.0 - plugins.fireHook('filter:user.delete', uid, next); } ], next); }, @@ -143,15 +146,15 @@ module.exports = function(User) { }, next); }, function (pids, next) { - pids = pids.upvotedPids.concat(pids.downvotedPids).filter(function(pid, index, array) { + pids = pids.upvotedPids.concat(pids.downvotedPids).filter(function (pid, index, array) { return pid && array.indexOf(pid) === index; }); - async.eachSeries(pids, function(pid, next) { - favourites.unvote(pid, uid, next); + async.eachSeries(pids, function (pid, next) { + posts.unvote(pid, uid, next); }, next); } - ], function(err) { + ], function (err) { callback(err); }); } @@ -162,10 +165,10 @@ module.exports = function(User) { db.getSortedSetRange('uid:' + uid + ':chat:rooms', 0, -1, next); }, function (roomIds, next) { - var userKeys = roomIds.map(function(roomId) { + var userKeys = roomIds.map(function (roomId) { return 'uid:' + uid + ':chat:room:' + roomId + ':mids'; }); - var roomKeys = roomIds.map(function(roomId) { + var roomKeys = roomIds.map(function (roomId) { return 'chat:room:' + roomId + ':uids'; }); @@ -174,7 +177,7 @@ module.exports = function(User) { async.apply(db.deleteAll, userKeys) ], next); } - ], function(err) { + ], function (err) { callback(err); }); } @@ -185,7 +188,7 @@ module.exports = function(User) { db.getSortedSetRange('uid:' + uid + ':ip', 0, -1, next); }, function (ips, next) { - var keys = ips.map(function(ip) { + var keys = ips.map(function (ip) { return 'ip:' + ip + ':uid'; }); db.sortedSetsRemove(keys, uid, next); @@ -200,10 +203,10 @@ module.exports = function(User) { async.parallel({ followers: async.apply(db.getSortedSetRange, 'followers:' + uid, 0, -1), following: async.apply(db.getSortedSetRange, 'following:' + uid, 0, -1) - }, function(err, results) { + }, function (err, results) { function updateCount(uids, name, fieldName, next) { - async.each(uids, function(uid, next) { - db.sortedSetCard(name + uid, function(err, count) { + async.each(uids, function (uid, next) { + db.sortedSetCard(name + uid, function (err, count) { if (err) { return next(err); } @@ -217,11 +220,11 @@ module.exports = function(User) { return callback(err); } - var followingSets = results.followers.map(function(uid) { + var followingSets = results.followers.map(function (uid) { return 'following:' + uid; }); - var followerSets = results.following.map(function(uid) { + var followerSets = results.following.map(function (uid) { return 'followers:' + uid; }); diff --git a/src/user/digest.js b/src/user/digest.js index 81b6ea0220..2ea1401c2c 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -12,8 +12,8 @@ var plugins = require('../plugins'); var emailer = require('../emailer'); var utils = require('../../public/src/utils'); -(function(Digest) { - Digest.execute = function(interval) { +(function (Digest) { + Digest.execute = function (interval) { var digestsDisabled = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1; if (digestsDisabled) { return winston.verbose('[user/jobs] Did not send digests (' + interval + ') because subscription system is disabled.'); @@ -27,13 +27,13 @@ var utils = require('../../public/src/utils'); async.parallel({ topics: async.apply(topics.getLatestTopics, 0, 0, 9, interval), subscribers: async.apply(Digest.getSubscribers, interval) - }, function(err, data) { + }, function (err, data) { if (err) { return winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); } // Fix relative paths in topic data - data.topics.topics = data.topics.topics.map(function(topicObj) { + data.topics.topics = data.topics.topics.map(function (topicObj) { var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; if (user && user.picture && utils.isRelativeUrl(user.picture)) { user.picture = nconf.get('base_url') + user.picture; @@ -45,7 +45,7 @@ var utils = require('../../public/src/utils'); data.interval = interval; if (data.subscribers.length) { - Digest.send(data, function(err) { + Digest.send(data, function (err) { if (err) { winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); } else { @@ -58,28 +58,32 @@ var utils = require('../../public/src/utils'); }); }; - Digest.getSubscribers = function(interval, callback) { - db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, function(err, subscribers) { + Digest.getSubscribers = function (interval, callback) { + db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, function (err, subscribers) { + if (err) { + return callback(err); + } + plugins.fireHook('filter:digest.subscribers', { interval: interval, subscribers: subscribers - }, function(err, returnData) { + }, function (err, returnData) { callback(err, returnData.subscribers); }); }); }; - Digest.send = function(data, callback) { + Digest.send = function (data, callback) { var now = new Date(); - user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], function(err, users) { + user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], function (err, users) { if (err) { winston.error('[user/jobs] Could not send digests (' + data.interval + '): ' + err.message); return callback(err); } - async.eachLimit(users, 100, function(userObj, next) { - user.notifications.getDailyUnread(userObj.uid, function(err, notifications) { + async.eachLimit(users, 100, function (userObj, next) { + user.notifications.getDailyUnread(userObj.uid, function (err, notifications) { if (err) { winston.error('[user/jobs] Could not send digests (' + data.interval + '): ' + err.message); return next(err); @@ -92,14 +96,14 @@ var utils = require('../../public/src/utils'); return next(); } - for(var i=0; i<notifications.length; ++i) { + for(var i = 0; i < notifications.length; ++i) { if (notifications[i].image && notifications[i].image.indexOf('http') !== 0) { notifications[i].image = nconf.get('url') + notifications[i].image; } } emailer.send('digest', userObj.uid, { - subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear()+ '/' + (now.getMonth()+1) + '/' + now.getDate()) + ']]', + subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', username: userObj.username, userslug: userObj.userslug, url: nconf.get('url'), diff --git a/src/user/email.js b/src/user/email.js index 4b98cc72aa..5eb93b71e0 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -12,59 +12,59 @@ var db = require('../database'); var meta = require('../meta'); var emailer = require('../emailer'); -(function(UserEmail) { +(function (UserEmail) { - UserEmail.exists = function(email, callback) { - user.getUidByEmail(email.toLowerCase(), function(err, exists) { + UserEmail.exists = function (email, callback) { + user.getUidByEmail(email.toLowerCase(), function (err, exists) { callback(err, !!exists); }); }; - UserEmail.available = function(email, callback) { - db.isSortedSetMember('email:uid', email.toLowerCase(), function(err, exists) { + UserEmail.available = function (email, callback) { + db.isSortedSetMember('email:uid', email.toLowerCase(), function (err, exists) { callback(err, !exists); }); }; - UserEmail.sendValidationEmail = function(uid, email, callback) { - callback = callback || function() {}; - var confirm_code = utils.generateUUID(), - confirm_link = nconf.get('url') + '/confirm/' + confirm_code; + UserEmail.sendValidationEmail = function (uid, email, callback) { + callback = callback || function () {}; + var confirm_code = utils.generateUUID(); + var confirm_link = nconf.get('url') + '/confirm/' + confirm_code; var emailInterval = meta.config.hasOwnProperty('emailConfirmInterval') ? parseInt(meta.config.emailConfirmInterval, 10) : 10; async.waterfall([ - function(next) { + function (next) { db.get('uid:' + uid + ':confirm:email:sent', next); }, - function(sent, next) { + function (sent, next) { if (sent) { return next(new Error('[[error:confirm-email-already-sent, ' + emailInterval + ']]')); } db.set('uid:' + uid + ':confirm:email:sent', 1, next); }, - function(next) { + function (next) { db.pexpireAt('uid:' + uid + ':confirm:email:sent', Date.now() + (emailInterval * 60 * 1000), next); }, - function(next) { + function (next) { plugins.fireHook('filter:user.verify.code', confirm_code, next); }, - function(_confirm_code, next) { + function (_confirm_code, next) { confirm_code = _confirm_code; db.setObject('confirm:' + confirm_code, { email: email.toLowerCase(), uid: uid }, next); }, - function(next) { + function (next) { db.expireAt('confirm:' + confirm_code, Math.floor(Date.now() / 1000 + 60 * 60 * 24), next); }, - function(next) { + function (next) { user.getUserField(uid, 'username', next); }, - function(username, next) { + function (username, next) { var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; - translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function(subject) { + translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function (subject) { var data = { site_title: title, username: username, @@ -87,8 +87,8 @@ var emailer = require('../emailer'); ], callback); }; - UserEmail.confirm = function(code, callback) { - db.getObject('confirm:' + code, function(err, confirmObj) { + UserEmail.confirm = function (code, callback) { + db.getObject('confirm:' + code, function (err, confirmObj) { if (err) { return callback(new Error('[[error:parse-error]]')); } @@ -97,10 +97,11 @@ var emailer = require('../emailer'); async.series([ async.apply(user.setUserField, confirmObj.uid, 'email:confirmed', 1), async.apply(db.delete, 'confirm:' + code), - function(next) { + async.apply(db.delete, 'uid:' + confirmObj.uid + ':confirm:email:sent'), + function (next) { db.sortedSetRemove('users:notvalidated', confirmObj.uid, next); } - ], function(err) { + ], function (err) { callback(err ? new Error('[[error:email-confirm-failed]]') : null); }); } else { diff --git a/src/user/follow.js b/src/user/follow.js index 094d33c460..0812c1004d 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -5,13 +5,13 @@ var async = require('async'), plugins = require('../plugins'), db = require('../database'); -module.exports = function(User) { +module.exports = function (User) { - User.follow = function(uid, followuid, callback) { + User.follow = function (uid, followuid, callback) { toggleFollow('follow', uid, followuid, callback); }; - User.unfollow = function(uid, unfollowuid, callback) { + User.unfollow = function (uid, unfollowuid, callback) { toggleFollow('unfollow', uid, unfollowuid, callback); }; @@ -61,11 +61,11 @@ module.exports = function(User) { ], callback); } - User.getFollowing = function(uid, start, stop, callback) { + User.getFollowing = function (uid, start, stop, callback) { getFollow(uid, 'following', start, stop, callback); }; - User.getFollowers = function(uid, start, stop, callback) { + User.getFollowers = function (uid, start, stop, callback) { getFollow(uid, 'followers', start, stop, callback); }; @@ -74,7 +74,7 @@ module.exports = function(User) { return callback(null, []); } - db.getSortedSetRevRange(type + ':' + uid, start, stop, function(err, uids) { + db.getSortedSetRevRange(type + ':' + uid, start, stop, function (err, uids) { if (err) { return callback(err); } @@ -84,13 +84,17 @@ module.exports = function(User) { uid: uid, start: start, stop: stop - }, function(err, data) { + }, function (err, data) { + if (err) { + return callback(err); + } + User.getUsers(data.uids, uid, callback); }); }); } - User.isFollowing = function(uid, theirid, callback) { + User.isFollowing = function (uid, theirid, callback) { if (!parseInt(uid, 10) || !parseInt(theirid, 10)) { return callback(null, false); } diff --git a/src/user/info.js b/src/user/info.js new file mode 100644 index 0000000000..43991a800f --- /dev/null +++ b/src/user/info.js @@ -0,0 +1,135 @@ +'use strict'; + +var async = require('async'); +var _ = require('underscore'); +var validator = require('validator'); + +var db = require('../database'); +var posts = require('../posts'); +var topics = require('../topics'); + +module.exports = function (User) { + User.getLatestBanInfo = function (uid, callback) { + // Simply retrieves the last record of the user's ban, even if they've been unbanned since then. + var timestamp, expiry, reason; + + async.waterfall([ + async.apply(db.getSortedSetRevRangeWithScores, 'uid:' + uid + ':bans', 0, 0), + function (record, next) { + timestamp = record[0].score; + expiry = record[0].value; + + db.getSortedSetRangeByScore('banned:' + uid + ':reasons', 0, -1, timestamp, timestamp, next); + }, + function (_reason, next) { + reason = _reason && _reason.length ? _reason[0] : ''; + next(); + } + ], function (err) { + if (err) { + return callback(err); + } + + callback(null, { + uid: uid, + timestamp: timestamp, + expiry: parseInt(expiry, 10), + expiry_readable: new Date(parseInt(expiry, 10)).toString().replace(/:/g, '%3A'), + reason: validator.escape(String(reason)) + }); + }) + }; + + User.getModerationHistory = function (uid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + flags: async.apply(db.getSortedSetRevRangeWithScores, 'uid:' + uid + ':flag:pids', 0, 19), + bans: async.apply(db.getSortedSetRevRangeWithScores, 'uid:' + uid + ':bans', 0, 19), + reasons: async.apply(db.getSortedSetRevRangeWithScores, 'banned:' + uid + ':reasons', 0, 19) + }, next); + }, + function (data, next) { + getFlagMetadata(data, next); + } + ], function (err, data) { + if (err) { + return callback(err); + } + formatBanData(data); + callback(null, data); + }); + }; + + User.getHistory = function (set, callback) { + db.getSortedSetRevRangeWithScores(set, 0, -1, function (err, data) { + if (err) { + return callback(err); + } + callback(null, data.map(function (set) { + set.timestamp = set.score; + set.timestampISO = new Date(set.score).toISOString(); + set.value = validator.escape(String(set.value.split(':')[0])); + delete set.score; + return set; + })); + }); + }; + + function getFlagMetadata(data, callback) { + var pids = data.flags.map(function (flagObj) { + return parseInt(flagObj.value, 10); + }); + + posts.getPostsFields(pids, ['tid'], function (err, postData) { + if (err) { + return callback(err); + } + + var tids = postData.map(function (post) { + return post.tid; + }); + + topics.getTopicsFields(tids, ['title'], function (err, topicData) { + if (err) { + return callback(err); + } + data.flags = data.flags.map(function (flagObj, idx) { + flagObj.pid = flagObj.value; + flagObj.timestamp = flagObj.score; + flagObj.timestampISO = new Date(flagObj.score).toISOString(); + flagObj.timestampReadable = new Date(flagObj.score).toString(); + + delete flagObj.value; + delete flagObj.score; + + return _.extend(flagObj, topicData[idx]); + }); + + callback(null, data); + }); + }); + } + + function formatBanData(data) { + var reasons = data.reasons.reduce(function (memo, cur) { + memo[cur.score] = cur.value; + return memo; + }, {}); + + data.bans = data.bans.map(function (banObj) { + banObj.until = parseInt(banObj.value, 10); + banObj.untilReadable = new Date(banObj.until).toString(); + banObj.timestamp = parseInt(banObj.score, 10); + banObj.timestampReadable = new Date(banObj.score).toString(); + banObj.timestampISO = new Date(banObj.score).toISOString(); + banObj.reason = validator.escape(String(reasons[banObj.score])) || '[[user:info.banned-no-reason]]'; + + delete banObj.value; + delete banObj.score; + delete data.reasons; + + return banObj; + }); + } +}; \ No newline at end of file diff --git a/src/user/invite.js b/src/user/invite.js index 34521550a5..726a7af144 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -11,30 +11,30 @@ var translator = require('../../public/src/modules/translator'); var utils = require('../../public/src/utils'); -module.exports = function(User) { +module.exports = function (User) { - User.getInvites = function(uid, callback) { + User.getInvites = function (uid, callback) { db.getSetMembers('invitation:uid:' + uid, callback); }; - User.getInvitesNumber = function(uid, callback) { + User.getInvitesNumber = function (uid, callback) { db.setCount('invitation:uid:' + uid, callback); }; - User.getInvitingUsers = function(callback) { + User.getInvitingUsers = function (callback) { db.getSetMembers('invitation:uids', callback); }; - User.getAllInvites = function(callback) { + User.getAllInvites = function (callback) { var uids; async.waterfall([ User.getInvitingUsers, - function(_uids, next) { + function (_uids, next) { uids = _uids; async.map(uids, User.getInvites, next); }, - function(invitations, next) { - invitations = invitations.map(function(invites, index) { + function (invitations, next) { + invitations = invitations.map(function (invites, index) { return { uid: uids[index], invitations: invites @@ -45,8 +45,8 @@ module.exports = function(User) { ], callback); }; - User.sendInvitationEmail = function(uid, email, callback) { - callback = callback || function() {}; + User.sendInvitationEmail = function (uid, email, callback) { + callback = callback || function () {}; var token = utils.generateUUID(); var registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + encodeURIComponent(email); @@ -54,39 +54,39 @@ module.exports = function(User) { var oneDay = 86400000; async.waterfall([ - function(next) { + function (next) { User.getUidByEmail(email, next); }, - function(exists, next) { + function (exists, next) { if (exists) { return next(new Error('[[error:email-taken]]')); } next(); }, - function(next) { + function (next) { async.parallel([ - function(next) { + function (next) { db.setAdd('invitation:uid:' + uid, email, next); }, - function(next) { + function (next) { db.setAdd('invitation:uids', uid, next); } - ], function(err) { + ], function (err) { next(err); }); }, - function(next) { + function (next) { db.set('invitation:email:' + email, token, next); }, - function(next) { + function (next) { db.pexpireAt('invitation:email:' + email, Date.now() + oneDay, next); }, - function(next) { + function (next) { User.getUserField(uid, 'username', next); }, - function(username, next) { + function (username, next) { var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; - translator.translate('[[email:invite, ' + title + ']]', meta.config.defaultLang, function(subject) { + translator.translate('[[email:invite, ' + title + ']]', meta.config.defaultLang, function (subject) { var data = { site_title: title, registerLink: registerLink, @@ -101,16 +101,16 @@ module.exports = function(User) { ], callback); }; - User.verifyInvitation = function(query, callback) { + User.verifyInvitation = function (query, callback) { if (!query.token || !query.email) { return callback(new Error('[[error:invalid-data]]')); } async.waterfall([ - function(next) { + function (next) { db.get('invitation:email:' + query.email, next); }, - function(token, next) { + function (token, next) { if (!token || token !== query.token) { return next(new Error('[[error:invalid-token]]')); } @@ -120,8 +120,8 @@ module.exports = function(User) { ], callback); }; - User.deleteInvitation = function(invitedBy, email, callback) { - callback = callback || function() {}; + User.deleteInvitation = function (invitedBy, email, callback) { + callback = callback || function () {}; async.waterfall([ function getInvitedByUid(next) { User.getUidByUsername(invitedBy, next); @@ -137,15 +137,15 @@ module.exports = function(User) { function deleteInviteKey(next) { db.delete('invitation:email:' + email, callback); } - ], function(err) { + ], function (err) { next(err) }); } ], callback); }; - User.deleteInvitationKey = function(email, callback) { - callback = callback || function() {}; + User.deleteInvitationKey = function (email, callback) { + callback = callback || function () {}; db.delete('invitation:email:' + email, callback); }; diff --git a/src/user/jobs.js b/src/user/jobs.js index 16f6bd4ce8..956559b470 100644 --- a/src/user/jobs.js +++ b/src/user/jobs.js @@ -1,29 +1,69 @@ - 'use strict'; -var winston = require('winston'), - cronJob = require('cron').CronJob, +var winston = require('winston'); +var cronJob = require('cron').CronJob; + +var meta = require('../meta'); + +var jobs = {}; + +module.exports = function (User) { + User.startJobs = function (callback) { + winston.verbose('[user/jobs] (Re-)starting user jobs...'); + var terminated = 0; + var started = 0; + var digestHour = parseInt(meta.config.digestHour, 10); - meta = require('../meta'); + // Fix digest hour if invalid + if (isNaN(digestHour)) { + digestHour = 17; + } else if (digestHour > 23 || digestHour < 0) { + digestHour = 0; + } -module.exports = function(User) { - User.startJobs = function() { - new cronJob('0 0 17 * * *', function() { - winston.verbose('[user.startJobs] Digest job (daily) started.'); + // Terminate any active cron jobs + for(var jobId in jobs) { + if (jobs.hasOwnProperty(jobId)) { + winston.verbose('[user/jobs] Terminating job (' + jobId + ')'); + jobs[jobId].stop(); + delete jobs[jobId]; + ++terminated; + } + } + winston.verbose('[user/jobs] ' + terminated + ' jobs terminated'); + + jobs['digest.daily'] = new cronJob('0 0 ' + digestHour + ' * * *', function () { + winston.verbose('[user/jobs] Digest job (daily) started.'); User.digest.execute('day'); }, null, true); + winston.verbose('[user/jobs] Starting job (digest.daily)'); + ++started; - new cronJob('0 0 17 * * 0', function() { - winston.verbose('[user.startJobs] Digest job (weekly) started.'); + jobs['digest.weekly'] = new cronJob('0 0 ' + digestHour + ' * * 0', function () { + winston.verbose('[user/jobs] Digest job (weekly) started.'); User.digest.execute('week'); }, null, true); + winston.verbose('[user/jobs] Starting job (digest.weekly)'); + ++started; - new cronJob('0 0 17 1 * *', function() { - winston.verbose('[user.startJobs] Digest job (monthly) started.'); + jobs['digest.monthly'] = new cronJob('0 0 ' + digestHour + ' 1 * *', function () { + winston.verbose('[user/jobs] Digest job (monthly) started.'); User.digest.execute('month'); }, null, true); + winston.verbose('[user/jobs] Starting job (digest.monthly)'); + ++started; + + jobs['reset.clean'] = new cronJob('0 0 0 * * *', User.reset.clean, null, true); + winston.verbose('[user/jobs] Starting job (reset.clean)'); + ++started; + + winston.verbose('[user/jobs] ' + started + ' jobs started'); + + if (typeof callback === 'function') { + callback(); + } - new cronJob('0 0 0 * * *', User.reset.clean, null, true); + return; }; }; diff --git a/src/user/notifications.js b/src/user/notifications.js index 9ad100db2b..7182c1a223 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -1,27 +1,22 @@ 'use strict'; -var async = require('async'), - nconf = require('nconf'), - winston = require('winston'), - S = require('string'), - - user = require('../user'), - db = require('../database'), - meta = require('../meta'), - notifications = require('../notifications'), - posts = require('../posts'), - topics = require('../topics'), - privileges = require('../privileges'), - utils = require('../../public/src/utils'); - -(function(UserNotifications) { - - UserNotifications.get = function(uid, callback) { +var async = require('async'); +var winston = require('winston'); +var S = require('string'); + +var db = require('../database'); +var meta = require('../meta'); +var notifications = require('../notifications'); +var privileges = require('../privileges'); + +(function (UserNotifications) { + + UserNotifications.get = function (uid, callback) { if (!parseInt(uid, 10)) { return callback(null , {read: [], unread: []}); } - getNotifications(uid, 0, 9, function(err, notifications) { + getNotifications(uid, 0, 9, function (err, notifications) { if (err) { return callback(err); } @@ -38,13 +33,13 @@ var async = require('async'), }); }; - UserNotifications.getAll = function(uid, start, stop, callback) { - getNotifications(uid, start, stop, function(err, notifs) { + UserNotifications.getAll = function (uid, start, stop, callback) { + getNotifications(uid, start, stop, function (err, notifs) { if (err) { return callback(err); } notifs = notifs.unread.concat(notifs.read); - notifs = notifs.filter(Boolean).sort(function(a, b) { + notifs = notifs.filter(Boolean).sort(function (a, b) { return b.datetime - a.datetime; }); @@ -54,10 +49,10 @@ var async = require('async'), function getNotifications(uid, start, stop, callback) { async.parallel({ - unread: function(next) { + unread: function (next) { getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next); }, - read: function(next) { + read: function (next) { getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next); } }, callback); @@ -68,7 +63,7 @@ var async = require('async'), async.waterfall([ async.apply(db.getSortedSetRevRange, set, start, stop), - function(nids, next) { + function (nids, next) { if(!Array.isArray(nids) || !nids.length) { return callback(null, []); } @@ -76,10 +71,10 @@ var async = require('async'), setNids = nids; UserNotifications.getNotifications(nids, uid, next); }, - function(notifs, next) { + function (notifs, next) { var deletedNids = []; - notifs.forEach(function(notification, index) { + notifs.forEach(function (notification, index) { if (!notification) { winston.verbose('[notifications.get] nid ' + setNids[index] + ' not found. Removing.'); deletedNids.push(setNids[index]); @@ -98,99 +93,23 @@ var async = require('async'), ], callback); } - UserNotifications.getNotifications = function(nids, uid, callback) { - notifications.getMultiple(nids, function(err, notifications) { - if (err) { - return callback(err); - } - - UserNotifications.generateNotificationPaths(notifications, uid, callback); - }); - }; - - UserNotifications.generateNotificationPaths = function (notifications, uid, callback) { - var pids = notifications.map(function(notification) { - return notification ? notification.pid : null; - }); - - generatePostPaths(pids, uid, function(err, pidToPaths) { + UserNotifications.getNotifications = function (nids, uid, callback) { + notifications.getMultiple(nids, function (err, notifications) { if (err) { return callback(err); } - - notifications = notifications.map(function(notification, index) { - if (!notification) { - return null; - } - - notification.path = pidToPaths[notification.pid] || notification.path || null; - - if (notification.nid.startsWith('follow')) { - notification.path = '/user/' + notification.user.userslug; - } - - notification.datetimeISO = utils.toISOString(notification.datetime); - return notification; - }).filter(function(notification) { - // Remove notifications that do not resolve to a path - return notification && notification.path !== null; + notifications = notifications.filter(function (notification) { + return notification && notification.path; }); - callback(null, notifications); }); }; - function generatePostPaths(pids, uid, callback) { - pids = pids.filter(Boolean); - var postKeys = pids.map(function(pid) { - return 'post:' + pid; - }); - - db.getObjectsFields(postKeys, ['pid', 'tid'], function(err, postData) { - if (err) { - return callback(err); - } - - var topicKeys = postData.map(function(post) { - return post ? 'topic:' + post.tid : null; - }); - - async.parallel({ - indices: function(next) { - posts.getPostIndices(postData, uid, next); - }, - topics: function(next) { - db.getObjectsFields(topicKeys, ['slug', 'deleted'], next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - - var pidToPaths = {}; - pids.forEach(function(pid, index) { - if (parseInt(results.topics[index].deleted, 10) === 1) { - pidToPaths[pid] = null; - return; - } - - var slug = results.topics[index] ? results.topics[index].slug : null; - var postIndex = utils.isNumber(results.indices[index]) ? parseInt(results.indices[index], 10) + 1 : null; - - if (slug && postIndex) { - pidToPaths[pid] = '/topic/' + slug + '/' + postIndex; - } - }); - callback(null, pidToPaths); - }); - }); - } - - UserNotifications.getDailyUnread = function(uid, callback) { + UserNotifications.getDailyUnread = function (uid, callback) { var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really. - db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function(err, nids) { + db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function (err, nids) { if (err) { return callback(err); } @@ -203,7 +122,7 @@ var async = require('async'), }); }; - UserNotifications.getUnreadCount = function(uid, callback) { + UserNotifications.getUnreadCount = function (uid, callback) { if (!parseInt(uid, 10)) { return callback(null, 0); } @@ -211,20 +130,21 @@ var async = require('async'), // Collapse any notifications with identical mergeIds async.waterfall([ async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':notifications:unread', 0, 99), - function(nids, next) { - var keys = nids.map(function(nid) { + async.apply(notifications.filterExists), + function (nids, next) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); db.getObjectsFields(keys, ['mergeId'], next); } - ], function(err, mergeIds) { + ], function (err, mergeIds) { // A missing (null) mergeId means that notification is counted separately. - mergeIds = mergeIds.map(function(set) { + mergeIds = mergeIds.map(function (set) { return set.mergeId; }); - callback(err, mergeIds.reduce(function(count, cur, idx, arr) { + callback(err, mergeIds.reduce(function (count, cur, idx, arr) { if (cur === null || idx === arr.indexOf(cur)) { ++count; } @@ -234,8 +154,8 @@ var async = require('async'), }); }; - UserNotifications.getUnreadByField = function(uid, field, value, callback) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function(err, nids) { + UserNotifications.getUnreadByField = function (uid, field, values, callback) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { if (err) { return callback(err); } @@ -244,19 +164,19 @@ var async = require('async'), return callback(null, []); } - var keys = nids.map(function(nid) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); - db.getObjectsFields(keys, ['nid', field], function(err, notifications) { + db.getObjectsFields(keys, ['nid', field], function (err, notifications) { if (err) { return callback(err); } - value = value ? value.toString() : ''; - nids = notifications.filter(function(notification) { - return notification && notification[field] && notification[field].toString() === value; - }).map(function(notification) { + values = values.map(function () { return values.toString(); }); + nids = notifications.filter(function (notification) { + return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1; + }).map(function (notification) { return notification.nid; }); @@ -265,28 +185,35 @@ var async = require('async'), }); }; - UserNotifications.deleteAll = function(uid, callback) { + UserNotifications.deleteAll = function (uid, callback) { if (!parseInt(uid, 10)) { return callback(); } async.parallel([ - function(next) { + function (next) { db.delete('uid:' + uid + ':notifications:unread', next); }, - function(next) { + function (next) { db.delete('uid:' + uid + ':notifications:read', next); } ], callback); }; - UserNotifications.sendTopicNotificationToFollowers = function(uid, topicData, postData) { - db.getSortedSetRange('followers:' + uid, 0, -1, function(err, followers) { - if (err || !Array.isArray(followers) || !followers.length) { - return; - } - - privileges.categories.filterUids('read', topicData.cid, followers, function(err, followers) { - if (err || !followers.length) { + UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) { + var followers; + async.waterfall([ + function (next) { + db.getSortedSetRange('followers:' + uid, 0, -1, next); + }, + function (followers, next) { + if (!Array.isArray(followers) || !followers.length) { + return; + } + privileges.categories.filterUids('read', topicData.cid, followers, next); + }, + function (_followers, next) { + followers = _followers; + if (!followers.length) { return; } @@ -299,20 +226,25 @@ var async = require('async'), bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]', bodyLong: postData.content, pid: postData.pid, + path: '/post/' + postData.pid, nid: 'tid:' + postData.tid + ':uid:' + uid, tid: postData.tid, from: uid - }, function(err, notification) { - if (!err && notification) { - notifications.push(notification, followers); - } - }); - }); + }, next); + } + ], function (err, notification) { + if (err) { + return winston.error(err); + } + + if (notification) { + notifications.push(notification, followers); + } }); }; - UserNotifications.sendWelcomeNotification = function(uid, callback) { - callback = callback || function() {}; + UserNotifications.sendWelcomeNotification = function (uid, callback) { + callback = callback || function () {}; if (!meta.config.welcomeNotification) { return callback(); } @@ -323,7 +255,7 @@ var async = require('async'), bodyShort: meta.config.welcomeNotification, path: path, nid: 'welcome_' + uid - }, function(err, notification) { + }, function (err, notification) { if (err || !notification) { return callback(err); } @@ -332,22 +264,22 @@ var async = require('async'), }); }; - UserNotifications.sendNameChangeNotification = function(uid, username) { + UserNotifications.sendNameChangeNotification = function (uid, username) { notifications.create({ bodyShort: '[[user:username_taken_workaround, ' + username + ']]', image: 'brand:logo', nid: 'username_taken:' + uid, datetime: Date.now() - }, function(err, notification) { + }, function (err, notification) { if (!err && notification) { notifications.push(notification, uid); } }); }; - UserNotifications.pushCount = function(uid) { + UserNotifications.pushCount = function (uid) { var websockets = require('./../socket.io'); - UserNotifications.getUnreadCount(uid, function(err, count) { + UserNotifications.getUnreadCount(uid, function (err, count) { if (err) { return winston.error(err.stack); } diff --git a/src/user/password.js b/src/user/password.js index e60ae9b753..8e9b7780e3 100644 --- a/src/user/password.js +++ b/src/user/password.js @@ -6,9 +6,9 @@ var nconf = require('nconf'); var db = require('../database'); var Password = require('../password'); -module.exports = function(User) { +module.exports = function (User) { - User.hashPassword = function(password, callback) { + User.hashPassword = function (password, callback) { if (!password) { return callback(null, password); } @@ -16,7 +16,7 @@ module.exports = function(User) { Password.hash(nconf.get('bcrypt_rounds') || 12, password, callback); }; - User.isPasswordCorrect = function(uid, password, callback) { + User.isPasswordCorrect = function (uid, password, callback) { password = password || ''; async.waterfall([ function (next) { @@ -27,7 +27,7 @@ module.exports = function(User) { return callback(null, true); } - User.isPasswordValid(password, function(err) { + User.isPasswordValid(password, function (err) { if (err) { return next(err); } @@ -38,8 +38,8 @@ module.exports = function(User) { ], callback); }; - User.hasPassword = function(uid, callback) { - db.getObjectField('user:' + uid, 'password', function(err, hashedPassword) { + User.hasPassword = function (uid, callback) { + db.getObjectField('user:' + uid, 'password', function (err, hashedPassword) { callback(err, !!hashedPassword); }); }; diff --git a/src/user/picture.js b/src/user/picture.js index c9a760f10e..64f9c900c2 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -1,22 +1,22 @@ 'use strict'; -var async = require('async'), - path = require('path'), - fs = require('fs'), - os = require('os'), - nconf = require('nconf'), - crypto = require('crypto'), - winston = require('winston'), - request = require('request'), - mime = require('mime'), - - plugins = require('../plugins'), - file = require('../file'), - image = require('../image'), - meta = require('../meta'), - db = require('../database'); - -module.exports = function(User) { +var async = require('async'); +var path = require('path'); +var fs = require('fs'); +var os = require('os'); +var nconf = require('nconf'); +var crypto = require('crypto'); +var winston = require('winston'); +var request = require('request'); +var mime = require('mime'); + +var plugins = require('../plugins'); +var file = require('../file'); +var image = require('../image'); +var meta = require('../meta'); +var db = require('../database'); + +module.exports = function (User) { User.uploadPicture = function (uid, picture, callback) { @@ -25,30 +25,34 @@ module.exports = function(User) { var updateUid = uid; var imageDimension = parseInt(meta.config.profileImageDimension, 10) || 128; var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1; + var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1; var uploadedImage; + if (parseInt(meta.config.allowProfileImageUploads) !== 1) { + return callback(new Error('[[error:profile-image-uploads-disabled]]')); + } + + if (picture.size > uploadSize * 1024) { + return callback(new Error('[[error:file-too-big, ' + uploadSize + ']]')); + } + + if (!extension) { + return callback(new Error('[[error:invalid-image-extension]]')); + } + async.waterfall([ - function(next) { - next(parseInt(meta.config.allowProfileImageUploads) !== 1 ? new Error('[[error:profile-image-uploads-disabled]]') : null); - }, - function(next) { - next(picture.size > uploadSize * 1024 ? new Error('[[error:file-too-big, ' + uploadSize + ']]') : null); - }, - function(next) { - next(!extension ? new Error('[[error:invalid-image-extension]]') : null); - }, - function(next) { + function (next) { if (plugins.hasListeners('filter:uploadImage')) { return plugins.fireHook('filter:uploadImage', {image: picture, uid: updateUid}, next); } - var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension); + var filename = updateUid + '-profileimg' + (keepAllVersions ? '-' + Date.now() : '') + (convertToPNG ? '.png' : extension); async.waterfall([ - function(next) { + function (next) { file.isFileTypeAllowed(picture.path, next); }, - function(next) { + function (next) { image.resizeImage({ path: picture.path, extension: extension, @@ -56,48 +60,38 @@ module.exports = function(User) { height: imageDimension }, next); }, - function(next) { - if (convertToPNG) { - image.normalise(picture.path, extension, next); - } else { - next(); - } - }, - function(next) { - User.getUserField(updateUid, 'uploadedpicture', next); - }, - function(oldpicture, next) { - if (!oldpicture) { - return file.saveFileToLocal(filename, 'profile', picture.path, next); + function (next) { + if (!convertToPNG) { + return next(); } - var oldpicturePath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), 'profile', path.basename(oldpicture)); - - fs.unlink(oldpicturePath, function (err) { - if (err) { - winston.error(err); - } - - file.saveFileToLocal(filename, 'profile', picture.path, next); + async.series([ + async.apply(image.normalise, picture.path, extension), + async.apply(fs.rename, picture.path + '.png', picture.path) + ], function (err) { + next(err); }); }, + function (next) { + file.saveFileToLocal(filename, 'profile', picture.path, next); + }, ], next); }, - function(_image, next) { + function (_image, next) { uploadedImage = _image; User.setUserFields(updateUid, {uploadedpicture: uploadedImage.url, picture: uploadedImage.url}, next); }, - function(next) { + function (next) { next(null, uploadedImage); } ], callback); }; - User.uploadFromUrl = function(uid, url, callback) { + User.uploadFromUrl = function (uid, url, callback) { if (!plugins.hasListeners('filter:uploadImage')) { return callback(new Error('[[error:no-plugin]]')); } - request.head(url, function(err, res) { + request.head(url, function (err, res) { if (err) { return callback(err); } @@ -115,7 +109,7 @@ module.exports = function(User) { } var picture = {url: url, name: ''}; - plugins.fireHook('filter:uploadImage', {image: picture, uid: uid}, function(err, image) { + plugins.fireHook('filter:uploadImage', {image: picture, uid: uid}, function (err, image) { if (err) { return callback(err); } @@ -125,11 +119,12 @@ module.exports = function(User) { }); }; - User.updateCoverPosition = function(uid, position, callback) { + User.updateCoverPosition = function (uid, position, callback) { User.setUserField(uid, 'cover:position', position, callback); }; - User.updateCoverPicture = function(data, callback) { + User.updateCoverPicture = function (data, callback) { + var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1; var url, md5sum; if (!data.imageData && data.position) { @@ -141,7 +136,7 @@ module.exports = function(User) { } async.waterfall([ - function(next) { + function (next) { var size = data.file ? data.file.size : data.imageData.length; meta.config.maximumCoverImageSize = meta.config.maximumCoverImageSize || 2048; if (size > parseInt(meta.config.maximumCoverImageSize, 10) * 1024) { @@ -166,7 +161,7 @@ module.exports = function(User) { encoding: 'base64' }, next); }, - function(next) { + function (next) { var image = { name: 'profileCover', path: data.file.path, @@ -177,7 +172,7 @@ module.exports = function(User) { return plugins.fireHook('filter:uploadImage', {image: image, uid: data.uid}, next); } - var filename = data.uid + '-profilecover'; + var filename = data.uid + '-profilecover' + (keepAllVersions ? '-' + Date.now() : ''); async.waterfall([ function (next) { file.isFileTypeAllowed(data.file.path, next); @@ -193,27 +188,31 @@ module.exports = function(User) { } ], next); }, - function(uploadData, next) { + function (uploadData, next) { url = uploadData.url; User.setUserField(data.uid, 'cover:url', uploadData.url, next); }, - function(next) { - fs.unlink(data.file.path, function(err) { + function (next) { + fs.unlink(data.file.path, function (err) { if (err) { winston.error(err); } next(); }); } - ], function(err) { + ], function (err) { if (err) { - return fs.unlink(data.file.path, function(unlinkErr) { + return fs.unlink(data.file.path, function (unlinkErr) { + if (unlinkErr) { + winston.error(unlinkErr); + } + callback(err); // send back the original error }); } if (data.position) { - User.updateCoverPosition(data.uid, data.position, function(err) { + User.updateCoverPosition(data.uid, data.position, function (err) { callback(err, {url: url}); }); } else { @@ -222,7 +221,7 @@ module.exports = function(User) { }); }; - User.removeCoverPicture = function(data, callback) { + User.removeCoverPicture = function (data, callback) { db.deleteObjectField('user:' + data.uid, 'cover:url', callback); }; -}; \ No newline at end of file +}; diff --git a/src/user/posts.js b/src/user/posts.js index 78a2db0923..37b5e92b16 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -1,28 +1,28 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - meta = require('../meta'), - privileges = require('../privileges'); +var async = require('async'); +var db = require('../database'); +var meta = require('../meta'); +var privileges = require('../privileges'); -module.exports = function(User) { +module.exports = function (User) { - User.isReadyToPost = function(uid, cid, callback) { + User.isReadyToPost = function (uid, cid, callback) { if (parseInt(uid, 10) === 0) { return callback(); } async.parallel({ - userData: function(next) { + userData: function (next) { User.getUserFields(uid, ['banned', 'lastposttime', 'joindate', 'email', 'email:confirmed', 'reputation'], next); }, - exists: function(next) { + exists: function (next) { db.exists('user:' + uid, next); }, - isAdminOrMod: function(next) { + isAdminOrMod: function (next) { privileges.categories.isAdminOrMod(cid, uid, next); } - }, function(err, results) { + }, function (err, results) { if (err) { return callback(err); } @@ -62,37 +62,30 @@ module.exports = function(User) { }); }; - User.onNewPostMade = function(postData, callback) { + User.onNewPostMade = function (postData, callback) { async.series([ - function(next) { + function (next) { User.addPostIdToUser(postData.uid, postData.pid, postData.timestamp, next); }, - function(next) { + function (next) { User.incrementUserPostCountBy(postData.uid, 1, next); }, - function(next) { + function (next) { User.setUserField(postData.uid, 'lastposttime', postData.timestamp, next); }, - function(next) { + function (next) { User.updateLastOnlineTime(postData.uid, next); } ], callback); }; - User.addPostIdToUser = function(uid, pid, timestamp, callback) { + User.addPostIdToUser = function (uid, pid, timestamp, callback) { db.sortedSetAdd('uid:' + uid + ':posts', timestamp, pid, callback); }; - User.addTopicIdToUser = function(uid, tid, timestamp, callback) { - async.parallel([ - async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid), - async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1) - ], callback); - }; - - User.incrementUserPostCountBy = function(uid, value, callback) { - callback = callback || function() {}; - User.incrementUserFieldBy(uid, 'postcount', value, function(err, newpostcount) { + User.incrementUserPostCountBy = function (uid, value, callback) { + callback = callback || function () {}; + User.incrementUserFieldBy(uid, 'postcount', value, function (err, newpostcount) { if (err) { return callback(err); } @@ -103,8 +96,8 @@ module.exports = function(User) { }); }; - User.getPostIds = function(uid, start, stop, callback) { - db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop, function(err, pids) { + User.getPostIds = function (uid, start, stop, callback) { + db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop, function (err, pids) { callback(err, Array.isArray(pids) ? pids : []); }); }; diff --git a/src/user/profile.js b/src/user/profile.js index 50aa92c1d0..7ebf7cfafd 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -7,137 +7,147 @@ var S = require('string'); var utils = require('../../public/src/utils'); var meta = require('../meta'); var db = require('../database'); +var groups = require('../groups'); var plugins = require('../plugins'); -module.exports = function(User) { +module.exports = function (User) { - User.updateProfile = function(uid, data, callback) { - var fields = ['username', 'email', 'fullname', 'website', 'location', 'birthday', 'signature', 'aboutme']; + User.updateProfile = function (uid, data, callback) { + var fields = ['username', 'email', 'fullname', 'website', 'location', + 'groupTitle', 'birthday', 'signature', 'aboutme', 'picture', 'uploadedpicture']; - plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, function(err, data) { - if (err) { - return callback(err); - } - - fields = data.fields; - data = data.data; - - function isAboutMeValid(next) { - if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { - next(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]')); - } else { - next(); - } - } - - function isSignatureValid(next) { - if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { - next(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]')); - } else { - next(); - } - } - - function isEmailAvailable(next) { - if (!data.email) { - return next(); - } - - if (!utils.isEmailValid(data.email)) { - return next(new Error('[[error:invalid-email]]')); - } - - User.getUserField(uid, 'email', function(err, email) { - if(email === data.email) { - return next(); - } - - User.email.available(data.email, function(err, available) { - if (err) { - return next(err); - } - - next(!available ? new Error('[[error:email-taken]]') : null); - }); + async.waterfall([ + function (next) { + plugins.fireHook('filter:user.updateProfile', {uid: uid, data: data, fields: fields}, next); + }, + function (data, next) { + fields = data.fields; + data = data.data; + + async.series([ + async.apply(isAboutMeValid, data), + async.apply(isSignatureValid, data), + async.apply(isEmailAvailable, data, uid), + async.apply(isUsernameAvailable, data, uid), + async.apply(isGroupTitleValid, data) + ], function (err) { + next(err); }); - } - - function isUsernameAvailable(next) { - if (!data.username) { - return next(); - } - data.username = data.username.trim(); - User.getUserFields(uid, ['username', 'userslug'], function(err, userData) { - if (err) { - return next(err); + }, + function (next) { + async.each(fields, function (field, next) { + if (!(data[field] !== undefined && typeof data[field] === 'string')) { + return next(); } - var userslug = utils.slugify(data.username); + data[field] = data[field].trim(); - if (data.username.length < meta.config.minimumUsernameLength) { - return next(new Error('[[error:username-too-short]]')); + if (field === 'email') { + return updateEmail(uid, data.email, next); + } else if (field === 'username') { + return updateUsername(uid, data.username, next); + } else if (field === 'fullname') { + return updateFullname(uid, data.fullname, next); + } else if (field === 'signature') { + data[field] = S(data[field]).stripTags().s; } - if (data.username.length > meta.config.maximumUsernameLength) { - return next(new Error('[[error:username-too-long]]')); - } + User.setUserField(uid, field, data[field], next); + }, next); + }, + function (next) { + plugins.fireHook('action:user.updateProfile', {data: data, uid: uid}); + User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next); + } + ], callback); + }; - if (!utils.isUserNameValid(data.username) || !userslug) { - return next(new Error('[[error:invalid-username]]')); - } + function isAboutMeValid(data, callback) { + if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { + callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]')); + } else { + callback(); + } + } - if (userslug === userData.userslug) { - return next(); - } + function isSignatureValid(data, callback) { + if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { + callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]')); + } else { + callback(); + } + } - User.existsBySlug(userslug, function(err, exists) { - if (err) { - return next(err); - } + function isEmailAvailable(data, uid, callback) { + if (!data.email) { + return callback(); + } - next(exists ? new Error('[[error:username-taken]]') : null); - }); - }); - } + if (!utils.isEmailValid(data.email)) { + return callback(new Error('[[error:invalid-email]]')); + } - async.series([isAboutMeValid, isSignatureValid, isEmailAvailable, isUsernameAvailable], function(err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + User.getUserField(uid, 'email', next); + }, + function (email, next) { + if (email === data.email) { + return callback(); } + User.email.available(data.email, next); + }, + function (available, next) { + next(!available ? new Error('[[error:email-taken]]') : null); + } + ], callback); + } - async.each(fields, updateField, function(err) { - if (err) { - return callback(err); - } - plugins.fireHook('action:user.updateProfile', {data: data, uid: uid}); - User.getUserFields(uid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], callback); - }); - }); + function isUsernameAvailable(data, uid, callback) { + if (!data.username) { + return callback(); + } + data.username = data.username.trim(); + async.waterfall([ + function (next) { + User.getUserFields(uid, ['username', 'userslug'], next); + }, + function (userData, next) { + var userslug = utils.slugify(data.username); - function updateField(field, next) { - if (!(data[field] !== undefined && typeof data[field] === 'string')) { - return next(); + if (data.username.length < meta.config.minimumUsernameLength) { + return next(new Error('[[error:username-too-short]]')); } - data[field] = data[field].trim(); + if (data.username.length > meta.config.maximumUsernameLength) { + return next(new Error('[[error:username-too-long]]')); + } - if (field === 'email') { - return updateEmail(uid, data.email, next); - } else if (field === 'username') { - return updateUsername(uid, data.username, next); - } else if (field === 'fullname') { - return updateFullname(uid, data.fullname, next); - } else if (field === 'signature') { - data[field] = S(data[field]).stripTags().s; + if (!utils.isUserNameValid(data.username) || !userslug) { + return next(new Error('[[error:invalid-username]]')); } - User.setUserField(uid, field, data[field], next); + if (userslug === userData.userslug) { + return callback(); + } + User.existsBySlug(userslug, next); + }, + function (exists, next) { + next(exists ? new Error('[[error:username-taken]]') : null); } - }); - }; + ], callback); + } + + function isGroupTitleValid(data, callback) { + if (data.groupTitle === 'registered-users' || groups.isPrivilegeGroup(data.groupTitle)) { + callback(new Error('[[error:invalid-group-title]]')); + } else { + callback(); + } + } function updateEmail(uid, newEmail, callback) { - User.getUserFields(uid, ['email', 'picture', 'uploadedpicture'], function(err, userData) { + User.getUserFields(uid, ['email', 'picture', 'uploadedpicture'], function (err, userData) { if (err) { return callback(err); } @@ -150,22 +160,23 @@ module.exports = function(User) { async.series([ async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()), async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid) - ], function(err) { + ], function (err) { if (err) { return callback(err); } async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('email:uid', uid, newEmail.toLowerCase(), next); }, - function(next) { + async.apply(db.sortedSetAdd, 'user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()), + function (next) { db.sortedSetAdd('email:sorted', 0, newEmail.toLowerCase() + ':' + uid, next); }, - function(next) { + function (next) { User.setUserField(uid, 'email', newEmail, next); }, - function(next) { + function (next) { if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && newEmail) { User.email.sendValidationEmail(uid, newEmail); } @@ -184,23 +195,24 @@ module.exports = function(User) { return callback(); } - User.getUserFields(uid, ['username', 'userslug'], function(err, userData) { + User.getUserFields(uid, ['username', 'userslug'], function (err, userData) { if (err) { return callback(err); } async.parallel([ - function(next) { + function (next) { updateUidMapping('username', uid, newUsername, userData.username, next); }, - function(next) { + function (next) { var newUserslug = utils.slugify(newUsername); updateUidMapping('userslug', uid, newUserslug, userData.userslug, next); }, - function(next) { + function (next) { async.series([ async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid), - async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid) + async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid), + async.apply(db.sortedSetAdd, 'user:' + uid + ':usernames', Date.now(), newUsername + ':' + Date.now()) ], next); }, ], callback); @@ -213,13 +225,13 @@ module.exports = function(User) { } async.series([ - function(next) { + function (next) { db.sortedSetRemove(field + ':uid', oldValue, next); }, - function(next) { + function (next) { User.setUserField(uid, field, value, next); }, - function(next) { + function (next) { if (value) { db.sortedSetAdd(field + ':uid', uid, value, next); } else { @@ -231,16 +243,16 @@ module.exports = function(User) { function updateFullname(uid, newFullname, callback) { async.waterfall([ - function(next) { + function (next) { User.getUserField(uid, 'fullname', next); }, - function(fullname, next) { + function (fullname, next) { updateUidMapping('fullname', uid, newFullname, fullname, next); } ], callback); } - User.changePassword = function(uid, data, callback) { + User.changePassword = function (uid, data, callback) { if (!uid || !data || !data.uid) { return callback(new Error('[[error:invalid-uid]]')); } @@ -267,7 +279,7 @@ module.exports = function(User) { async.parallel([ async.apply(User.setUserField, data.uid, 'password', hashedPassword), async.apply(User.reset.updateExpiry, data.uid) - ], function(err) { + ], function (err) { next(err); }); } diff --git a/src/user/reset.js b/src/user/reset.js index 222e988de6..39bf1f0e07 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -12,32 +12,32 @@ var async = require('async'), meta = require('../meta'), emailer = require('../emailer'); -(function(UserReset) { +(function (UserReset) { var twoHours = 7200000; - UserReset.validate = function(code, callback) { + UserReset.validate = function (code, callback) { async.waterfall([ - function(next) { + function (next) { db.getObjectField('reset:uid', code, next); }, - function(uid, next) { + function (uid, next) { if (!uid) { return callback(null, false); } db.sortedSetScore('reset:issueDate', code, next); }, - function(issueDate, next) { + function (issueDate, next) { next(null, parseInt(issueDate, 10) > Date.now() - twoHours); } ], callback); }; - UserReset.generate = function(uid, callback) { + UserReset.generate = function (uid, callback) { var code = utils.generateUUID(); async.parallel([ async.apply(db.setObjectField, 'reset:uid', code, uid), async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), code) - ], function(err) { + ], function (err) { callback(err, code); }); }; @@ -56,13 +56,13 @@ var async = require('async'), ], callback); } - UserReset.send = function(email, callback) { + UserReset.send = function (email, callback) { var uid; async.waterfall([ - function(next) { + function (next) { user.getUidByEmail(email, next); }, - function(_uid, next) { + function (_uid, next) { if (!_uid) { return next(new Error('[[error:invalid-email]]')); } @@ -70,18 +70,18 @@ var async = require('async'), uid = _uid; canGenerate(uid, next); }, - function(next) { + function (next) { db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid, next); }, - function(next) { + function (next) { UserReset.generate(uid, next); }, - function(code, next) { - translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function(subject) { + function (code, next) { + translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function (subject) { next(null, subject, code); }); }, - function(subject, code, next) { + function (subject, code, next) { var reset_link = nconf.get('url') + '/reset/' + code; emailer.send('reset', uid, { site_title: (meta.config.title || 'NodeBB'), @@ -94,22 +94,22 @@ var async = require('async'), ], callback); }; - UserReset.commit = function(code, password, callback) { + UserReset.commit = function (code, password, callback) { var uid; async.waterfall([ - function(next) { + function (next) { user.isPasswordValid(password, next); }, - function(next) { + function (next) { UserReset.validate(code, next); }, - function(validated, next) { + function (validated, next) { if (!validated) { return next(new Error('[[error:reset-code-not-valid]]')); } db.getObjectField('reset:uid', code, next); }, - function(_uid, next) { + function (_uid, next) { uid = _uid; if (!uid) { return next(new Error('[[error:reset-code-not-valid]]')); @@ -117,7 +117,7 @@ var async = require('async'), user.hashPassword(password, next); }, - function(hash, next) { + function (hash, next) { async.parallel([ async.apply(user.setUserField, uid, 'password', hash), async.apply(db.deleteObjectField, 'reset:uid', code), @@ -130,28 +130,28 @@ var async = require('async'), ], callback); }; - UserReset.updateExpiry = function(uid, callback) { + UserReset.updateExpiry = function (uid, callback) { var oneDay = 1000 * 60 * 60 * 24; var expireDays = parseInt(meta.config.passwordExpiryDays || 0, 10); var expiry = Date.now() + (oneDay * expireDays); - callback = callback || function() {}; + callback = callback || function () {}; user.setUserField(uid, 'passwordExpiry', expireDays > 0 ? expiry : 0, callback); }; - UserReset.clean = function(callback) { + UserReset.clean = function (callback) { async.waterfall([ - function(next) { + function (next) { async.parallel({ - tokens: function(next) { + tokens: function (next) { db.getSortedSetRangeByScore('reset:issueDate', 0, -1, '-inf', Date.now() - twoHours, next); }, - uids: function(next) { + uids: function (next) { db.getSortedSetRangeByScore('reset:issueDate:uid', 0, -1, '-inf', Date.now() - twoHours, next); } }, next); }, - function(results, next) { + function (results, next) { if (!results.tokens.length && !results.uids.length) { return next(); } diff --git a/src/user/search.js b/src/user/search.js index b2a1e83a80..65d0e41a6a 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -1,14 +1,14 @@ 'use strict'; -var async = require('async'), - meta = require('../meta'), - plugins = require('../plugins'), - db = require('../database'); +var async = require('async'); +var meta = require('../meta'); +var plugins = require('../plugins'); +var db = require('../database'); -module.exports = function(User) { +module.exports = function (User) { - User.search = function(data, callback) { + User.search = function (data, callback) { var query = data.query || ''; var searchBy = data.searchBy || 'username'; var page = data.page || 1; @@ -23,20 +23,20 @@ module.exports = function(User) { var searchResult = {}; async.waterfall([ - function(next) { + function (next) { if (data.findUids) { data.findUids(query, searchBy, next); } else { findUids(query, searchBy, next); } }, - function(uids, next) { + function (uids, next) { filterAndSortUids(uids, data, next); }, - function(uids, next) { + function (uids, next) { plugins.fireHook('filter:users.search', {uids: uids, uid: uid}, next); }, - function(data, next) { + function (data, next) { var uids = data.uids; searchResult.matchCount = uids.length; @@ -50,7 +50,7 @@ module.exports = function(User) { User.getUsers(uids, uid, next); }, - function(userData, next) { + function (userData, next) { searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); searchResult.users = userData; next(null, searchResult); @@ -69,12 +69,12 @@ module.exports = function(User) { var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; var hardCap = resultsPerPage * 10; - db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function(err, data) { + db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function (err, data) { if (err) { return callback(err); } - var uids = data.map(function(data) { + var uids = data.map(function (data) { return data.split(':')[1]; }); callback(null, uids); @@ -84,28 +84,43 @@ module.exports = function(User) { function filterAndSortUids(uids, data, callback) { var sortBy = data.sortBy || 'joindate'; - var fields = ['uid', 'status', 'lastonline', 'banned', sortBy]; + var fields = ['uid', sortBy]; + if (data.onlineOnly) { + fields = fields.concat(['status', 'lastonline']); + } + if (data.bannedOnly) { + fields.push('banned'); + } + if (data.flaggedOnly) { + fields.push('flags'); + } - User.getUsersFields(uids, fields, function(err, userData) { + User.getUsersFields(uids, fields, function (err, userData) { if (err) { return callback(err); } if (data.onlineOnly) { - userData = userData.filter(function(user) { + userData = userData.filter(function (user) { return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000); }); } - - if(data.bannedOnly) { - userData = userData.filter(function(user) { + + if (data.bannedOnly) { + userData = userData.filter(function (user) { return user && user.banned; }); } + if (data.flaggedOnly) { + userData = userData.filter(function (user) { + return user && parseInt(user.flags, 10) > 0; + }); + } + sortUsers(userData, sortBy); - uids = userData.map(function(user) { + uids = userData.map(function (user) { return user && user.uid; }); @@ -115,11 +130,11 @@ module.exports = function(User) { function sortUsers(userData, sortBy) { if (sortBy === 'joindate' || sortBy === 'postcount' || sortBy === 'reputation') { - userData.sort(function(u1, u2) { + userData.sort(function (u1, u2) { return u2[sortBy] - u1[sortBy]; }); } else { - userData.sort(function(u1, u2) { + userData.sort(function (u1, u2) { if(u1[sortBy] < u2[sortBy]) { return -1; } else if(u1[sortBy] > u2[sortBy]) { @@ -133,13 +148,13 @@ module.exports = function(User) { function searchByIP(ip, uid, callback) { var start = process.hrtime(); async.waterfall([ - function(next) { + function (next) { db.getSortedSetRevRange('ip:' + ip + ':uid', 0, -1, next); }, - function(uids, next) { + function (uids, next) { User.getUsers(uids, uid, next); }, - function(users, next) { + function (users, next) { var diff = process.hrtime(start); var timing = (diff[0] * 1e3 + diff[1] / 1e6).toFixed(1); next(null, {timing: timing, users: users}); diff --git a/src/user/settings.js b/src/user/settings.js index 04e23e1152..29666cfce5 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -1,19 +1,19 @@ 'use strict'; -var async = require('async'), - meta = require('../meta'), - db = require('../database'), - plugins = require('../plugins'); +var async = require('async'); +var meta = require('../meta'); +var db = require('../database'); +var plugins = require('../plugins'); -module.exports = function(User) { +module.exports = function (User) { - User.getSettings = function(uid, callback) { + User.getSettings = function (uid, callback) { if (!parseInt(uid, 10)) { return onSettingsLoaded(0, {}, callback); } - db.getObject('user:' + uid + ':settings', function(err, settings) { + db.getObject('user:' + uid + ':settings', function (err, settings) { if (err) { return callback(err); } @@ -22,33 +22,33 @@ module.exports = function(User) { }); }; - User.getMultipleUserSettings = function(uids, callback) { + User.getMultipleUserSettings = function (uids, callback) { if (!Array.isArray(uids) || !uids.length) { return callback(null, []); } - var keys = uids.map(function(uid) { + var keys = uids.map(function (uid) { return 'user:' + uid + ':settings'; }); - db.getObjects(keys, function(err, settings) { + db.getObjects(keys, function (err, settings) { if (err) { return callback(err); } - for (var i=0; i<settings.length; ++i) { + for (var i = 0; i < settings.length; ++i) { settings[i] = settings[i] || {}; settings[i].uid = uids[i]; } - async.map(settings, function(setting, next) { + async.map(settings, function (setting, next) { onSettingsLoaded(setting.uid, setting, next); }, callback); }); }; function onSettingsLoaded(uid, settings, callback) { - plugins.fireHook('filter:user.getSettings', {uid: uid, settings: settings}, function(err, data) { + plugins.fireHook('filter:user.getSettings', {uid: uid, settings: settings}, function (err, data) { if (err) { return callback(err); } @@ -65,7 +65,6 @@ module.exports = function(User) { settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage); settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage); - settings.notificationSounds = parseInt(getSetting(settings, 'notificationSounds', 0), 10) === 1; settings.userLang = settings.userLang || meta.config.defaultLang || 'en_GB'; settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); @@ -75,6 +74,7 @@ module.exports = function(User) { settings.sendPostNotifications = parseInt(getSetting(settings, 'sendPostNotifications', 0), 10) === 1; settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; + settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1; settings.bootswatchSkin = settings.bootswatchSkin || 'default'; settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; @@ -91,7 +91,7 @@ module.exports = function(User) { return defaultValue; } - User.saveSettings = function(uid, data, callback) { + User.saveSettings = function (uid, data, callback) { if (!data.postsPerPage || parseInt(data.postsPerPage, 10) <= 1 || parseInt(data.postsPerPage, 10) > meta.config.postsPerPage) { return callback(new Error('[[error:invalid-pagination-value, 2, ' + meta.config.postsPerPage + ']]')); } @@ -112,7 +112,6 @@ module.exports = function(User) { usePagination: data.usePagination, topicsPerPage: Math.min(data.topicsPerPage, parseInt(meta.config.topicsPerPage, 10) || 20), postsPerPage: Math.min(data.postsPerPage, parseInt(meta.config.postsPerPage, 10) || 20), - notificationSounds: data.notificationSounds, userLang: data.userLang || meta.config.defaultLang, followTopicsOnCreate: data.followTopicsOnCreate, followTopicsOnReply: data.followTopicsOnReply, @@ -120,9 +119,12 @@ module.exports = function(User) { sendPostNotifications: data.sendPostNotifications, restrictChat: data.restrictChat, topicSearchEnabled: data.topicSearchEnabled, - groupTitle: data.groupTitle, - homePageRoute: data.homePageCustom || data.homePageRoute, - scrollToMyPost: data.scrollToMyPost + delayImageLoading: data.delayImageLoading, + homePageRoute : ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''), + scrollToMyPost: data.scrollToMyPost, + notificationSound: data.notificationSound, + incomingChatSound: data.incomingChatSound, + outgoingChatSound: data.outgoingChatSound }; if (data.bootswatchSkin) { @@ -130,24 +132,24 @@ module.exports = function(User) { } async.waterfall([ - function(next) { + function (next) { db.setObject('user:' + uid + ':settings', settings, next); }, - function(next) { - updateDigestSetting(uid, data.dailyDigestFreq, next); + function (next) { + User.updateDigestSetting(uid, data.dailyDigestFreq, next); }, - function(next) { + function (next) { User.getSettings(uid, next); } ], callback); }; - function updateDigestSetting(uid, dailyDigestFreq, callback) { + User.updateDigestSetting = function (uid, dailyDigestFreq, callback) { async.waterfall([ - function(next) { + function (next) { db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid, next); }, - function(next) { + function (next) { if (['day', 'week', 'month'].indexOf(dailyDigestFreq) !== -1) { db.sortedSetAdd('digest:' + dailyDigestFreq + ':uids', Date.now(), uid, next); } else { @@ -155,22 +157,9 @@ module.exports = function(User) { } } ], callback); - } - - User.setSetting = function(uid, key, value, callback) { - db.setObjectField('user:' + uid + ':settings', key, value, callback); }; - User.setGroupTitle = function(groupName, uid, callback) { - if (groupName === 'registered-users') { - return callback(); - } - db.getObjectField('user:' + uid + ':settings', 'groupTitle', function(err, currentTitle) { - if (err || (currentTitle || currentTitle === '')) { - return callback(err); - } - - User.setSetting(uid, 'groupTitle', groupName, callback); - }); + User.setSetting = function (uid, key, value, callback) { + db.setObjectField('user:' + uid + ':settings', key, value, callback); }; }; diff --git a/src/user/topics.js b/src/user/topics.js new file mode 100644 index 0000000000..53dade36e9 --- /dev/null +++ b/src/user/topics.js @@ -0,0 +1,19 @@ +'use strict'; + +var async = require('async'); +var db = require('../database'); + +module.exports = function (User) { + + User.getIgnoredTids = function (uid, start, stop, callback) { + db.getSortedSetRevRange('uid:' + uid + ':ignored_tids', start, stop, callback); + }; + + User.addTopicIdToUser = function (uid, tid, timestamp, callback) { + async.parallel([ + async.apply(db.sortedSetAdd, 'uid:' + uid + ':topics', timestamp, tid), + async.apply(User.incrementUserFieldBy, uid, 'topiccount', 1) + ], callback); + }; + +}; \ No newline at end of file diff --git a/src/views/400.tpl b/src/views/400.tpl new file mode 100644 index 0000000000..9c263fcff1 --- /dev/null +++ b/src/views/400.tpl @@ -0,0 +1,4 @@ +<div class="alert alert-danger"> + <strong>[[global:400.title]]</strong> + <p>[[global:400.message, {config.relative_path}]]</p> +</div> diff --git a/src/views/500-embed.tpl b/src/views/500-embed.tpl index 537cbac136..9d911e9848 100644 --- a/src/views/500-embed.tpl +++ b/src/views/500-embed.tpl @@ -1,9 +1,8 @@ <script type="text/tpl" data-template="500"> -<div class="alert alert-danger"> - <strong>[[global:500.title]]</strong> - <p>[[global:500.message]]</p> - <p>{path}</p> - <!-- IF error --><p>{error}</p><!-- ENDIF error --> - -</div> +<div class="alert alert-danger"> +	<strong>[[global:500.title]]</strong> +	<p>[[global:500.message]]</p> +	<p>{path}</p> +	<!-- IF error --><p>{error}</p><!-- ENDIF error --> +</div> </script> \ No newline at end of file diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl new file mode 100644 index 0000000000..bd01bf99e0 --- /dev/null +++ b/src/views/admin/advanced/cache.tpl @@ -0,0 +1,66 @@ + +<div class="post-cache"> + <div class="col-lg-9"> + <div class="panel panel-default"> + <div class="panel-heading"><i class="fa fa-calendar-o"></i> Post Cache</div> + <div class="panel-body"> + + <label>Posts in Cache</label><br/> + <span>{postCache.itemCount}</span><br/> + + <label>Average Post Size</label><br/> + <span>{postCache.avgPostSize}</span><br/> + + <label>Length / Max</label><br/> + <span>{postCache.length} / {postCache.max}</span><br/> + + <div class="progress"> + <div class="progress-bar" role="progressbar" aria-valuenow="{postCache.percentFull}" aria-valuemin="0" aria-valuemax="100" style="width: {postCache.percentFull}%;"> + {postCache.percentFull}% Full + </div> + </div> + <div class="form-group"> + <label for="postCacheSize">Post Cache Size</label> + <input id="postCacheSize" type="text" class="form-control" value="" data-field="postCacheSize"> + </div> + </div> + </div> + + <div class="panel panel-default"> + <div class="panel-heading"><i class="fa fa-calendar-o"></i> Group Cache</div> + <div class="panel-body"> + + <label>Items in Cache</label><br/> + <span>{groupCache.itemCount}</span><br/> + + <label>Length / Max</label><br/> + <span>{groupCache.length} / {groupCache.max}</span><br/> + + <div class="progress"> + <div class="progress-bar" role="progressbar" aria-valuenow="{groupCache.percentFull}" aria-valuemin="0" aria-valuemax="100" style="width: {groupCache.percentFull}%;"> + {groupCache.percentFull}% Full + </div> + </div> + + <!-- IF groupCache.dump --> + <pre>{groupCache.dump}</pre> + <!-- ENDIF groupCache.dump --> + + </div> + </div> + </div> + <div class="col-lg-3 acp-sidebar"> + <div class="panel panel-default"> + <div class="panel-heading">Control Panel</div> + <div class="panel-body"> + <button class="btn btn-primary" id="save">Update Cache Settings</button> + </div> + </div> + </div> +</div> + +<script> + require(['admin/settings'], function(Settings) { + Settings.prepare(); + }); +</script> \ No newline at end of file diff --git a/src/views/admin/advanced/errors.tpl b/src/views/admin/advanced/errors.tpl new file mode 100644 index 0000000000..d3c23e78b7 --- /dev/null +++ b/src/views/admin/advanced/errors.tpl @@ -0,0 +1,66 @@ +<div class="row"> + <div class="col-lg-9"> + <div class="row"> + <div class="col-sm-6 text-center"> + <div class="panel panel-default"> + <div class="panel-body"> + <div><canvas id="not-found" height="250"></canvas></div> + </div> + <div class="panel-footer"><small><strong>Figure 1</strong> – <code>404 Not Found</code> events per day</small></div> + </div> + </div> + <div class="col-sm-6 text-center"> + <div class="panel panel-default"> + <div class="panel-body"> + <div><canvas id="toobusy" height="250"></canvas></div> + </div> + <div class="panel-footer"><small><strong>Figure 2</strong> – <code>503 Service Unavailable</code> events per day</small></div> + </div> + </div> + </div> + </div> + <div class="col-lg-3 acp-sidebar"> + <div class="panel panel-default"> + <div class="panel-heading">Manage Error Log</div> + <div class="panel-body"> + <div class="btn-group-vertical btn-block" role="group"> + <a class="btn btn-info" target="_top" href="{config.relative_path}/admin/advanced/errors/export"><i class="fa fa-download"></i> Export Error Log (CSV)</a> + <button class="btn btn-danger" data-action="clear"><i class="fa fa-trash"></i> Clear Error Log</button> + </div> + </div> + </div> + </div> +</div> + +<div class="row"> + <div class="col-xs-12"> + <div class="panel panel-default"> + <div class="panel-heading"><i class="fa fa-exclamation-triangle"></i> 404 Not Found</div> + <div class="panel-body"> + <table class="table table-striped"> + <thead> + <th>Route</th> + <th>Count</th> + </thead> + <tbody> + <!-- BEGIN not-found --> + <tr> + <td>{../value}</td> + <td>{../score}</td> + </tr> + <!-- END not-found --> + <!-- IF !not-found.length --> + <tr> + <td colspan="2"> + <div class="alert alert-success"> + Hooray! There are no routes that were not found. + </div> + </td> + </tr> + <!-- ENDIF !not-found.length --> + </tbody> + </table> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/views/admin/advanced/events.tpl b/src/views/admin/advanced/events.tpl index f69d7928a5..f1c0973501 100644 --- a/src/views/admin/advanced/events.tpl +++ b/src/views/admin/advanced/events.tpl @@ -23,6 +23,7 @@ <pre>{events.jsonString}</pre> </div> <!-- END events --> + <!-- IMPORT partials/paginator.tpl --> </div> </div> </div> diff --git a/src/views/admin/advanced/post-cache.tpl b/src/views/admin/advanced/post-cache.tpl deleted file mode 100644 index 7f688327e4..0000000000 --- a/src/views/admin/advanced/post-cache.tpl +++ /dev/null @@ -1,27 +0,0 @@ - -<div class="post-cache"> - <div class="col-lg-9"> - <div class="panel panel-default"> - <div class="panel-heading"><i class="fa fa-calendar-o"></i> Post Cache</div> - <div class="panel-body" data-next="{next}"> - - <label>Posts in Cache</label><br/> - <span>{cache.itemCount}</span><br/> - - <label>Average Post Size</label><br/> - <span>{cache.avgPostSize}</span><br/> - - <label>Length / Max</label><br/> - <span>{cache.length} / {cache.max}</span><br/> - - <div class="progress"> - <div class="progress-bar" role="progressbar" aria-valuenow="{cache.percentFull}" aria-valuemin="0" aria-valuemax="100" style="width: {cache.percentFull}%;"> - {cache.percentFull}% Full - </div> - </div> - - </div> - </div> - </div> - -</div> diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index ac6781c66b..07c51bcb73 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -1,7 +1,12 @@ <ul class="nav nav-pills"> - <li class="active"><a href="#installed" data-toggle="tab">Installed Plugins</a></li> - <li><a href="#download" data-toggle="tab">Download Plugins</a></li> - <li><a href="#upgrade" data-toggle="tab">Upgradable Plugins</a></li> + <li class="active"><a href="#installed" data-toggle="tab">Installed</a></li> + <li><a href="#active" data-toggle="tab">Active</a></li> + <li><a href="#deactive" data-toggle="tab">Inactive</a></li> + <li><a href="#upgrade" data-toggle="tab"> + Out of Date + <span class="badge">{upgradeCount}</span> + </a></li> + <li><a href="#download" data-toggle="tab">Find Plugins</a></li> </ul> <br /> @@ -15,6 +20,15 @@ <!-- END installed --> </ul> </div> + <div class="tab-pane fade" id="active"> + <ul class="active"></ul> + </div> + <div class="tab-pane fade" id="deactive"> + <ul class="deactive"></ul> + </div> + <div class="tab-pane fade" id="upgrade"> + <ul class="upgrade"></ul> + </div> <div class="tab-pane fade" id="download"> <ul class="download"> <!-- BEGIN download --> @@ -22,9 +36,6 @@ <!-- END download --> </ul> </div> - <div class="tab-pane fade" id="upgrade"> - <ul class="upgrade"></ul> - </div> </div> </div> diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index 6ea841352a..4f7775b226 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -95,11 +95,10 @@ <div class="panel-heading">System Control</div> <div class="panel-body text-center"> <p> - <button class="btn btn-warning reload" data-placement="bottom" data-toggle="tooltip" title="Reload NodeBB to activate new plugins">Reload</button> - <button class="btn btn-danger restart" data-placement="bottom" data-toggle="tooltip" title="Restarting NodeBB will drop all existing connections for a few seconds">Restart</button> + <button class="btn btn-danger btn-block restart" data-placement="bottom" data-toggle="tooltip" title="Restarting NodeBB will drop all existing connections for a few seconds">Restart</button> </p> <p> - <a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info" data-placement="bottom" data-toggle="tooltip" title="Click here to set up maintenance mode for NodeBB">Maintenance Mode</a> + <a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block" data-placement="bottom" data-toggle="tooltip" title="Click here to set up maintenance mode for NodeBB">Maintenance Mode</a> </p> <hr /> @@ -107,6 +106,13 @@ </div> </div> + <div class="panel panel-default"> + <div class="panel-heading">Active Users</div> + <div class="panel-body"> + <div id="active-users"></div> + </div> + </div> + <div class="panel panel-default"> <div class="panel-heading">Anonymous vs Registered Users</div> <div class="panel-body"> @@ -146,13 +152,5 @@ </div> </div> - - - <div class="panel panel-default"> - <div class="panel-heading">Active Users</div> - <div class="panel-body"> - <div id="active-users"></div> - </div> - </div> </div> </div> \ No newline at end of file diff --git a/src/views/admin/general/sounds.tpl b/src/views/admin/general/sounds.tpl index dfdbae512d..1154f4ec75 100644 --- a/src/views/admin/general/sounds.tpl +++ b/src/views/admin/general/sounds.tpl @@ -1,5 +1,5 @@ <div class="sounds settings" class="row"> - <div class="col-xs-9"> + <div class="col-xs-12"> <form role="form"> <div class="row"> <div class="col-sm-2 col-xs-12 settings-header">Notifications</div> @@ -8,14 +8,14 @@ <div class="row"> <div class="form-group col-xs-9"> <select class="form-control" id="notification" name="notification"> - <option value=""></option> + <option value="">[[user:no-sound]]</option> <!-- BEGIN sounds --> <option value="{sounds.name}">{sounds.name}</option> <!-- END sounds --> </select> </div> <div class="btn-group col-xs-3"> - <button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button> + <button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button> </div> </div> </div> @@ -28,14 +28,14 @@ <div class="row"> <div class="form-group col-xs-9"> <select class="form-control" id="chat-incoming" name="chat-incoming"> - <option value=""></option> + <option value="">[[user:no-sound]]</option> <!-- BEGIN sounds --> <option value="{sounds.name}">{sounds.name}</option> <!-- END sounds --> </select> </div> <div class="btn-group col-xs-3"> - <button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button> + <button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button> </div> </div> @@ -43,40 +43,29 @@ <div class="row"> <div class="form-group col-xs-9"> <select class="form-control" id="chat-outgoing" name="chat-outgoing"> - <option value=""></option> + <option value="">[[user:no-sound]]</option> <!-- BEGIN sounds --> <option value="{sounds.name}">{sounds.name}</option> <!-- END sounds --> </select> </div> <div class="btn-group col-xs-3"> - <button type="button" class="form-control btn btn-sm btn-default" data-action="play">Play <i class="fa fa-play"></i></button> + <button type="button" class="form-control btn btn-sm btn-default" data-action="play"><span class="hidden-xs">Play </span><i class="fa fa-play"></i></button> </div> </div> + + <div class="input-group"> + <span class="input-group-btn"> + <input data-action="upload" data-title="Upload Sound" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary" value="Upload New Sound"></input> + </span> + </div> </div> </div> </form> </div> - <div class="col-xs-3"> - <div class="panel"> - <div class="panel-body"> - <div class="input-group"> - <span class="input-group-btn"> - <input data-action="upload" data-title="Upload Sound" data-route="{config.relative_path}/api/admin/upload/sound" type="button" class="btn btn-primary btn-block" value="Upload New Sound"></input> - </span> - </div> - </div> - </div> - </div> </div> <button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored"> <i class="material-icons">save</i> -</button> - -<script> - require(['admin/settings'], function(Settings) { - Settings.init(); - }); -</script> \ No newline at end of file +</button> \ No newline at end of file diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl index 19b3b065cb..ccf93afa3d 100644 --- a/src/views/admin/header.tpl +++ b/src/views/admin/header.tpl @@ -4,7 +4,6 @@ <title>{title}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="stylesheet" href="{relative_path}/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css?{cache-buster}"> <link rel="stylesheet" type="text/css" href="{relative_path}/admin.css?{cache-buster}" /> <link rel="stylesheet" type="text/css" href="{relative_path}/vendor/mdl/mdl.min.css?{cache-buster}" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> @@ -15,7 +14,9 @@ var app = { template: "{template.name}", user: JSON.parse('{{userJSON}}'), - config: JSON.parse(decodeURIComponent("{{adminConfigJSON}}")) + config: JSON.parse(decodeURIComponent("{{adminConfigJSON}}")), + flags: {}, + inAdmin: true }; </script> @@ -27,43 +28,23 @@ <![endif]--> <script src="https://storage.googleapis.com/code.getmdl.io/1.0.3/material.min.js"></script> - <script type="text/javascript" src="{relative_path}/vendor/chart.js/chart.min.js?{cache-buster}"></script> - <script type="text/javascript" src="{relative_path}/vendor/hammer/hammer.min.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/jquery/sortable/Sortable.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/acp.min.js?{cache-buster}"></script> - <script> - require.config({ - baseUrl: "{relative_path}/src/modules", - waitSeconds: 3, - urlArgs: "{cache-buster}", - paths: { - 'forum': '../client', - 'admin': '../admin', - 'vendor': '../../vendor', - 'buzz': '../../vendor/buzz/buzz.min' - } - }); - - app.inAdmin = true; - </script> <script type="text/javascript" src="{relative_path}/vendor/colorpicker/colorpicker.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/src/admin/admin.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/ace/ace.js?{cache-buster}"></script> - <script type="text/javascript" src="{relative_path}/vendor/jquery/event/jquery.event.drag.js?{cache-buster}"></script> - <script type="text/javascript" src="{relative_path}/vendor/jquery/event/jquery.event.drop.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/semver/semver.browser.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/jquery/deserialize/jquery.deserialize.min.js?{cache-buster}"></script> - <script type="text/javascript" src="{relative_path}/vendor/mousetrap/mousetrap.js?{cache-buster}"></script> - <script type="text/javascript" src="{relative_path}/vendor/jquery/js/jquery-ui-1.10.4.custom.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/snackbar/snackbar.min.js?{cache-buster}"></script> <script type="text/javascript" src="{relative_path}/vendor/slideout/slideout.min.js?{cache-buster}"></script> + <script type="text/javascript" src="{relative_path}/vendor/nprogress.min.js?{cache-buster}"></script> <!-- BEGIN scripts --> <script type="text/javascript" src="{scripts.src}"></script> <!-- END scripts --> </head> - <body class="admin"> + <body class="admin {bodyClass}"> <!-- IMPORT admin/partials/menu.tpl --> <div class="container" id="content"> \ No newline at end of file diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl index d5807ce88e..a14348ba42 100644 --- a/src/views/admin/manage/flags.tpl +++ b/src/views/admin/manage/flags.tpl @@ -1,34 +1,64 @@ <div class="flags"> - <div class="col-lg-9"> - <div data-next="{next}"> - <form id="flag-search" method="GET" action="flags"> - <div class="form-group"> - <div class="row"> - <div class="col-md-6"> - <label>Flags by user</label> - <input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}"> - </div> + + <div class="col-lg-12"> + + <div class="text-center"> + <div class="panel panel-default"> + <div class="panel-body"> + <div><canvas id="flags:daily" height="250"></canvas></div> + <p> + + </p> + </div> + <div class="panel-footer"><small>Daily flags</small></div> + </div> + </div> + + <form id="flag-search" method="GET" action="flags"> + <div class="form-group"> + <div> + <div> + <label>Flags by user</label> + <input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}"> </div> </div> + </div> - <div class="form-group"> - <label>Sort By</label> - <div class="row"> - <div class="col-md-6"> - <select id="flag-sort-by" class="form-control" name="sortBy"> - <option value="count">Most Flags</option> - <option value="time">Most Recent</option> - </select> - </div> + <div class="form-group"> + <div> + <div> + <label>Category</label> + <select class="form-control" id="category-selector" name="cid"> + <option value="">[[unread:all_categories]]</option> + <!-- BEGIN categories --> + <option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>{categories.text}</option> + <!-- END categories --> + </select> </div> </div> + </div> + + <div class="form-group"> + <label>Sort By</label> + <div> + <div> + <select id="flag-sort-by" class="form-control" name="sortBy"> + <option value="count" <!-- IF sortByCount -->selected<!-- ENDIF sortByCount -->>Most Flags</option> + <option value="time" <!-- IF sortByTime -->selected<!-- ENDIF sortByTime -->>Most Recent</option> + </select> + </div> + </div> + </div> - <button type="submit" class="btn btn-primary">Search</button> - </form> - <br /> - <hr/> + <button type="submit" class="btn btn-primary">Search</button> + <button class="btn btn-primary" id="dismissAll">Dismiss All</button> + </form> - <div class="post-container" data-next="{next}"> + <hr/> + + <div data-next="{next}"> + + <div component="posts/flags" class="panel-group post-container" id="accordion" role="tablist" aria-multiselectable="true" data-next="{next}"> <!-- IF !posts.length --> <div class="alert alert-success"> No flagged posts! @@ -36,70 +66,130 @@ <!-- ENDIF !posts.length --> <!-- BEGIN posts --> - <div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}"> - <div class="col-sm-8"> - <div class="panel panel-default"> - <div class="panel-body flag-post-body"> - <a href="{config.relative_path}/user/{../user.userslug}"> - <!-- IF ../user.picture --> - <img title="{posts.user.username}" src="{../user.picture}"> + <div class="panel panel-default" component="posts/flag" data-pid="{../pid}"> + <div class="panel-heading" role="tab"> + <h4 class="panel-title"> + <a role="button" data-toggle="collapse" data-parent="#accordion" href="#flag-pid-{posts.pid}" aria-expanded="true" aria-controls="flag-pid-{posts.pid}"> + <!-- IF ../flagData.assignee --> + <div class="pull-right"> + <!-- IF ../flagData.assigneeUser.picture --> + <img class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" src="{../flagData.assigneeUser.picture}"> <!-- ELSE --> - <div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div> - <!-- ENDIF ../user.picture --> - </a> + <div class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" style="background-color: {../flagData.assigneeUser.icon:bgColor};">{../flagData.assigneeUser.icon:text}</div> + <!-- ENDIF ../flagData.assigneeUser.picture --> + </div> + <!-- ENDIF ../flagData.assignee --> + <span class="label <!-- IF ../flagData.labelClass -->label-{../flagData.labelClass}<!-- ELSE -->label-info<!-- ENDIF ../flagData.labelClass -->">[[topic:flag_manage_state_<!-- IF ../flagData.state -->{../flagData.state}<!-- ELSE -->open<!-- ENDIF ../flagData.state -->]]</span> + [[topic:flag_manage_title, {posts.category.name}]] + <small><span class="timeago" title="{posts.timestampISO}"></span></small> + </a> + </h4> + </div> + <div id="flag-pid-{posts.pid}" class="panel-collapse collapse" role="tabpanel"> + <div class="panel-body"> + <div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}"> + <div class="col-sm-8"> + <div class="well flag-post-body"> + <a href="{config.relative_path}/user/{../user.userslug}"> + <!-- IF ../user.picture --> + <img title="{posts.user.username}" src="{../user.picture}"> + <!-- ELSE --> + <div title="{posts.user.username}" class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div> + <!-- ENDIF ../user.picture --> + </a> - <a href="{config.relative_path}/user/{../user.userslug}"> - <strong><span>{../user.username}</span></strong> - </a> - <div class="content"> - <p>{posts.content}</p> - <p class="fade-out"></p> + <a href="{config.relative_path}/user/{../user.userslug}"> + <strong><span>{../user.username}</span></strong> + </a> + <div class="content"> + <p>{posts.content}</p> + </div> + <small> + <span class="pull-right"> + Posted in <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>, <span class="timeago" title="{posts.timestampISO}"></span> • + <a href="{config.relative_path}/post/{posts.pid}" target="_blank">Read More</a> + </span> + </small> + </div> + </div> + <div class="col-sm-4"> + <i class="fa fa-flag"></i> This post has been flagged {posts.flags} time(s): + <blockquote class="flag-reporters"> + <ul> + <!-- BEGIN posts.flagReasons --> + <li> + <a target="_blank" href="{config.relative_path}/user/{../user.userslug}"> + <!-- IF ../user.picture --> + <img src="{../user.picture}" /> + <!-- ELSE --> + <div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div> + <!-- ENDIF ../user.picture --> + {../user.username} + </a>: "{posts.flagReasons.reason}" + </li> + <!-- END posts.flagReasons --> + </ul> + </blockquote> + <div class="btn-group"> + <button class="btn btn-sm btn-success dismiss">Dismiss this Flag</button> + <button class="btn btn-sm btn-danger delete">Delete the Post</button> + </div> + </div> + </div> + <hr /> + <div class="row"> + <div class="col-sm-6"> + <form role="form"> + <div class="form-group"> + <label for="{posts.pid}-assignee">[[topic:flag_manage_assignee]]</label> + <select class="form-control" id="{posts.pid}-assignee" name="assignee"> + <!-- BEGIN assignees --> + <option value="{assignees.uid}">{assignees.username}</option> + <!-- END assignees --> + </select> + </div> + <div class="form-group"> + <label for="{posts.pid}-state">[[topic:flag_manage_state]]</label> + <select class="form-control" id="{posts.pid}-state" name="state"> + <option value="open">[[topic:flag_manage_state_open]]</option> + <option value="wip">[[topic:flag_manage_state_wip]]</option> + <option value="resolved">[[topic:flag_manage_state_resolved]]</option> + <option value="rejected">[[topic:flag_manage_state_rejected]]</option> + </select> + </div> + <div class="form-group"> + <label for="{posts.pid}-notes">[[topic:flag_manage_notes]]</label> + <textarea class="form-control" id="{posts.pid}-notes" name="notes"></textarea> + </div> + <button type="button" component="posts/flag/update" class="btn btn-sm btn-primary btn-block">[[topic:flag_manage_update]]</button> + </form> + </div> + <div class="col-sm-6"> + <h5>[[topic:flag_manage_history]]</h5> + <!-- IF !posts.flagData.history.length --> + <div class="alert alert-info">[[topic:flag_manage_no_history]]</div> + <!-- ELSE --> + <ul class="list-group" component="posts/flag/history"> + <!-- BEGIN posts.flagData.history --> + <li class="list-group-item"> + <div class="pull-right"><small><span class="timeago" title="{posts.flagData.history.timestampISO}"></span></small></div> + <!-- IF ../user.picture --> + <img class="avatar avatar-sm avatar-rounded" src="{../user.picture}" title="{../user.username}" /> + <!-- ELSE --> + <div class="avatar avatar-sm avatar-rounded" style="background-color: {../user.icon:bgColor};" title="{../user.username}">{../user.icon:text}</div> + <!-- ENDIF ../user.picture --> + [[topic:flag_manage_history_{posts.flagData.history.type}, {posts.flagData.history.label}]] + </li> + <!-- END posts.flagData.history --> + </ul> + <!-- ENDIF !posts.flagData.history.length --> </div> - <small> - <span class="pull-right"> - Posted in <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>, <span class="timeago" title="{posts.timestampISO}"></span> • - <a href="{config.relative_path}/topic/{posts.topic.slug}/{posts.index}" target="_blank">Read More</a> - </span> - </small> </div> - </div> - </div> - <div class="col-sm-4"> - <i class="fa fa-flag"></i> This post has been flagged {posts.flags} time(s): - <blockquote class="flag-reporters"> - <ul> - <!-- BEGIN posts.flagReasons --> - <li> - <a target="_blank" href="{config.relative_path}/user/{../user.userslug}"> - <!-- IF ../user.picture --> - <img src="{../user.picture}" /> - <!-- ELSE --> - <div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div> - <!-- ENDIF ../user.picture --> - {../user.username} - </a>: "{../reason}" - </li> - <!-- END posts.flagReasons --> - </ul> - </blockquote> - <div class="btn-group"> - <button class="btn btn-sm btn-success dismiss">Dismiss</button> - <button class="btn btn-sm btn-danger delete">Delete</button> </div> </div> </div> <!-- END posts --> - </div> - </div> - </div> - - <div class="col-lg-3 acp-sidebar"> - <div class="panel panel-default"> - <div class="panel-heading">Flags Control Panel</div> - <div class="panel-body"> - <div> - <button class="btn btn-primary" id="dismissAll">Dismiss All</button> - </div> + <!-- IMPORT partials/paginator.tpl --> </div> </div> </div> diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl index d293d8cfa4..486686996d 100644 --- a/src/views/admin/manage/registration.tpl +++ b/src/views/admin/manage/registration.tpl @@ -5,7 +5,7 @@ <!-- IF !users.length --> <p class="panel-body"> There are no users in the registration queue. <br> - To enable this feature, go to <a href="{config.relative_path}/admin/settings/user">Settings -> User -> Authentication</a> and set + To enable this feature, go to <a href="{config.relative_path}/admin/settings/user">Settings → User → Authentication</a> and set <strong>Registration Type</strong> to "Admin Approval". </p> <!-- ENDIF !users.length --> @@ -13,8 +13,11 @@ <tr> <th>Name</th> <th>Email</th> - <th>IP</th> - <th>Time</th> + <th class="hidden-xs">IP</th> + <th class="hidden-xs">Time</th> + <!-- BEGIN customHeaders --> + <th class="hidden-xs">{customHeaders.label}</th> + <!-- END customHeaders --> <th></th> </tr> <!-- BEGIN users --> @@ -35,17 +38,31 @@ <!-- ENDIF users.emailSpam --> {users.email} </td> - <td> + <td class="hidden-xs"> <!-- IF users.ipSpam --> <i class="fa fa-times-circle text-danger" title="Frequency: {users.spamData.ip.frequency} Appears: {users.spamData.ip.appears}"></i> <!-- ELSE --> <i class="fa fa-check text-success"></i> <!-- ENDIF users.ipSpam --> {users.ip} + <!-- BEGIN users.ipMatch --> + <br> + <!-- IF users.ipMatch.picture --> + <img src="{users.ipMatch.picture}" class="user-img"/> + <!-- ELSE --> + <div class="user-img avatar avatar-sm" style="background-color: {users.ipMatch.icon:bgColor};">{users.ipMatch.icon:text}</div> + <!-- ENDIF users.ipMatch.picture --> + <a href="/uid/{users.ipMatch.uid}">{users.ipMatch.username}</a> + <!-- END users.ipMatch --> </td> - <td> + <td class="hidden-xs"> <span class="timeago" title="{users.timestampISO}"></span> </td> + + <!-- BEGIN users.customRows --> + <td class="hidden-xs">{users.customRows.value}</td> + <!-- END users.customRows --> + <td> <div class="btn-group pull-right"> <button class="btn btn-success btn-xs" data-action="accept"><i class="fa fa-check"></i></button> diff --git a/src/views/admin/manage/tags.tpl b/src/views/admin/manage/tags.tpl index bc47f93731..d60cdc4147 100644 --- a/src/views/admin/manage/tags.tpl +++ b/src/views/admin/manage/tags.tpl @@ -6,7 +6,7 @@ <!-- IF !tags.length --> Your forum does not have any topics with tags yet. <!-- ENDIF !tags.length --> - + <div class="tag-list"> <!-- BEGIN tags --> <div class="tag-row" data-tag="{tags.value}"> @@ -32,9 +32,10 @@ <div class="col-lg-3 acp-sidebar"> <div class="panel panel-default"> - <div class="panel-heading">Modify Tag</div> + <div class="panel-heading">Create & Modify Tags</div> <div class="panel-body"> <p>Select tags via clicking and/or dragging, use shift to select multiple.</p> + <button class="btn btn-primary btn-block" id="create">Create Tag</button> <button class="btn btn-primary btn-block" id="modify">Modify Tags</button> <button class="btn btn-warning btn-block" id="deleteSelected">Delete Tags</button> </div> @@ -48,4 +49,26 @@ </div> </div> + <div class="modal fade" id="create-modal"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">Create Tag</h4> + </div> + <div class="modal-body"> + <form> + <div class="form-group"> + <label for="create-tag-name">Tag Name</label> + <input type="text" class="form-control" id="create-tag-name" placeholder="Tag Name" /> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" id="create-modal-go">Create</button> + </div> + </div> + </div> + </div> + </div> diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index cfdc7bb72c..aa74b72ed5 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -1,5 +1,6 @@ <div class="manage-users"> - <div class="col-lg-9"> + + <div class="col-lg-12"> <div class="panel panel-default"> <div class="panel-heading"><i class="fa fa-user"></i> Users</div> <div class="panel-body"> @@ -8,10 +9,10 @@ <li><a href='{config.relative_path}/admin/manage/users/not-validated'>Not validated</a></li> <li><a href='{config.relative_path}/admin/manage/users/no-posts'>No Posts</a></li> <li><a href='{config.relative_path}/admin/manage/users/inactive'>Inactive</a></li> + <li><a href='{config.relative_path}/admin/manage/users/flagged'>Most Flags</a></li> <li><a href='{config.relative_path}/admin/manage/users/banned'>Banned</a></li> <li><a href='{config.relative_path}/admin/manage/users/search'>User Search</a></li> - <div class="btn-group pull-right"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">Edit <span class="caret"></span></button> <ul class="dropdown-menu"> @@ -22,14 +23,26 @@ <li><a href="#" class="send-validation-email"><i class="fa fa-fw fa-mail-forward"></i> Send Validation Email</a></li> <li><a href="#" class="password-reset-email"><i class="fa fa-fw fa-key"></i> Send Password Reset Email</a></li> <li class="divider"></li> - <li><a href="#" class="ban-user"><i class="fa fa-fw fa-gavel"></i> Ban User</a></li> - <li><a href="#" class="unban-user"><i class="fa fa-fw fa-comment-o"></i> Unban User</a></li> + <li><a href="#" class="ban-user"><i class="fa fa-fw fa-gavel"></i> Ban User(s)</a></li> + <li><a href="#" class="ban-user-temporary"><i class="fa fa-fw fa-clock-o"></i> Ban User(s) Temporarily</a></li> + <li><a href="#" class="unban-user"><i class="fa fa-fw fa-comment-o"></i> Unban User(s)</a></li> <li><a href="#" class="reset-lockout"><i class="fa fa-fw fa-unlock"></i> Reset Lockout</a></li> <li><a href="#" class="reset-flags"><i class="fa fa-fw fa-flag"></i> Reset Flags</a></li> <li class="divider"></li> - <li><a href="#" class="delete-user"><i class="fa fa-fw fa-trash-o"></i> Delete User</a></li> + <li><a href="#" class="delete-user"><i class="fa fa-fw fa-trash-o"></i> Delete User(s)</a></li> + <li><a href="#" class="delete-user-and-content"><i class="fa fa-fw fa-trash-o"></i> Delete User(s) and Content</a></li> </ul> </div> + + <a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary pull-right">Download CSV</a> + + <!-- IF inviteOnly --> + <!-- IF loggedIn --> + <button component="user/invite" class="btn btn-success form-control"><i class="fa fa-users"></i> Invite</button> + <!-- ENDIF loggedIn --> + <!-- ENDIF inviteOnly --> + + <button id="createUser" class="btn btn-primary pull-right">New User</button> </ul> <br /> @@ -47,100 +60,48 @@ <i class="fa fa-spinner fa-spin hidden"></i> <span id="user-notfound-notify" class="label label-danger hide">User not found!</span><br/> </div> + <!-- IF inactive --> <a href="{config.relative_path}/admin/manage/users/inactive?months=3" class="btn btn-default">3 months</a> <a href="{config.relative_path}/admin/manage/users/inactive?months=6" class="btn btn-default">6 months</a> <a href="{config.relative_path}/admin/manage/users/inactive?months=12" class="btn btn-default">12 months</a> <!-- ENDIF inactive --> - - <ul id="users-container"> + <table class="table table-striped users-table"> + <tr> + <th><input component="user/select/all" type="checkbox"/></th> + <th>uid</th> + <th>username</th> + <th>email</th> + <th class="text-right">postcount</th> + <th class="text-right">reputation</th> + <th class="text-right">flags</th> + <th>joined</th> + <th>last online</th> + <th>banned</th> + </tr> <!-- BEGIN users --> - <div class="users-box" data-uid="{users.uid}" data-username="{users.username}"> - <div class="user-image"> - <!-- IF users.picture --> - <img src="{users.picture}" class="img-thumbnail user-selectable"/> - <!-- ELSE --> - <div class="user-icon user-selectable" style="background-color: {users.icon:bgColor};">{users.icon:text}</div> - <!-- ENDIF users.picture --> - <div class="labels"> - <!-- IF config.requireEmailConfirmation --> - <!-- IF !users.email:confirmed --> - <span class="notvalidated label label-danger">Not Validated</span> - <!-- ENDIF !users.email:confirmed --> - <!-- ENDIF config.requireEmailConfirmation --> - <span class="administrator label label-primary <!-- IF !users.administrator -->hide<!-- ENDIF !users.administrator -->">Admin</span> - <span class="ban label label-danger <!-- IF !users.banned -->hide<!-- ENDIF !users.banned -->">Banned</span> - </div> - </div> - - <a href="{config.relative_path}/user/{users.userslug}" target="_blank">{users.username} ({users.uid})</a><br/> - <!-- IF users.email --> - <small><span title="{users.email}">{users.email}</span></small> - <!-- ENDIF users.email --> - - joined <span class="timeago" title="{users.joindateISO}"></span><br/> - login <span class="timeago" title="{users.lastonlineISO}"></span><br/> - posts {users.postcount} - - <!-- IF users.flags --> - <div><small><span><i class="fa fa-flag"></i> {users.flags}</span></small></div> - <!-- ENDIF users.flags --> - </div> + <tr class="user-row"> + <th><input component="user/select/single" data-uid="{users.uid}" type="checkbox"/></th> + <td class="text-right">{users.uid}</td> + <td><i class="administrator fa fa-shield text-success<!-- IF !users.administrator --> hidden<!-- ENDIF !users.administrator -->"></i><a href="{config.relative_path}/user/{users.userslug}"> {users.username}</a></td> + + <td> + <!-- IF config.requireEmailConfirmation --> + <i class="validated fa fa-check text-success<!-- IF !users.email:confirmed --> hidden<!-- ENDIF !users.email:confirmed -->" title="validated"></i> + <i class="notvalidated fa fa-times text-danger<!-- IF users.email:confirmed --> hidden<!-- ENDIF users.email:confirmed -->" title="not validated"></i> + <!-- ENDIF config.requireEmailConfirmation --> {users.email}</td> + <td class="text-right">{users.postcount}</td> + <td class="text-right">{users.reputation}</td> + <td class="text-right"><!-- IF users.flags -->{users.flags}<!-- ELSE -->0<!-- ENDIF users.flags --></td> + <td><span class="timeago" title="{users.joindateISO}"></span></td> + <td><span class="timeago" title="{users.lastonlineISO}"></span></td> + <td class="text-center"><i class="ban fa fa-gavel text-danger<!-- IF !users.banned --> hidden<!-- ENDIF !users.banned -->"></i></td> + </tr> <!-- END users --> - </ul> + </table> <!-- IMPORT partials/paginator.tpl --> - - <div class="modal fade" id="create-modal"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">Create User</h4> - </div> - <div class="modal-body"> - <div class="alert alert-danger hide" id="create-modal-error"></div> - <form> - <div class="form-group"> - <label for="group-name">User Name</label> - <input type="text" class="form-control" id="create-user-name" placeholder="User Name" /> - </div> - <div class="form-group"> - <label for="group-name">Email</label> - <input type="text" class="form-control" id="create-user-email" placeholder="Email of this user" /> - </div> - - <div class="form-group"> - <label for="group-name">Password</label> - <input type="password" class="form-control" id="create-user-password" placeholder="Password" /> - </div> - - <div class="form-group"> - <label for="group-name">Password Confirm</label> - <input type="password" class="form-control" id="create-user-password-again" placeholder="Password" /> - </div> - - </form> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-primary" id="create-modal-go">Create</button> - </div> - </div> - </div> - </div> - - </div> - </div> - </div> - - <div class="col-lg-3 acp-sidebar"> - <div class="panel panel-default"> - <div class="panel-heading">Users Control Panel</div> - <div class="panel-body"> - <button id="createUser" class="btn btn-primary">New User</button> - <a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary">Download CSV</a> </div> </div> </div> diff --git a/src/views/admin/partials/categories/groups.tpl b/src/views/admin/partials/categories/groups.tpl index aa2da68cfc..495aa9589f 100644 --- a/src/views/admin/partials/categories/groups.tpl +++ b/src/views/admin/partials/categories/groups.tpl @@ -5,12 +5,16 @@ Privileges <span class="caret"></span> </button> <ul class="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" data-priv="groups:find" class="<!-- IF groups.privileges.groups:find -->active<!-- ENDIF groups.privileges.groups:find -->">Find category</a></li> - <li role="presentation"><a href="#" data-priv="groups:read" class="<!-- IF groups.privileges.groups:read -->active<!-- ENDIF groups.privileges.groups:read -->">Access & Read</a></li> + <li role="presentation"><a href="#" data-priv="groups:find" class="<!-- IF groups.privileges.groups:find -->active<!-- ENDIF groups.privileges.groups:find -->">Find Category</a></li> + <li role="presentation"><a href="#" data-priv="groups:read" class="<!-- IF groups.privileges.groups:read -->active<!-- ENDIF groups.privileges.groups:read -->">Access Category</a></li> + <li role="presentation"><a href="#" data-priv="groups:topics:read" class="<!-- IF groups.privileges.groups:topics:read -->active<!-- ENDIF groups.privileges.groups:topics:read -->">Access Topics</a></li> <li role="presentation"><a href="#" data-priv="groups:topics:create" class="<!-- IF groups.privileges.groups:topics:create -->active<!-- ENDIF groups.privileges.groups:topics:create -->">Create Topics</a></li> <li role="presentation"><a href="#" data-priv="groups:topics:reply" class="<!-- IF groups.privileges.groups:topics:reply -->active<!-- ENDIF groups.privileges.groups:topics:reply -->">Reply to Topics</a></li> + <li role="presentation"><a href="#" data-priv="groups:posts:edit" class="<!-- IF groups.privileges.groups:posts:edit -->active<!-- ENDIF groups.privileges.groups:posts:edit -->">Edit Posts</a></li> + <li role="presentation"><a href="#" data-priv="groups:posts:delete" class="<!-- IF groups.privileges.groups:posts:delete -->active<!-- ENDIF groups.privileges.groups:posts:delete -->">Delete Posts</a></li> + <li role="presentation"><a href="#" data-priv="groups:topics:delete" class="<!-- IF groups.privileges.groups:topics:delete -->active<!-- ENDIF groups.privileges.groups:topics:delete -->">Delete Topics</a></li> </ul> </div> {groups.displayName} </li> -<!-- END groups --> \ No newline at end of file +<!-- END groups --> diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index b5605c0935..497fa71ac0 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -58,6 +58,7 @@ <div class="btn-toolbar"> <button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group"> Add Group</button> <button type="button" class="btn btn-info pull-right" data-ajaxify="false" data-action="copyToChildren"> Copy to Children</button> + <button type="button" class="btn btn-info pull-right" data-ajaxify="fakse" data-action="copyPrivilegesFrom"> Copy From Category</button> </div> </td> </tr> diff --git a/src/views/admin/partials/categories/users.tpl b/src/views/admin/partials/categories/users.tpl index 40991fcf3a..772053d5b0 100644 --- a/src/views/admin/partials/categories/users.tpl +++ b/src/views/admin/partials/categories/users.tpl @@ -5,14 +5,18 @@ Privileges <span class="caret"></span> </button> <ul class="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" data-priv="find" class="<!-- IF users.privileges.find -->active<!-- ENDIF users.privileges.find -->">Find category</a></li> - <li role="presentation"><a href="#" data-priv="read" class="<!-- IF users.privileges.read -->active<!-- ENDIF users.privileges.read -->">Access & Read</a></li> + <li role="presentation"><a href="#" data-priv="find" class="<!-- IF users.privileges.find -->active<!-- ENDIF users.privileges.find -->">Find Category</a></li> + <li role="presentation"><a href="#" data-priv="read" class="<!-- IF users.privileges.read -->active<!-- ENDIF users.privileges.read -->">Access Category</a></li> + <li role="presentation"><a href="#" data-priv="topics:read" class="<!-- IF users.privileges.topics:read -->active<!-- ENDIF users.privileges.topics:read -->">Access Topics</a></li> <li role="presentation"><a href="#" data-priv="topics:create" class="<!-- IF users.privileges.topics:create -->active<!-- ENDIF users.privileges.topics:create -->">Create Topics</a></li> <li role="presentation"><a href="#" data-priv="topics:reply" class="<!-- IF users.privileges.topics:reply -->active<!-- ENDIF users.privileges.topics:reply -->">Reply to Topics</a></li> + <li role="presentation"><a href="#" data-priv="posts:edit" class="<!-- IF users.privileges.posts:edit -->active<!-- ENDIF users.privileges.posts:edit -->">Edit Posts</a></li> + <li role="presentation"><a href="#" data-priv="posts:delete" class="<!-- IF users.privileges.posts:delete -->active<!-- ENDIF users.privileges.posts:delete -->">Delete Posts</a></li> + <li role="presentation"><a href="#" data-priv="topics:delete" class="<!-- IF users.privileges.topics:delete -->active<!-- ENDIF users.privileges.topics:delete -->">Delete Topics</a></li> <li role="presentation" class="divider"></li> <li role="presentation"><a href="#" data-priv="mods" class="<!-- IF users.privileges.mods -->active<!-- ENDIF users.privileges.mods -->">Moderator</a></li> </ul> </div> <img src="{users.picture}" /> {users.username} </li> -<!-- END users --> \ No newline at end of file +<!-- END users --> diff --git a/src/views/admin/partials/create_user_modal.tpl b/src/views/admin/partials/create_user_modal.tpl new file mode 100644 index 0000000000..b065479b06 --- /dev/null +++ b/src/views/admin/partials/create_user_modal.tpl @@ -0,0 +1,21 @@ +<div class="alert alert-danger hide" id="create-modal-error"></div> +<form> + <div class="form-group"> + <label for="group-name">User Name</label> + <input type="text" class="form-control" id="create-user-name" placeholder="User Name" /> + </div> + <div class="form-group"> + <label for="group-name">Email</label> + <input type="text" class="form-control" id="create-user-email" placeholder="Email of this user" /> + </div> + + <div class="form-group"> + <label for="group-name">Password</label> + <input type="password" class="form-control" id="create-user-password" placeholder="Password" /> + </div> + + <div class="form-group"> + <label for="group-name">Password Confirm</label> + <input type="password" class="form-control" id="create-user-password-again" placeholder="Password" /> + </div> +</form> diff --git a/src/views/admin/partials/installed_plugin_item.tpl b/src/views/admin/partials/installed_plugin_item.tpl index b0399df5e2..3c95bcc2c1 100644 --- a/src/views/admin/partials/installed_plugin_item.tpl +++ b/src/views/admin/partials/installed_plugin_item.tpl @@ -1,5 +1,5 @@ <!-- IF !installed.error --> - <li id="{installed.id}" data-plugin-id="{installed.id}" data-version="{installed.version}" class="clearfix"> + <li id="{installed.id}" data-plugin-id="{installed.id}" data-version="{installed.version}" class="clearfix <!-- IF installed.active -->active<!-- ENDIF installed.active -->"> <div class="pull-right"> <!-- IF installed.isTheme --> <a href="{config.relative_path}/admin/appearance/themes" class="btn btn-info">Themes</a> diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 8d0f0f853d..712155a30b 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -16,7 +16,7 @@ <ul class="menu-section-list"> <li><a href="{relative_path}/admin/manage/categories">Categories</a></li> <li><a href="{relative_path}/admin/manage/tags">Tags</a></li> - <li><a href="{relative_path}/admin/manage/users/latest">Users</a></li> + <li><a href="{relative_path}/admin/manage/users">Users</a></li> <li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li> <li><a href="{relative_path}/admin/manage/groups">Groups</a></li> <li><a href="{relative_path}/admin/manage/flags">Flags</a></li> @@ -95,7 +95,8 @@ <li><a href="{relative_path}/admin/advanced/database">Database</a></li> <li><a href="{relative_path}/admin/advanced/events">Events</a></li> <li><a href="{relative_path}/admin/advanced/logs">Logs</a></li> - <li><a href="{relative_path}/admin/advanced/post-cache">Post Cache</a></li> + <li><a href="{relative_path}/admin/advanced/errors">Errors</a></li> + <li><a href="{relative_path}/admin/advanced/cache">Cache</a></li> <!-- IF env --> <li><a href="{relative_path}/admin/development/logger">Logger</a></li> <!-- ENDIF env --> @@ -106,31 +107,20 @@ <main id="panel"> <nav class="header" id="header"> <div class="pull-left"> - <button id="mobile-menu"> + <div id="mobile-menu"> <div class="bar"></div> <div class="bar"></div> <div class="bar"></div> - </button> + </div> <h1 id="main-page-title"></h1> </div> <ul id="user_label" class="pull-right"> <li class="dropdown pull-right"> <a class="dropdown-toggle" data-toggle="dropdown" href="#" id="user_dropdown"> - <i class="fa fa-ellipsis-v"></i> + <i class="fa fa-fw fa-ellipsis-v"></i> </a> <ul id="user-control-list" class="dropdown-menu" aria-labelledby="user_dropdown"> - <li> - <a href="{relative_path}/" target="_top" title="View Forum"> - View Forum - </a> - </li> - <li role="presentation" class="divider"></li> - <li> - <a href="#" class="reload" title="Reload Forum"> - Reload Forum - </a> - </li> <li> <a href="#" class="restart" title="Restart Forum"> Restart Forum @@ -142,6 +132,13 @@ </li> </ul> </li> + + <li class="pull-right"> + <a href="{config.relative_path}/"> + <i class="fa fa-fw fa-home" title="View Forum"></i> + </a> + </li> + <form class="pull-right hidden-sm hidden-xs" role="search"> <div class="" id="acp-search" > <div class="dropdown"> @@ -170,7 +167,7 @@ <ul class="dropdown-menu" role="menu"> <li><a href="{relative_path}/admin/manage/categories">Categories</a></li> <li><a href="{relative_path}/admin/manage/tags">Tags</a></li> - <li><a href="{relative_path}/admin/manage/users/latest">Users</a></li> + <li><a href="{relative_path}/admin/manage/users">Users</a></li> <li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li> <li><a href="{relative_path}/admin/manage/groups">Groups</a></li> <li><a href="{relative_path}/admin/manage/flags">Flags</a></li> @@ -247,11 +244,20 @@ <li><a href="{relative_path}/admin/advanced/database">Database</a></li> <li><a href="{relative_path}/admin/advanced/events">Events</a></li> <li><a href="{relative_path}/admin/advanced/logs">Logs</a></li> - <li><a href="{relative_path}/admin/advanced/post-cache">Post Cache</a></li> + <li><a href="{relative_path}/admin/advanced/errors">Errors</a></li> + <li><a href="{relative_path}/admin/advanced/cache">Cache</a></li> <!-- IF env --> <li><a href="{relative_path}/admin/development/logger">Logger</a></li> <!-- ENDIF env --> </ul> </li> </ul> + + <ul class="nav navbar-nav navbar-right hidden-xs reconnect-spinner"> + <li> + <a href="#" id="reconnect" class="hide" title="Connection to {title} has been lost, attempting to reconnect..."> + <i class="fa fa-check"></i> + </a> + </li> + </ul> </nav> \ No newline at end of file diff --git a/src/views/admin/partials/temporary-ban.tpl b/src/views/admin/partials/temporary-ban.tpl new file mode 100644 index 0000000000..da8d02c79f --- /dev/null +++ b/src/views/admin/partials/temporary-ban.tpl @@ -0,0 +1,32 @@ +<form class="form"> + <div class="row"> + <div class="col-xs-4"> + <div class="form-group"> + <label for="length">Ban Length</label> + <input class="form-control" id="length" name="length" type="number" min="1" value="1" /> + </div> + </div> + <div class="col-xs-8"> + <div class="form-group"> + <label for="reason">Reason <span class="text-muted">(Optional)</span></label> + <input type="text" class="form-control" id="reason" name="reason" /> + </div> + </div> + </div> + <div class="row"> + <div class="col-sm-4 text-center"> + <div class="form-group units"> + <label>Hours</label> + <input type="radio" name="unit" value="0" checked /> + + <label>Days</label> + <input type="radio" name="unit" value="1" /> + </div> + </div> + <div class="col-sm-8"> + <p class="help-block"> + Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban. + </p> + </div> + </div> +</form> \ No newline at end of file diff --git a/src/views/admin/partials/widget-settings.tpl b/src/views/admin/partials/widget-settings.tpl new file mode 100644 index 0000000000..dfb7f8f4bc --- /dev/null +++ b/src/views/admin/partials/widget-settings.tpl @@ -0,0 +1,18 @@ +<br /> +<label>Title:</label> +<input type="text" class="form-control" name="title" placeholder="Title (only shown on some containers)" /><br /> + +<label>Container:</label> +<textarea rows="4" class="form-control container-html" name="container" placeholder="Drag and drop a container or enter HTML here."></textarea> + +<div class="checkbox"> +<label><input name="hide-guests" type="checkbox"> Hide from anonymous users?</label> +</div> + +<div class="checkbox"> +<label><input name="hide-registered" type="checkbox"> Hide from registered users?</input></label> +</div> + +<div class="checkbox"> +<label><input name="hide-mobile" type="checkbox"> Hide on mobile?</input></label> +</div> diff --git a/src/views/admin/settings/advanced.tpl b/src/views/admin/settings/advanced.tpl index 3fd617cf83..f845d06a1c 100644 --- a/src/views/admin/settings/advanced.tpl +++ b/src/views/admin/settings/advanced.tpl @@ -88,7 +88,7 @@ <input class="form-control" id="eventLoopLagThreshold" type="number" data-field="eventLoopLagThreshold" placeholder="Default: 70" step="10" min="10" value="70" /> <p class="help-block"> Lowering this value decreases wait times for page loads, but will also show the - "excessive load" message to more users. (Reload required) + "excessive load" message to more users. (Restart required) </p> </div> <div class="form-group"> @@ -96,7 +96,7 @@ <input class="form-control" id="eventLoopInterval" type="number" data-field="eventLoopInterval" placeholder="Default: 500" value="500" step="50" /> <p class="help-block"> Lowering this value causes NodeBB to become more sensitive to spikes in load, but - may also cause the check to become too sensitive. (Reload required) + may also cause the check to become too sensitive. (Restart required) </p> </div> </form> diff --git a/src/views/admin/settings/chat.tpl b/src/views/admin/settings/chat.tpl index 3f0fb1e222..5f4d0b8315 100644 --- a/src/views/admin/settings/chat.tpl +++ b/src/views/admin/settings/chat.tpl @@ -12,9 +12,6 @@ </label> </div> </div> - <div class="form-group"> - <strong>Chat Message Inbox Size</strong><br /> <input type="text" class="form-control" value="250" data-field="chatMessageInboxSize"> - </div> <div class="form-group"> <label>Maximum length of chat messages</label> @@ -25,6 +22,12 @@ <label>Maximum number of users in chat rooms</label> <input type="text" class="form-control" value="0" data-field="maximumUsersInChatRoom"> </div> + + + <div class="form-group"> + <label>Time between chat messages in milliseconds</label> + <input type="text" class="form-control" value="200" data-field="chatMessageDelay"> + </div> </div> </div> diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 683a13f4cc..3b65cbc0a4 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -34,14 +34,14 @@ </div> <div class="form-group"> <label for="email:GmailTransport:user"><strong>Username</strong></label> - <input type="text" class="form-control input-lg" id="email:GmailTransport:user" data-field="email:GmailTransport:user" placeholder="admin@example.org" /><br /> + <input type="text" class="form-control input-lg" id="email:GmailTransport:user" data-field="email:GmailTransport:user" placeholder="admin@example.org" /> <p class="help-block"> Enter the full email address here, especially if you are using a Google Apps managed domain. </p> </div> <div class="form-group"> <label for="email:GmailTransport:pass"><strong>Password</strong></label> - <input type="password" class="form-control input-lg" id="email:GmailTransport:pass" data-field="email:GmailTransport:pass" /><br /> + <input type="password" class="form-control input-lg" id="email:GmailTransport:pass" data-field="email:GmailTransport:pass" /> </div> </form> </div> @@ -67,12 +67,14 @@ <div class="row"> <div class="col-sm-2 col-xs-12 settings-header">Email Testing</div> <div class="col-sm-10 col-xs-12"> - <label>Select Email Template</label><br /> - <select id="test-email" class="form-control"> - <!-- BEGIN sendable --> - <option value="{sendable.path}">{sendable.path}</option> - <!-- END sendable --> - </select><br /> + <div class="form-group"> + <label>Select Email Template</label> + <select id="test-email" class="form-control"> + <!-- BEGIN sendable --> + <option value="{sendable.path}">{sendable.path}</option> + <!-- END sendable --> + </select> + </div> <button class="btn btn-primary" type="button" data-action="email.test">Send Test Email</button> <p class="help-block"> The test email will be sent to the currently logged in user's email address. @@ -90,6 +92,17 @@ <span class="mdl-switch__label">Disable subscriber notification emails</span> </label> </div> + + <div class="form-group"> + <label for="digestHour"><strong>Digest Hour</strong></label> + <input type="number" class="form-control input-lg" id="digestHour" data-field="digestHour" placeholder="17" min="0" max="24" /> + <p class="help-block"> + Please enter a number representing the hour to send scheduled email digests (e.g. <code>0</code> for midnight, <code>17</code> for 5:00pm). + Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.<br /> + The approximate server time is: <span id="serverTime"></span><br /> + The next daily digest is scheduled to be sent <span id="nextDigestTime"></span> + </p> + </div> </form> </div> </div> diff --git a/src/views/admin/settings/footer.tpl b/src/views/admin/settings/footer.tpl index fc80967c47..af46d0ad59 100644 --- a/src/views/admin/settings/footer.tpl +++ b/src/views/admin/settings/footer.tpl @@ -6,7 +6,7 @@ <script> require(['admin/settings'], function(Settings) { - Settings.init(); + Settings.prepare(); Settings.populateTOC(); }); </script> diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index fa41b38f61..79cba3caf7 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -50,6 +50,7 @@ </span> </div> </div> + <div class="form-group"> <label for="brand:logo:url">URL</label> <input id ="brand:logo:url" type="text" class="form-control" placeholder="The URL of the site logo" data-field="brand:logo:url" /> @@ -61,6 +62,17 @@ <label for="brand:logo:alt">Alt Text</label> <input id ="brand:logo:alt" type="text" class="form-control" placeholder="Alternative text for accessibility" data-field="brand:logo:alt" /> </div> + + <div class="form-group"> + <label for="og_image">og:image</label> + <div class="input-group"> + <input id="og_image" type="text" class="form-control" placeholder="" data-field="og:image" data-action="upload" data-target="og_image" data-route="{config.relative_path}/api/admin/uploadOgImage" readonly /> + <span class="input-group-btn"> + <input data-action="upload" data-target="og_image" data-route="{config.relative_path}/api/admin/uploadOgImage" type="button" class="btn btn-default" value="Upload"></input> + <button data-action="removeOgImage" type="button" class="btn btn-default btn-danger"><i class="fa fa-times"></i></button> + </span> + </div> + </div> </div> </div> diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl index 13b5121b24..8e1ee54b3f 100644 --- a/src/views/admin/settings/group.tpl +++ b/src/views/admin/settings/group.tpl @@ -28,6 +28,9 @@ <p class="help-block"> If enabled, users can create groups <em>(Default: disabled)</em> </p> + + <label>Maximum Group Name Length</label> + <input class="form-control" type="text" placeholder="255" data-field="maximumGroupNameLength" /> </form> </div> </div> diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 590f0e18cd..f58f524aca 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -48,6 +48,14 @@ <label for="postEditDuration">Number of seconds users are allowed to edit posts after posting. (0 disabled)</label> <input id="postEditDuration" type="text" class="form-control" value="0" data-field="postEditDuration"> </div> + <div class="form-group"> + <label for="postDeleteDuration">Number of seconds users are allowed to delete posts after posting. (0 disabled)</label> + <input id="postDeleteDuration" type="text" class="form-control" value="0" data-field="postDeleteDuration"> + </div> + <div class="form-group"> + <label for="preventTopicDeleteAfterReplies">Number of replies after users are disallowed to delete their own topics. (0 disabled)</label> + <input id="preventTopicDeleteAfterReplies" type="text" class="form-control" value="0" data-field="preventTopicDeleteAfterReplies"> + </div> <div class="form-group"> <label for="minimumTitleLength">Minimum Title Length</label> <input id="minimumTitleLength" type="text" class="form-control" value="3" data-field="minimumTitleLength"> @@ -83,7 +91,8 @@ <div class="form-group"> <label>Teaser Post</label> <select class="form-control" data-field="teaserPost"> - <option value="last">Last</option> + <option value="last-post">Last – Show the latest post, including the original post, if no replies</option> + <option value="last-reply">Last – Show the latest reply, or a "No replies" placeholder if no replies</option> <option value="first">First</option> </select> </div> @@ -100,6 +109,10 @@ <label for="unreadCutoff">Unread cutoff days</label> <input id="unreadCutoff" type="text" class="form-control" value="2" data-field="unreadCutoff"> </div> + <div class="form-group"> + <label for="bookmarkthreshold">Minimum posts in topic before tracking last read</label> + <input id="bookmarkthreshold" type="text" class="form-control" value="5" data-field="bookmarkThreshold"> + </div> </form> </div> </div> diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl index 76b053332b..80fbe3074d 100644 --- a/src/views/admin/settings/uploads.tpl +++ b/src/views/admin/settings/uploads.tpl @@ -20,16 +20,20 @@ </label> </div> - <div class="checkbox"> - <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect"> - <input class="mdl-switch__input" type="checkbox" data-field="allowGuestUploads"> - <span class="mdl-switch__label"><strong>Allow Guests to Upload Files</strong></span> - </label> + <div class="form-group"> + <label for="maximumImageWidth">Resize images down to specified width (in pixels)</label> + <input type="text" class="form-control" value="760" data-field="maximumImageWidth" placeholder="760"> + <p class="help-block"> + (in pixels, default: 760 pixels, set to 0 to disable) + </p> </div> - + <div class="form-group"> - <label for="maximumFileSize">Maximum File Size</label> + <label for="maximumFileSize">Maximum File Size (in KiB)</label> <input type="text" class="form-control" value="2048" data-field="maximumFileSize"> + <p class="help-block"> + (in kilobytes, default: 2048 KiB) + </p> </div> <div class="checkbox"> @@ -41,7 +45,7 @@ <div class="form-group"> <label for="topicThumbSize">Topic Thumb Size</label> - <input type="text" class="form-control" value="120" data-field="topicThumbSize"> + <input type="text" class="form-control" value="120" data-field="topicThumbSize"> </div> <div class="form-group"> @@ -56,8 +60,6 @@ </div> </div> -<!-- IMPORT admin/settings/header.tpl --> - <div class="row"> <div class="col-sm-2 col-xs-12 settings-header"> Profile Avatars @@ -91,16 +93,32 @@ <div class="form-group"> <label for="profileImageDimension">Profile Image Dimension</label> <input id="profileImageDimension" type="text" class="form-control" data-field="profileImageDimension" placeholder="128" /> + <p class="help-block"> + (in pixels, default: 128 pixels) + </p> </div> <div class="form-group"> <label>Maximum Profile Image File Size</label> <input type="text" class="form-control" placeholder="Maximum size of uploaded user images in kilobytes" data-field="maximumProfileImageSize" /> + <p class="help-block"> + (in kilobytes, default: 256 KiB) + </p> </div> <div class="form-group"> <label>Maximum Cover Image File Size</label> <input type="text" class="form-control" placeholder="Maximum size of uploaded cover images in kilobytes" data-field="maximumCoverImageSize" /> + <p class="help-block"> + (in kilobytes, default: 2,048 KiB) + </p> + </div> + + <div class="checkbox"> + <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect"> + <input class="mdl-switch__input" type="checkbox" data-field="profile:keepAllUserImages"> + <span class="mdl-switch__label"><strong>Keep old versions of avatars and profile covers on the server</strong></span> + </label> </div> </form> </div> @@ -119,4 +137,4 @@ </div> </div> -<!-- IMPORT admin/settings/footer.tpl --> \ No newline at end of file +<!-- IMPORT admin/settings/footer.tpl --> diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 55c54149c6..aecdd1a0cb 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -32,26 +32,6 @@ <option value="email">Email Only</option> </select> </div> - - <div class="form-group"> - <label>Registration Type</label> - <select class="form-control" data-field="registrationType"> - <option value="normal">Normal</option> - <option value="admin-approval">Admin Approval</option> - <option value="invite-only">Invite Only</option> - <option value="admin-invite-only">Admin Invite Only</option> - <option value="disabled">No registration</option> - </select> - </div> - - <div class="form-group"> - <label>Maximum Invitations per User</label> - <input type="number" class="form-control" data-field="maximumInvites" placeholder="0"> - <p class="help-block"> - 0 for no restriction. Admins get infinite invitations<br> - Only applicable for "Invite Only" - </p> - </div> </form> </div> </div> @@ -139,6 +119,33 @@ <div class="col-sm-2 col-xs-12 settings-header">User Registration</div> <div class="col-sm-10 col-xs-12"> <form> + <div class="form-group"> + <label>Registration Type</label> + <select class="form-control" data-field="registrationType"> + <option value="normal">Normal</option> + <option value="admin-approval">Admin Approval</option> + <option value="admin-approval-ip">Admin Approval for IPs</option> + <option value="invite-only">Invite Only</option> + <option value="admin-invite-only">Admin Invite Only</option> + <option value="disabled">No registration</option> + </select> + <p class="help-block"> + Normal - Users can register from the /register page.<br/> + Admin Approval - User registrations are placed in an <a href="{config.relative_path}/admin/manage/registration">approval queue</a> for administrators.<br/> + Admin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.<br/> + Invite Only - Users can invite others from the <a href="{config.relative_path}/users" target="_blank">users</a> page.<br/> + Admin Invite Only - Only administrators can invite others from <a href="{config.relative_path}/users" target="_blank">users</a> and <a href="{config.relative_path}/admin/manage/users">admin/manage/users</a> pages.<br/> + No registration - No user registration.<br/> + </p> + </div> + <div class="form-group"> + <label>Maximum Invitations per User</label> + <input type="number" class="form-control" data-field="maximumInvites" placeholder="0"> + <p class="help-block"> + 0 for no restriction. Admins get infinite invitations<br> + Only applicable for "Invite Only" + </p> + </div> <div class="form-group"> <label>Minimum Username Length</label> <input type="text" class="form-control" value="2" data-field="minimumUsernameLength"> @@ -254,13 +261,6 @@ </label> </div> - <div class="checkbox"> - <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect"> - <input class="mdl-switch__input" type="checkbox" data-field="notificationSounds" /> - <span class="mdl-switch__label">Play a sound when you receive a notification</span> - </label> - </div> - </form> </div> </div> diff --git a/src/views/emails/notif_chat.tpl b/src/views/emails/notif_chat.tpl index a0669f3bac..f1d5a5fc42 100644 --- a/src/views/emails/notif_chat.tpl +++ b/src/views/emails/notif_chat.tpl @@ -3,7 +3,7 @@ <p>{summary}:</p> <blockquote>{message.content}</blockquote> -<a href="{url}/chats/{fromUserslug}">[[email:notif.chat.cta]]</a> +<a href="{url}/chats/{roomId}">[[email:notif.chat.cta]]</a> <!-- IMPORT emails/partials/footer.tpl --> diff --git a/src/views/install/index.tpl b/src/views/install/index.tpl index 75d6c1e16a..cb5b69dbcb 100644 --- a/src/views/install/index.tpl +++ b/src/views/install/index.tpl @@ -7,7 +7,7 @@ <title>NodeBB Web Installer</title> <link rel="stylesheet" type="text/css" href="https://bootswatch.com/united/bootstrap.min.css"> - <link href='http://fonts.googleapis.com/css?family=Roboto:400,300,500,700' rel='stylesheet' type='text/css'> + <link href='https://fonts.googleapis.com/css?family=Roboto:400,300,500,700' rel='stylesheet' type='text/css'> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="stylesheet.css"> @@ -72,9 +72,9 @@ <div class="row input-row"> <div class="col-sm-7 col-xs-12 input-field"> <label for="admin:password">Password</label> - <input type="password" class="form-control" name="admin:password" value="<!-- IF admin:password -->{admin:password}<!-- ENDIF admin:password -->" placeholder="Password" /> + <input type="password" class="form-control" name="admin:password" value="<!-- IF admin:password -->{admin:password}<!-- ENDIF admin:password -->" placeholder="Password" data-minimum-length="{minimumPasswordLength}" /> </div> - <div class="col-sm-5 help-text" data-help="Use a combination of numbers, symbols, and different cases. You can change the strictness of password creation in the Admin Control Panel."></div> + <div class="col-sm-5 help-text" data-help="Use a combination of numbers, symbols, and different cases. You can change the strictness of password creation in the Admin Control Panel. Minimum {minimumPasswordLength} characters."></div> </div> <div class="row input-row"> <div class="col-sm-7 col-xs-12 input-field"> @@ -119,7 +119,7 @@ <div class="container <!-- IF !success -->hide<!-- ENDIF !success -->"> <p> <h1>Congratulations! Your NodeBB has been set-up.</h1> - + <button id="launch" class="btn btn-lg btn-success">Launch NodeBB <i class="fa fa-spinner fa-spin hide"></i></button> </p> </div> @@ -136,7 +136,7 @@ </div> <!-- END questions --> </div> - <!-- END databases --> + <!-- END databases --> </div> <script src="//code.jquery.com/jquery-1.11.2.min.js"></script> diff --git a/src/views/partials/data/topic.tpl b/src/views/partials/data/topic.tpl index d7fd1deb50..ea6fea2a3c 100644 --- a/src/views/partials/data/topic.tpl +++ b/src/views/partials/data/topic.tpl @@ -1 +1 @@ -data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment" \ No newline at end of file +data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment" \ No newline at end of file diff --git a/src/views/partials/fontawesome.tpl b/src/views/partials/fontawesome.tpl index a179bfdd49..f6a84268bc 100644 --- a/src/views/partials/fontawesome.tpl +++ b/src/views/partials/fontawesome.tpl @@ -4,7 +4,737 @@ <input type="text" class="form-control" id="fa-filter" data-action="filter" placeholder="e.g. umbrella" /> </div> <div class="row fa-icons"> - <i class="fa fa-500px"></i> <i class="fa fa-adjust"></i> <i class="fa fa-adn"></i> <i class="fa fa-align-center"></i> <i class="fa fa-align-justify"></i> <i class="fa fa-align-left"></i> <i class="fa fa-align-right"></i> <i class="fa fa-amazon"></i> <i class="fa fa-ambulance"></i> <i class="fa fa-anchor"></i> <i class="fa fa-android"></i> <i class="fa fa-angellist"></i> <i class="fa fa-angle-double-down"></i> <i class="fa fa-angle-double-left"></i> <i class="fa fa-angle-double-right"></i> <i class="fa fa-angle-double-up"></i> <i class="fa fa-angle-down"></i> <i class="fa fa-angle-left"></i> <i class="fa fa-angle-right"></i> <i class="fa fa-angle-up"></i> <i class="fa fa-apple"></i> <i class="fa fa-archive"></i> <i class="fa fa-area-chart"></i> <i class="fa fa-arrow-circle-down"></i> <i class="fa fa-arrow-circle-left"></i> <i class="fa fa-arrow-circle-o-down"></i> <i class="fa fa-arrow-circle-o-left"></i> <i class="fa fa-arrow-circle-o-right"></i> <i class="fa fa-arrow-circle-o-up"></i> <i class="fa fa-arrow-circle-right"></i> <i class="fa fa-arrow-circle-up"></i> <i class="fa fa-arrow-down"></i> <i class="fa fa-arrow-left"></i> <i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-up"></i> <i class="fa fa-arrows"></i> <i class="fa fa-arrows-alt"></i> <i class="fa fa-arrows-h"></i> <i class="fa fa-arrows-v"></i> <i class="fa fa-asterisk"></i> <i class="fa fa-at"></i> <i class="fa fa-automobile"></i> <i class="fa fa-backward"></i> <i class="fa fa-balance-scale"></i> <i class="fa fa-ban"></i> <i class="fa fa-bank"></i> <i class="fa fa-bar-chart"></i> <i class="fa fa-bar-chart-o"></i> <i class="fa fa-barcode"></i> <i class="fa fa-bars"></i> <i class="fa fa-battery-0"></i> <i class="fa fa-battery-1"></i> <i class="fa fa-battery-2"></i> <i class="fa fa-battery-3"></i> <i class="fa fa-battery-4"></i> <i class="fa fa-battery-empty"></i> <i class="fa fa-battery-full"></i> <i class="fa fa-battery-half"></i> <i class="fa fa-battery-quarter"></i> <i class="fa fa-battery-three-quarters"></i> <i class="fa fa-bed"></i> <i class="fa fa-beer"></i> <i class="fa fa-behance"></i> <i class="fa fa-behance-square"></i> <i class="fa fa-bell"></i> <i class="fa fa-bell-o"></i> <i class="fa fa-bell-slash"></i> <i class="fa fa-bell-slash-o"></i> <i class="fa fa-bicycle"></i> <i class="fa fa-binoculars"></i> <i class="fa fa-birthday-cake"></i> <i class="fa fa-bitbucket"></i> <i class="fa fa-bitbucket-square"></i> <i class="fa fa-bitcoin"></i> <i class="fa fa-black-tie"></i> <i class="fa fa-bluetooth"></i> <i class="fa fa-bluetooth-b"></i> <i class="fa fa-bold"></i> <i class="fa fa-bolt"></i> <i class="fa fa-bomb"></i> <i class="fa fa-book"></i> <i class="fa fa-bookmark"></i> <i class="fa fa-bookmark-o"></i> <i class="fa fa-briefcase"></i> <i class="fa fa-btc"></i> <i class="fa fa-bug"></i> <i class="fa fa-building"></i> <i class="fa fa-building-o"></i> <i class="fa fa-bullhorn"></i> <i class="fa fa-bullseye"></i> <i class="fa fa-bus"></i> <i class="fa fa-buysellads"></i> <i class="fa fa-cab"></i> <i class="fa fa-calculator"></i> <i class="fa fa-calendar"></i> <i class="fa fa-calendar-check-o"></i> <i class="fa fa-calendar-minus-o"></i> <i class="fa fa-calendar-o"></i> <i class="fa fa-calendar-plus-o"></i> <i class="fa fa-calendar-times-o"></i> <i class="fa fa-camera"></i> <i class="fa fa-camera-retro"></i> <i class="fa fa-car"></i> <i class="fa fa-caret-down"></i> <i class="fa fa-caret-left"></i> <i class="fa fa-caret-right"></i> <i class="fa fa-caret-square-o-down"></i> <i class="fa fa-caret-square-o-left"></i> <i class="fa fa-caret-square-o-right"></i> <i class="fa fa-caret-square-o-up"></i> <i class="fa fa-caret-up"></i> <i class="fa fa-cart-arrow-down"></i> <i class="fa fa-cart-plus"></i> <i class="fa fa-cc"></i> <i class="fa fa-cc-amex"></i> <i class="fa fa-cc-diners-club"></i> <i class="fa fa-cc-discover"></i> <i class="fa fa-cc-jcb"></i> <i class="fa fa-cc-mastercard"></i> <i class="fa fa-cc-paypal"></i> <i class="fa fa-cc-stripe"></i> <i class="fa fa-cc-visa"></i> <i class="fa fa-certificate"></i> <i class="fa fa-chain"></i> <i class="fa fa-chain-broken"></i> <i class="fa fa-check"></i> <i class="fa fa-check-circle"></i> <i class="fa fa-check-circle-o"></i> <i class="fa fa-check-square"></i> <i class="fa fa-check-square-o"></i> <i class="fa fa-chevron-circle-down"></i> <i class="fa fa-chevron-circle-left"></i> <i class="fa fa-chevron-circle-right"></i> <i class="fa fa-chevron-circle-up"></i> <i class="fa fa-chevron-down"></i> <i class="fa fa-chevron-left"></i> <i class="fa fa-chevron-right"></i> <i class="fa fa-chevron-up"></i> <i class="fa fa-child"></i> <i class="fa fa-chrome"></i> <i class="fa fa-circle"></i> <i class="fa fa-circle-o"></i> <i class="fa fa-circle-o-notch"></i> <i class="fa fa-circle-thin"></i> <i class="fa fa-clipboard"></i> <i class="fa fa-clock-o"></i> <i class="fa fa-clone"></i> <i class="fa fa-close"></i> <i class="fa fa-cloud"></i> <i class="fa fa-cloud-download"></i> <i class="fa fa-cloud-upload"></i> <i class="fa fa-cny"></i> <i class="fa fa-code"></i> <i class="fa fa-code-fork"></i> <i class="fa fa-codepen"></i> <i class="fa fa-codiepie"></i> <i class="fa fa-coffee"></i> <i class="fa fa-cog"></i> <i class="fa fa-cogs"></i> <i class="fa fa-columns"></i> <i class="fa fa-comment"></i> <i class="fa fa-comment-o"></i> <i class="fa fa-commenting"></i> <i class="fa fa-commenting-o"></i> <i class="fa fa-comments"></i> <i class="fa fa-comments-o"></i> <i class="fa fa-compass"></i> <i class="fa fa-compress"></i> <i class="fa fa-connectdevelop"></i> <i class="fa fa-contao"></i> <i class="fa fa-copy"></i> <i class="fa fa-copyright"></i> <i class="fa fa-creative-commons"></i> <i class="fa fa-credit-card"></i> <i class="fa fa-credit-card-alt"></i> <i class="fa fa-crop"></i> <i class="fa fa-crosshairs"></i> <i class="fa fa-css3"></i> <i class="fa fa-cube"></i> <i class="fa fa-cubes"></i> <i class="fa fa-cut"></i> <i class="fa fa-cutlery"></i> <i class="fa fa-dashboard"></i> <i class="fa fa-dashcube"></i> <i class="fa fa-database"></i> <i class="fa fa-dedent"></i> <i class="fa fa-delicious"></i> <i class="fa fa-desktop"></i> <i class="fa fa-deviantart"></i> <i class="fa fa-diamond"></i> <i class="fa fa-digg"></i> <i class="fa fa-dollar"></i> <i class="fa fa-dot-circle-o"></i> <i class="fa fa-download"></i> <i class="fa fa-dribbble"></i> <i class="fa fa-dropbox"></i> <i class="fa fa-drupal"></i> <i class="fa fa-edge"></i> <i class="fa fa-edit"></i> <i class="fa fa-eject"></i> <i class="fa fa-ellipsis-h"></i> <i class="fa fa-ellipsis-v"></i> <i class="fa fa-empire"></i> <i class="fa fa-envelope"></i> <i class="fa fa-envelope-o"></i> <i class="fa fa-envelope-square"></i> <i class="fa fa-eraser"></i> <i class="fa fa-eur"></i> <i class="fa fa-euro"></i> <i class="fa fa-exchange"></i> <i class="fa fa-exclamation"></i> <i class="fa fa-exclamation-circle"></i> <i class="fa fa-exclamation-triangle"></i> <i class="fa fa-expand"></i> <i class="fa fa-expeditedssl"></i> <i class="fa fa-external-link"></i> <i class="fa fa-external-link-square"></i> <i class="fa fa-eye"></i> <i class="fa fa-eye-slash"></i> <i class="fa fa-eyedropper"></i> <i class="fa fa-facebook"></i> <i class="fa fa-facebook-f"></i> <i class="fa fa-facebook-official"></i> <i class="fa fa-facebook-square"></i> <i class="fa fa-fast-backward"></i> <i class="fa fa-fast-forward"></i> <i class="fa fa-fax"></i> <i class="fa fa-feed"></i> <i class="fa fa-female"></i> <i class="fa fa-fighter-jet"></i> <i class="fa fa-file"></i> <i class="fa fa-file-archive-o"></i> <i class="fa fa-file-audio-o"></i> <i class="fa fa-file-code-o"></i> <i class="fa fa-file-excel-o"></i> <i class="fa fa-file-image-o"></i> <i class="fa fa-file-movie-o"></i> <i class="fa fa-file-o"></i> <i class="fa fa-file-pdf-o"></i> <i class="fa fa-file-photo-o"></i> <i class="fa fa-file-picture-o"></i> <i class="fa fa-file-powerpoint-o"></i> <i class="fa fa-file-sound-o"></i> <i class="fa fa-file-text"></i> <i class="fa fa-file-text-o"></i> <i class="fa fa-file-video-o"></i> <i class="fa fa-file-word-o"></i> <i class="fa fa-file-zip-o"></i> <i class="fa fa-files-o"></i> <i class="fa fa-film"></i> <i class="fa fa-filter"></i> <i class="fa fa-fire"></i> <i class="fa fa-fire-extinguisher"></i> <i class="fa fa-firefox"></i> <i class="fa fa-flag"></i> <i class="fa fa-flag-checkered"></i> <i class="fa fa-flag-o"></i> <i class="fa fa-flash"></i> <i class="fa fa-flask"></i> <i class="fa fa-flickr"></i> <i class="fa fa-floppy-o"></i> <i class="fa fa-folder"></i> <i class="fa fa-folder-o"></i> <i class="fa fa-folder-open"></i> <i class="fa fa-folder-open-o"></i> <i class="fa fa-font"></i> <i class="fa fa-fonticons"></i> <i class="fa fa-fort-awesome"></i> <i class="fa fa-forumbee"></i> <i class="fa fa-forward"></i> <i class="fa fa-foursquare"></i> <i class="fa fa-frown-o"></i> <i class="fa fa-futbol-o"></i> <i class="fa fa-gamepad"></i> <i class="fa fa-gavel"></i> <i class="fa fa-gbp"></i> <i class="fa fa-ge"></i> <i class="fa fa-gear"></i> <i class="fa fa-gears"></i> <i class="fa fa-genderless"></i> <i class="fa fa-get-pocket"></i> <i class="fa fa-gg"></i> <i class="fa fa-gg-circle"></i> <i class="fa fa-gift"></i> <i class="fa fa-git"></i> <i class="fa fa-git-square"></i> <i class="fa fa-github"></i> <i class="fa fa-github-alt"></i> <i class="fa fa-github-square"></i> <i class="fa fa-gittip"></i> <i class="fa fa-glass"></i> <i class="fa fa-globe"></i> <i class="fa fa-google"></i> <i class="fa fa-google-plus"></i> <i class="fa fa-google-plus-square"></i> <i class="fa fa-google-wallet"></i> <i class="fa fa-graduation-cap"></i> <i class="fa fa-gratipay"></i> <i class="fa fa-group"></i> <i class="fa fa-h-square"></i> <i class="fa fa-hacker-news"></i> <i class="fa fa-hand-grab-o"></i> <i class="fa fa-hand-lizard-o"></i> <i class="fa fa-hand-o-down"></i> <i class="fa fa-hand-o-left"></i> <i class="fa fa-hand-o-right"></i> <i class="fa fa-hand-o-up"></i> <i class="fa fa-hand-paper-o"></i> <i class="fa fa-hand-peace-o"></i> <i class="fa fa-hand-pointer-o"></i> <i class="fa fa-hand-rock-o"></i> <i class="fa fa-hand-scissors-o"></i> <i class="fa fa-hand-spock-o"></i> <i class="fa fa-hand-stop-o"></i> <i class="fa fa-hashtag"></i> <i class="fa fa-hdd-o"></i> <i class="fa fa-header"></i> <i class="fa fa-headphones"></i> <i class="fa fa-heart"></i> <i class="fa fa-heart-o"></i> <i class="fa fa-heartbeat"></i> <i class="fa fa-history"></i> <i class="fa fa-home"></i> <i class="fa fa-hospital-o"></i> <i class="fa fa-hotel"></i> <i class="fa fa-hourglass"></i> <i class="fa fa-hourglass-1"></i> <i class="fa fa-hourglass-2"></i> <i class="fa fa-hourglass-3"></i> <i class="fa fa-hourglass-end"></i> <i class="fa fa-hourglass-half"></i> <i class="fa fa-hourglass-o"></i> <i class="fa fa-hourglass-start"></i> <i class="fa fa-houzz"></i> <i class="fa fa-html5"></i> <i class="fa fa-i-cursor"></i> <i class="fa fa-ils"></i> <i class="fa fa-image"></i> <i class="fa fa-inbox"></i> <i class="fa fa-indent"></i> <i class="fa fa-industry"></i> <i class="fa fa-info"></i> <i class="fa fa-info-circle"></i> <i class="fa fa-inr"></i> <i class="fa fa-instagram"></i> <i class="fa fa-institution"></i> <i class="fa fa-internet-explorer"></i> <i class="fa fa-intersex"></i> <i class="fa fa-ioxhost"></i> <i class="fa fa-italic"></i> <i class="fa fa-joomla"></i> <i class="fa fa-jpy"></i> <i class="fa fa-jsfiddle"></i> <i class="fa fa-key"></i> <i class="fa fa-keyboard-o"></i> <i class="fa fa-krw"></i> <i class="fa fa-language"></i> <i class="fa fa-laptop"></i> <i class="fa fa-lastfm"></i> <i class="fa fa-lastfm-square"></i> <i class="fa fa-leaf"></i> <i class="fa fa-leanpub"></i> <i class="fa fa-legal"></i> <i class="fa fa-lemon-o"></i> <i class="fa fa-level-down"></i> <i class="fa fa-level-up"></i> <i class="fa fa-life-bouy"></i> <i class="fa fa-life-buoy"></i> <i class="fa fa-life-ring"></i> <i class="fa fa-life-saver"></i> <i class="fa fa-lightbulb-o"></i> <i class="fa fa-line-chart"></i> <i class="fa fa-link"></i> <i class="fa fa-linkedin"></i> <i class="fa fa-linkedin-square"></i> <i class="fa fa-linux"></i> <i class="fa fa-list"></i> <i class="fa fa-list-alt"></i> <i class="fa fa-list-ol"></i> <i class="fa fa-list-ul"></i> <i class="fa fa-location-arrow"></i> <i class="fa fa-lock"></i> <i class="fa fa-long-arrow-down"></i> <i class="fa fa-long-arrow-left"></i> <i class="fa fa-long-arrow-right"></i> <i class="fa fa-long-arrow-up"></i> <i class="fa fa-magic"></i> <i class="fa fa-magnet"></i> <i class="fa fa-mail-forward"></i> <i class="fa fa-mail-reply"></i> <i class="fa fa-mail-reply-all"></i> <i class="fa fa-male"></i> <i class="fa fa-map"></i> <i class="fa fa-map-marker"></i> <i class="fa fa-map-o"></i> <i class="fa fa-map-pin"></i> <i class="fa fa-map-signs"></i> <i class="fa fa-mars"></i> <i class="fa fa-mars-double"></i> <i class="fa fa-mars-stroke"></i> <i class="fa fa-mars-stroke-h"></i> <i class="fa fa-mars-stroke-v"></i> <i class="fa fa-maxcdn"></i> <i class="fa fa-meanpath"></i> <i class="fa fa-medium"></i> <i class="fa fa-medkit"></i> <i class="fa fa-meh-o"></i> <i class="fa fa-mercury"></i> <i class="fa fa-microphone"></i> <i class="fa fa-microphone-slash"></i> <i class="fa fa-minus"></i> <i class="fa fa-minus-circle"></i> <i class="fa fa-minus-square"></i> <i class="fa fa-minus-square-o"></i> <i class="fa fa-mixcloud"></i> <i class="fa fa-mobile"></i> <i class="fa fa-mobile-phone"></i> <i class="fa fa-modx"></i> <i class="fa fa-money"></i> <i class="fa fa-moon-o"></i> <i class="fa fa-mortar-board"></i> <i class="fa fa-motorcycle"></i> <i class="fa fa-mouse-pointer"></i> <i class="fa fa-music"></i> <i class="fa fa-navicon"></i> <i class="fa fa-neuter"></i> <i class="fa fa-newspaper-o"></i> <i class="fa fa-object-group"></i> <i class="fa fa-object-ungroup"></i> <i class="fa fa-odnoklassniki"></i> <i class="fa fa-odnoklassniki-square"></i> <i class="fa fa-opencart"></i> <i class="fa fa-openid"></i> <i class="fa fa-opera"></i> <i class="fa fa-optin-monster"></i> <i class="fa fa-outdent"></i> <i class="fa fa-pagelines"></i> <i class="fa fa-paint-brush"></i> <i class="fa fa-paper-plane"></i> <i class="fa fa-paper-plane-o"></i> <i class="fa fa-paperclip"></i> <i class="fa fa-paragraph"></i> <i class="fa fa-paste"></i> <i class="fa fa-pause"></i> <i class="fa fa-pause-circle"></i> <i class="fa fa-pause-circle-o"></i> <i class="fa fa-paw"></i> <i class="fa fa-paypal"></i> <i class="fa fa-pencil"></i> <i class="fa fa-pencil-square"></i> <i class="fa fa-pencil-square-o"></i> <i class="fa fa-percent"></i> <i class="fa fa-phone"></i> <i class="fa fa-phone-square"></i> <i class="fa fa-photo"></i> <i class="fa fa-picture-o"></i> <i class="fa fa-pie-chart"></i> <i class="fa fa-pied-piper"></i> <i class="fa fa-pied-piper-alt"></i> <i class="fa fa-pinterest"></i> <i class="fa fa-pinterest-p"></i> <i class="fa fa-pinterest-square"></i> <i class="fa fa-plane"></i> <i class="fa fa-play"></i> <i class="fa fa-play-circle"></i> <i class="fa fa-play-circle-o"></i> <i class="fa fa-plug"></i> <i class="fa fa-plus"></i> <i class="fa fa-plus-circle"></i> <i class="fa fa-plus-square"></i> <i class="fa fa-plus-square-o"></i> <i class="fa fa-power-off"></i> <i class="fa fa-print"></i> <i class="fa fa-product-hunt"></i> <i class="fa fa-puzzle-piece"></i> <i class="fa fa-qq"></i> <i class="fa fa-qrcode"></i> <i class="fa fa-question"></i> <i class="fa fa-question-circle"></i> <i class="fa fa-quote-left"></i> <i class="fa fa-quote-right"></i> <i class="fa fa-ra"></i> <i class="fa fa-random"></i> <i class="fa fa-rebel"></i> <i class="fa fa-recycle"></i> <i class="fa fa-reddit"></i> <i class="fa fa-reddit-alien"></i> <i class="fa fa-reddit-square"></i> <i class="fa fa-refresh"></i> <i class="fa fa-registered"></i> <i class="fa fa-remove"></i> <i class="fa fa-renren"></i> <i class="fa fa-reorder"></i> <i class="fa fa-repeat"></i> <i class="fa fa-reply"></i> <i class="fa fa-reply-all"></i> <i class="fa fa-retweet"></i> <i class="fa fa-rmb"></i> <i class="fa fa-road"></i> <i class="fa fa-rocket"></i> <i class="fa fa-rotate-left"></i> <i class="fa fa-rotate-right"></i> <i class="fa fa-rouble"></i> <i class="fa fa-rss"></i> <i class="fa fa-rss-square"></i> <i class="fa fa-rub"></i> <i class="fa fa-ruble"></i> <i class="fa fa-rupee"></i> <i class="fa fa-safari"></i> <i class="fa fa-save"></i> <i class="fa fa-scissors"></i> <i class="fa fa-scribd"></i> <i class="fa fa-search"></i> <i class="fa fa-search-minus"></i> <i class="fa fa-search-plus"></i> <i class="fa fa-sellsy"></i> <i class="fa fa-send"></i> <i class="fa fa-send-o"></i> <i class="fa fa-server"></i> <i class="fa fa-share"></i> <i class="fa fa-share-alt"></i> <i class="fa fa-share-alt-square"></i> <i class="fa fa-share-square"></i> <i class="fa fa-share-square-o"></i> <i class="fa fa-shekel"></i> <i class="fa fa-sheqel"></i> <i class="fa fa-shield"></i> <i class="fa fa-ship"></i> <i class="fa fa-shirtsinbulk"></i> <i class="fa fa-shopping-bag"></i> <i class="fa fa-shopping-basket"></i> <i class="fa fa-shopping-cart"></i> <i class="fa fa-sign-in"></i> <i class="fa fa-sign-out"></i> <i class="fa fa-signal"></i> <i class="fa fa-simplybuilt"></i> <i class="fa fa-sitemap"></i> <i class="fa fa-skyatlas"></i> <i class="fa fa-skype"></i> <i class="fa fa-slack"></i> <i class="fa fa-sliders"></i> <i class="fa fa-slideshare"></i> <i class="fa fa-smile-o"></i> <i class="fa fa-soccer-ball-o"></i> <i class="fa fa-sort"></i> <i class="fa fa-sort-alpha-asc"></i> <i class="fa fa-sort-alpha-desc"></i> <i class="fa fa-sort-amount-asc"></i> <i class="fa fa-sort-amount-desc"></i> <i class="fa fa-sort-asc"></i> <i class="fa fa-sort-desc"></i> <i class="fa fa-sort-down"></i> <i class="fa fa-sort-numeric-asc"></i> <i class="fa fa-sort-numeric-desc"></i> <i class="fa fa-sort-up"></i> <i class="fa fa-soundcloud"></i> <i class="fa fa-space-shuttle"></i> <i class="fa fa-spinner"></i> <i class="fa fa-spoon"></i> <i class="fa fa-spotify"></i> <i class="fa fa-square"></i> <i class="fa fa-square-o"></i> <i class="fa fa-stack-exchange"></i> <i class="fa fa-stack-overflow"></i> <i class="fa fa-star"></i> <i class="fa fa-star-half"></i> <i class="fa fa-star-half-empty"></i> <i class="fa fa-star-half-full"></i> <i class="fa fa-star-half-o"></i> <i class="fa fa-star-o"></i> <i class="fa fa-steam"></i> <i class="fa fa-steam-square"></i> <i class="fa fa-step-backward"></i> <i class="fa fa-step-forward"></i> <i class="fa fa-stethoscope"></i> <i class="fa fa-sticky-note"></i> <i class="fa fa-sticky-note-o"></i> <i class="fa fa-stop"></i> <i class="fa fa-stop-circle"></i> <i class="fa fa-stop-circle-o"></i> <i class="fa fa-street-view"></i> <i class="fa fa-strikethrough"></i> <i class="fa fa-stumbleupon"></i> <i class="fa fa-stumbleupon-circle"></i> <i class="fa fa-subscript"></i> <i class="fa fa-subway"></i> <i class="fa fa-suitcase"></i> <i class="fa fa-sun-o"></i> <i class="fa fa-superscript"></i> <i class="fa fa-support"></i> <i class="fa fa-table"></i> <i class="fa fa-tablet"></i> <i class="fa fa-tachometer"></i> <i class="fa fa-tag"></i> <i class="fa fa-tags"></i> <i class="fa fa-tasks"></i> <i class="fa fa-taxi"></i> <i class="fa fa-television"></i> <i class="fa fa-tencent-weibo"></i> <i class="fa fa-terminal"></i> <i class="fa fa-text-height"></i> <i class="fa fa-text-width"></i> <i class="fa fa-th"></i> <i class="fa fa-th-large"></i> <i class="fa fa-th-list"></i> <i class="fa fa-thumb-tack"></i> <i class="fa fa-thumbs-down"></i> <i class="fa fa-thumbs-o-down"></i> <i class="fa fa-thumbs-o-up"></i> <i class="fa fa-thumbs-up"></i> <i class="fa fa-ticket"></i> <i class="fa fa-times"></i> <i class="fa fa-times-circle"></i> <i class="fa fa-times-circle-o"></i> <i class="fa fa-tint"></i> <i class="fa fa-toggle-down"></i> <i class="fa fa-toggle-left"></i> <i class="fa fa-toggle-off"></i> <i class="fa fa-toggle-on"></i> <i class="fa fa-toggle-right"></i> <i class="fa fa-toggle-up"></i> <i class="fa fa-trademark"></i> <i class="fa fa-train"></i> <i class="fa fa-transgender"></i> <i class="fa fa-transgender-alt"></i> <i class="fa fa-trash"></i> <i class="fa fa-trash-o"></i> <i class="fa fa-tree"></i> <i class="fa fa-trello"></i> <i class="fa fa-tripadvisor"></i> <i class="fa fa-trophy"></i> <i class="fa fa-truck"></i> <i class="fa fa-try"></i> <i class="fa fa-tty"></i> <i class="fa fa-tumblr"></i> <i class="fa fa-tumblr-square"></i> <i class="fa fa-turkish-lira"></i> <i class="fa fa-tv"></i> <i class="fa fa-twitch"></i> <i class="fa fa-twitter"></i> <i class="fa fa-twitter-square"></i> <i class="fa fa-umbrella"></i> <i class="fa fa-underline"></i> <i class="fa fa-undo"></i> <i class="fa fa-university"></i> <i class="fa fa-unlink"></i> <i class="fa fa-unlock"></i> <i class="fa fa-unlock-alt"></i> <i class="fa fa-unsorted"></i> <i class="fa fa-upload"></i> <i class="fa fa-usb"></i> <i class="fa fa-usd"></i> <i class="fa fa-user"></i> <i class="fa fa-user-md"></i> <i class="fa fa-user-plus"></i> <i class="fa fa-user-secret"></i> <i class="fa fa-user-times"></i> <i class="fa fa-users"></i> <i class="fa fa-venus"></i> <i class="fa fa-venus-double"></i> <i class="fa fa-venus-mars"></i> <i class="fa fa-viacoin"></i> <i class="fa fa-video-camera"></i> <i class="fa fa-vimeo"></i> <i class="fa fa-vimeo-square"></i> <i class="fa fa-vine"></i> <i class="fa fa-vk"></i> <i class="fa fa-volume-down"></i> <i class="fa fa-volume-off"></i> <i class="fa fa-volume-up"></i> <i class="fa fa-warning"></i> <i class="fa fa-wechat"></i> <i class="fa fa-weibo"></i> <i class="fa fa-weixin"></i> <i class="fa fa-whatsapp"></i> <i class="fa fa-wheelchair"></i> <i class="fa fa-wifi"></i> <i class="fa fa-wikipedia-w"></i> <i class="fa fa-windows"></i> <i class="fa fa-won"></i> <i class="fa fa-wordpress"></i> <i class="fa fa-wrench"></i> <i class="fa fa-xing"></i> <i class="fa fa-xing-square"></i> <i class="fa fa-y-combinator"></i> <i class="fa fa-y-combinator-square"></i> <i class="fa fa-yahoo"></i> <i class="fa fa-yc"></i> <i class="fa fa-yc-square"></i> <i class="fa fa-yelp"></i> <i class="fa fa-yen"></i> <i class="fa fa-youtube"></i> <i class="fa fa-youtube-play"></i> <i class="fa fa-youtube-square"></i> + <i class="fa fa-500px"></i> + <i class="fa fa-adjust"></i> + <i class="fa fa-adn"></i> + <i class="fa fa-align-center"></i> + <i class="fa fa-align-justify"></i> + <i class="fa fa-align-left"></i> + <i class="fa fa-align-right"></i> + <i class="fa fa-amazon"></i> + <i class="fa fa-ambulance"></i> + <i class="fa fa-american-sign-language-interpreting"></i> + <i class="fa fa-anchor"></i> + <i class="fa fa-android"></i> + <i class="fa fa-angellist"></i> + <i class="fa fa-angle-double-down"></i> + <i class="fa fa-angle-double-left"></i> + <i class="fa fa-angle-double-right"></i> + <i class="fa fa-angle-double-up"></i> + <i class="fa fa-angle-down"></i> + <i class="fa fa-angle-left"></i> + <i class="fa fa-angle-right"></i> + <i class="fa fa-angle-up"></i> + <i class="fa fa-apple"></i> + <i class="fa fa-archive"></i> + <i class="fa fa-area-chart"></i> + <i class="fa fa-arrow-circle-down"></i> + <i class="fa fa-arrow-circle-left"></i> + <i class="fa fa-arrow-circle-o-down"></i> + <i class="fa fa-arrow-circle-o-left"></i> + <i class="fa fa-arrow-circle-o-right"></i> + <i class="fa fa-arrow-circle-o-up"></i> + <i class="fa fa-arrow-circle-right"></i> + <i class="fa fa-arrow-circle-up"></i> + <i class="fa fa-arrow-down"></i> + <i class="fa fa-arrow-left"></i> + <i class="fa fa-arrow-right"></i> + <i class="fa fa-arrow-up"></i> + <i class="fa fa-arrows"></i> + <i class="fa fa-arrows-alt"></i> + <i class="fa fa-arrows-h"></i> + <i class="fa fa-arrows-v"></i> + <i class="fa fa-asl-interpreting"></i> + <i class="fa fa-assistive-listening-systems"></i> + <i class="fa fa-asterisk"></i> + <i class="fa fa-at"></i> + <i class="fa fa-audio-description"></i> + <i class="fa fa-automobile"></i> + <i class="fa fa-backward"></i> + <i class="fa fa-balance-scale"></i> + <i class="fa fa-ban"></i> + <i class="fa fa-bank"></i> + <i class="fa fa-bar-chart"></i> + <i class="fa fa-bar-chart-o"></i> + <i class="fa fa-barcode"></i> + <i class="fa fa-bars"></i> + <i class="fa fa-battery-0"></i> + <i class="fa fa-battery-1"></i> + <i class="fa fa-battery-2"></i> + <i class="fa fa-battery-3"></i> + <i class="fa fa-battery-4"></i> + <i class="fa fa-battery-empty"></i> + <i class="fa fa-battery-full"></i> + <i class="fa fa-battery-half"></i> + <i class="fa fa-battery-quarter"></i> + <i class="fa fa-battery-three-quarters"></i> + <i class="fa fa-bed"></i> + <i class="fa fa-beer"></i> + <i class="fa fa-behance"></i> + <i class="fa fa-behance-square"></i> + <i class="fa fa-bell"></i> + <i class="fa fa-bell-o"></i> + <i class="fa fa-bell-slash"></i> + <i class="fa fa-bell-slash-o"></i> + <i class="fa fa-bicycle"></i> + <i class="fa fa-binoculars"></i> + <i class="fa fa-birthday-cake"></i> + <i class="fa fa-bitbucket"></i> + <i class="fa fa-bitbucket-square"></i> + <i class="fa fa-bitcoin"></i> + <i class="fa fa-black-tie"></i> + <i class="fa fa-blind"></i> + <i class="fa fa-bluetooth"></i> + <i class="fa fa-bluetooth-b"></i> + <i class="fa fa-bold"></i> + <i class="fa fa-bolt"></i> + <i class="fa fa-bomb"></i> + <i class="fa fa-book"></i> + <i class="fa fa-bookmark"></i> + <i class="fa fa-bookmark-o"></i> + <i class="fa fa-braille"></i> + <i class="fa fa-briefcase"></i> + <i class="fa fa-btc"></i> + <i class="fa fa-bug"></i> + <i class="fa fa-building"></i> + <i class="fa fa-building-o"></i> + <i class="fa fa-bullhorn"></i> + <i class="fa fa-bullseye"></i> + <i class="fa fa-bus"></i> + <i class="fa fa-buysellads"></i> + <i class="fa fa-cab"></i> + <i class="fa fa-calculator"></i> + <i class="fa fa-calendar"></i> + <i class="fa fa-calendar-check-o"></i> + <i class="fa fa-calendar-minus-o"></i> + <i class="fa fa-calendar-o"></i> + <i class="fa fa-calendar-plus-o"></i> + <i class="fa fa-calendar-times-o"></i> + <i class="fa fa-camera"></i> + <i class="fa fa-camera-retro"></i> + <i class="fa fa-car"></i> + <i class="fa fa-caret-down"></i> + <i class="fa fa-caret-left"></i> + <i class="fa fa-caret-right"></i> + <i class="fa fa-caret-square-o-down"></i> + <i class="fa fa-caret-square-o-left"></i> + <i class="fa fa-caret-square-o-right"></i> + <i class="fa fa-caret-square-o-up"></i> + <i class="fa fa-caret-up"></i> + <i class="fa fa-cart-arrow-down"></i> + <i class="fa fa-cart-plus"></i> + <i class="fa fa-cc"></i> + <i class="fa fa-cc-amex"></i> + <i class="fa fa-cc-diners-club"></i> + <i class="fa fa-cc-discover"></i> + <i class="fa fa-cc-jcb"></i> + <i class="fa fa-cc-mastercard"></i> + <i class="fa fa-cc-paypal"></i> + <i class="fa fa-cc-stripe"></i> + <i class="fa fa-cc-visa"></i> + <i class="fa fa-certificate"></i> + <i class="fa fa-chain"></i> + <i class="fa fa-chain-broken"></i> + <i class="fa fa-check"></i> + <i class="fa fa-check-circle"></i> + <i class="fa fa-check-circle-o"></i> + <i class="fa fa-check-square"></i> + <i class="fa fa-check-square-o"></i> + <i class="fa fa-chevron-circle-down"></i> + <i class="fa fa-chevron-circle-left"></i> + <i class="fa fa-chevron-circle-right"></i> + <i class="fa fa-chevron-circle-up"></i> + <i class="fa fa-chevron-down"></i> + <i class="fa fa-chevron-left"></i> + <i class="fa fa-chevron-right"></i> + <i class="fa fa-chevron-up"></i> + <i class="fa fa-child"></i> + <i class="fa fa-chrome"></i> + <i class="fa fa-circle"></i> + <i class="fa fa-circle-o"></i> + <i class="fa fa-circle-o-notch"></i> + <i class="fa fa-circle-thin"></i> + <i class="fa fa-clipboard"></i> + <i class="fa fa-clock-o"></i> + <i class="fa fa-clone"></i> + <i class="fa fa-close"></i> + <i class="fa fa-cloud"></i> + <i class="fa fa-cloud-download"></i> + <i class="fa fa-cloud-upload"></i> + <i class="fa fa-cny"></i> + <i class="fa fa-code"></i> + <i class="fa fa-code-fork"></i> + <i class="fa fa-codepen"></i> + <i class="fa fa-codiepie"></i> + <i class="fa fa-coffee"></i> + <i class="fa fa-cog"></i> + <i class="fa fa-cogs"></i> + <i class="fa fa-columns"></i> + <i class="fa fa-comment"></i> + <i class="fa fa-comment-o"></i> + <i class="fa fa-commenting"></i> + <i class="fa fa-commenting-o"></i> + <i class="fa fa-comments"></i> + <i class="fa fa-comments-o"></i> + <i class="fa fa-compass"></i> + <i class="fa fa-compress"></i> + <i class="fa fa-connectdevelop"></i> + <i class="fa fa-contao"></i> + <i class="fa fa-copy"></i> + <i class="fa fa-copyright"></i> + <i class="fa fa-creative-commons"></i> + <i class="fa fa-credit-card"></i> + <i class="fa fa-credit-card-alt"></i> + <i class="fa fa-crop"></i> + <i class="fa fa-crosshairs"></i> + <i class="fa fa-css3"></i> + <i class="fa fa-cube"></i> + <i class="fa fa-cubes"></i> + <i class="fa fa-cut"></i> + <i class="fa fa-cutlery"></i> + <i class="fa fa-dashboard"></i> + <i class="fa fa-dashcube"></i> + <i class="fa fa-database"></i> + <i class="fa fa-deaf"></i> + <i class="fa fa-deafness"></i> + <i class="fa fa-dedent"></i> + <i class="fa fa-delicious"></i> + <i class="fa fa-desktop"></i> + <i class="fa fa-deviantart"></i> + <i class="fa fa-diamond"></i> + <i class="fa fa-digg"></i> + <i class="fa fa-dollar"></i> + <i class="fa fa-dot-circle-o"></i> + <i class="fa fa-download"></i> + <i class="fa fa-dribbble"></i> + <i class="fa fa-dropbox"></i> + <i class="fa fa-drupal"></i> + <i class="fa fa-edge"></i> + <i class="fa fa-edit"></i> + <i class="fa fa-eject"></i> + <i class="fa fa-ellipsis-h"></i> + <i class="fa fa-ellipsis-v"></i> + <i class="fa fa-empire"></i> + <i class="fa fa-envelope"></i> + <i class="fa fa-envelope-o"></i> + <i class="fa fa-envelope-square"></i> + <i class="fa fa-envira"></i> + <i class="fa fa-eraser"></i> + <i class="fa fa-eur"></i> + <i class="fa fa-euro"></i> + <i class="fa fa-exchange"></i> + <i class="fa fa-exclamation"></i> + <i class="fa fa-exclamation-circle"></i> + <i class="fa fa-exclamation-triangle"></i> + <i class="fa fa-expand"></i> + <i class="fa fa-expeditedssl"></i> + <i class="fa fa-external-link"></i> + <i class="fa fa-external-link-square"></i> + <i class="fa fa-eye"></i> + <i class="fa fa-eye-slash"></i> + <i class="fa fa-eyedropper"></i> + <i class="fa fa-fa"></i> + <i class="fa fa-facebook"></i> + <i class="fa fa-facebook-f"></i> + <i class="fa fa-facebook-official"></i> + <i class="fa fa-facebook-square"></i> + <i class="fa fa-fast-backward"></i> + <i class="fa fa-fast-forward"></i> + <i class="fa fa-fax"></i> + <i class="fa fa-feed"></i> + <i class="fa fa-female"></i> + <i class="fa fa-fighter-jet"></i> + <i class="fa fa-file"></i> + <i class="fa fa-file-archive-o"></i> + <i class="fa fa-file-audio-o"></i> + <i class="fa fa-file-code-o"></i> + <i class="fa fa-file-excel-o"></i> + <i class="fa fa-file-image-o"></i> + <i class="fa fa-file-movie-o"></i> + <i class="fa fa-file-o"></i> + <i class="fa fa-file-pdf-o"></i> + <i class="fa fa-file-photo-o"></i> + <i class="fa fa-file-picture-o"></i> + <i class="fa fa-file-powerpoint-o"></i> + <i class="fa fa-file-sound-o"></i> + <i class="fa fa-file-text"></i> + <i class="fa fa-file-text-o"></i> + <i class="fa fa-file-video-o"></i> + <i class="fa fa-file-word-o"></i> + <i class="fa fa-file-zip-o"></i> + <i class="fa fa-files-o"></i> + <i class="fa fa-film"></i> + <i class="fa fa-filter"></i> + <i class="fa fa-fire"></i> + <i class="fa fa-fire-extinguisher"></i> + <i class="fa fa-firefox"></i> + <i class="fa fa-first-order"></i> + <i class="fa fa-flag"></i> + <i class="fa fa-flag-checkered"></i> + <i class="fa fa-flag-o"></i> + <i class="fa fa-flash"></i> + <i class="fa fa-flask"></i> + <i class="fa fa-flickr"></i> + <i class="fa fa-floppy-o"></i> + <i class="fa fa-folder"></i> + <i class="fa fa-folder-o"></i> + <i class="fa fa-folder-open"></i> + <i class="fa fa-folder-open-o"></i> + <i class="fa fa-font"></i> + <i class="fa fa-font-awesome"></i> + <i class="fa fa-fonticons"></i> + <i class="fa fa-fort-awesome"></i> + <i class="fa fa-forumbee"></i> + <i class="fa fa-forward"></i> + <i class="fa fa-foursquare"></i> + <i class="fa fa-frown-o"></i> + <i class="fa fa-futbol-o"></i> + <i class="fa fa-gamepad"></i> + <i class="fa fa-gavel"></i> + <i class="fa fa-gbp"></i> + <i class="fa fa-ge"></i> + <i class="fa fa-gear"></i> + <i class="fa fa-gears"></i> + <i class="fa fa-genderless"></i> + <i class="fa fa-get-pocket"></i> + <i class="fa fa-gg"></i> + <i class="fa fa-gg-circle"></i> + <i class="fa fa-gift"></i> + <i class="fa fa-git"></i> + <i class="fa fa-git-square"></i> + <i class="fa fa-github"></i> + <i class="fa fa-github-alt"></i> + <i class="fa fa-github-square"></i> + <i class="fa fa-gitlab"></i> + <i class="fa fa-gittip"></i> + <i class="fa fa-glass"></i> + <i class="fa fa-glide"></i> + <i class="fa fa-glide-g"></i> + <i class="fa fa-globe"></i> + <i class="fa fa-google"></i> + <i class="fa fa-google-plus"></i> + <i class="fa fa-google-plus-circle"></i> + <i class="fa fa-google-plus-official"></i> + <i class="fa fa-google-plus-square"></i> + <i class="fa fa-google-wallet"></i> + <i class="fa fa-graduation-cap"></i> + <i class="fa fa-gratipay"></i> + <i class="fa fa-group"></i> + <i class="fa fa-h-square"></i> + <i class="fa fa-hacker-news"></i> + <i class="fa fa-hand-grab-o"></i> + <i class="fa fa-hand-lizard-o"></i> + <i class="fa fa-hand-o-down"></i> + <i class="fa fa-hand-o-left"></i> + <i class="fa fa-hand-o-right"></i> + <i class="fa fa-hand-o-up"></i> + <i class="fa fa-hand-paper-o"></i> + <i class="fa fa-hand-peace-o"></i> + <i class="fa fa-hand-pointer-o"></i> + <i class="fa fa-hand-rock-o"></i> + <i class="fa fa-hand-scissors-o"></i> + <i class="fa fa-hand-spock-o"></i> + <i class="fa fa-hand-stop-o"></i> + <i class="fa fa-hard-of-hearing"></i> + <i class="fa fa-hashtag"></i> + <i class="fa fa-hdd-o"></i> + <i class="fa fa-header"></i> + <i class="fa fa-headphones"></i> + <i class="fa fa-heart"></i> + <i class="fa fa-heart-o"></i> + <i class="fa fa-heartbeat"></i> + <i class="fa fa-history"></i> + <i class="fa fa-home"></i> + <i class="fa fa-hospital-o"></i> + <i class="fa fa-hotel"></i> + <i class="fa fa-hourglass"></i> + <i class="fa fa-hourglass-1"></i> + <i class="fa fa-hourglass-2"></i> + <i class="fa fa-hourglass-3"></i> + <i class="fa fa-hourglass-end"></i> + <i class="fa fa-hourglass-half"></i> + <i class="fa fa-hourglass-o"></i> + <i class="fa fa-hourglass-start"></i> + <i class="fa fa-houzz"></i> + <i class="fa fa-html5"></i> + <i class="fa fa-i-cursor"></i> + <i class="fa fa-ils"></i> + <i class="fa fa-image"></i> + <i class="fa fa-inbox"></i> + <i class="fa fa-indent"></i> + <i class="fa fa-industry"></i> + <i class="fa fa-info"></i> + <i class="fa fa-info-circle"></i> + <i class="fa fa-inr"></i> + <i class="fa fa-instagram"></i> + <i class="fa fa-institution"></i> + <i class="fa fa-internet-explorer"></i> + <i class="fa fa-intersex"></i> + <i class="fa fa-ioxhost"></i> + <i class="fa fa-italic"></i> + <i class="fa fa-joomla"></i> + <i class="fa fa-jpy"></i> + <i class="fa fa-jsfiddle"></i> + <i class="fa fa-key"></i> + <i class="fa fa-keyboard-o"></i> + <i class="fa fa-krw"></i> + <i class="fa fa-language"></i> + <i class="fa fa-laptop"></i> + <i class="fa fa-lastfm"></i> + <i class="fa fa-lastfm-square"></i> + <i class="fa fa-leaf"></i> + <i class="fa fa-leanpub"></i> + <i class="fa fa-legal"></i> + <i class="fa fa-lemon-o"></i> + <i class="fa fa-level-down"></i> + <i class="fa fa-level-up"></i> + <i class="fa fa-life-bouy"></i> + <i class="fa fa-life-buoy"></i> + <i class="fa fa-life-ring"></i> + <i class="fa fa-life-saver"></i> + <i class="fa fa-lightbulb-o"></i> + <i class="fa fa-line-chart"></i> + <i class="fa fa-link"></i> + <i class="fa fa-linkedin"></i> + <i class="fa fa-linkedin-square"></i> + <i class="fa fa-linux"></i> + <i class="fa fa-list"></i> + <i class="fa fa-list-alt"></i> + <i class="fa fa-list-ol"></i> + <i class="fa fa-list-ul"></i> + <i class="fa fa-location-arrow"></i> + <i class="fa fa-lock"></i> + <i class="fa fa-long-arrow-down"></i> + <i class="fa fa-long-arrow-left"></i> + <i class="fa fa-long-arrow-right"></i> + <i class="fa fa-long-arrow-up"></i> + <i class="fa fa-low-vision"></i> + <i class="fa fa-magic"></i> + <i class="fa fa-magnet"></i> + <i class="fa fa-mail-forward"></i> + <i class="fa fa-mail-reply"></i> + <i class="fa fa-mail-reply-all"></i> + <i class="fa fa-male"></i> + <i class="fa fa-map"></i> + <i class="fa fa-map-marker"></i> + <i class="fa fa-map-o"></i> + <i class="fa fa-map-pin"></i> + <i class="fa fa-map-signs"></i> + <i class="fa fa-mars"></i> + <i class="fa fa-mars-double"></i> + <i class="fa fa-mars-stroke"></i> + <i class="fa fa-mars-stroke-h"></i> + <i class="fa fa-mars-stroke-v"></i> + <i class="fa fa-maxcdn"></i> + <i class="fa fa-meanpath"></i> + <i class="fa fa-medium"></i> + <i class="fa fa-medkit"></i> + <i class="fa fa-meh-o"></i> + <i class="fa fa-mercury"></i> + <i class="fa fa-microphone"></i> + <i class="fa fa-microphone-slash"></i> + <i class="fa fa-minus"></i> + <i class="fa fa-minus-circle"></i> + <i class="fa fa-minus-square"></i> + <i class="fa fa-minus-square-o"></i> + <i class="fa fa-mixcloud"></i> + <i class="fa fa-mobile"></i> + <i class="fa fa-mobile-phone"></i> + <i class="fa fa-modx"></i> + <i class="fa fa-money"></i> + <i class="fa fa-moon-o"></i> + <i class="fa fa-mortar-board"></i> + <i class="fa fa-motorcycle"></i> + <i class="fa fa-mouse-pointer"></i> + <i class="fa fa-music"></i> + <i class="fa fa-navicon"></i> + <i class="fa fa-neuter"></i> + <i class="fa fa-newspaper-o"></i> + <i class="fa fa-object-group"></i> + <i class="fa fa-object-ungroup"></i> + <i class="fa fa-odnoklassniki"></i> + <i class="fa fa-odnoklassniki-square"></i> + <i class="fa fa-opencart"></i> + <i class="fa fa-openid"></i> + <i class="fa fa-opera"></i> + <i class="fa fa-optin-monster"></i> + <i class="fa fa-outdent"></i> + <i class="fa fa-pagelines"></i> + <i class="fa fa-paint-brush"></i> + <i class="fa fa-paper-plane"></i> + <i class="fa fa-paper-plane-o"></i> + <i class="fa fa-paperclip"></i> + <i class="fa fa-paragraph"></i> + <i class="fa fa-paste"></i> + <i class="fa fa-pause"></i> + <i class="fa fa-pause-circle"></i> + <i class="fa fa-pause-circle-o"></i> + <i class="fa fa-paw"></i> + <i class="fa fa-paypal"></i> + <i class="fa fa-pencil"></i> + <i class="fa fa-pencil-square"></i> + <i class="fa fa-pencil-square-o"></i> + <i class="fa fa-percent"></i> + <i class="fa fa-phone"></i> + <i class="fa fa-phone-square"></i> + <i class="fa fa-photo"></i> + <i class="fa fa-picture-o"></i> + <i class="fa fa-pie-chart"></i> + <i class="fa fa-pied-piper"></i> + <i class="fa fa-pied-piper-alt"></i> + <i class="fa fa-pied-piper-pp"></i> + <i class="fa fa-pinterest"></i> + <i class="fa fa-pinterest-p"></i> + <i class="fa fa-pinterest-square"></i> + <i class="fa fa-plane"></i> + <i class="fa fa-play"></i> + <i class="fa fa-play-circle"></i> + <i class="fa fa-play-circle-o"></i> + <i class="fa fa-plug"></i> + <i class="fa fa-plus"></i> + <i class="fa fa-plus-circle"></i> + <i class="fa fa-plus-square"></i> + <i class="fa fa-plus-square-o"></i> + <i class="fa fa-power-off"></i> + <i class="fa fa-print"></i> + <i class="fa fa-product-hunt"></i> + <i class="fa fa-puzzle-piece"></i> + <i class="fa fa-qq"></i> + <i class="fa fa-qrcode"></i> + <i class="fa fa-question"></i> + <i class="fa fa-question-circle"></i> + <i class="fa fa-question-circle-o"></i> + <i class="fa fa-quote-left"></i> + <i class="fa fa-quote-right"></i> + <i class="fa fa-ra"></i> + <i class="fa fa-random"></i> + <i class="fa fa-rebel"></i> + <i class="fa fa-recycle"></i> + <i class="fa fa-reddit"></i> + <i class="fa fa-reddit-alien"></i> + <i class="fa fa-reddit-square"></i> + <i class="fa fa-refresh"></i> + <i class="fa fa-registered"></i> + <i class="fa fa-remove"></i> + <i class="fa fa-renren"></i> + <i class="fa fa-reorder"></i> + <i class="fa fa-repeat"></i> + <i class="fa fa-reply"></i> + <i class="fa fa-reply-all"></i> + <i class="fa fa-resistance"></i> + <i class="fa fa-retweet"></i> + <i class="fa fa-rmb"></i> + <i class="fa fa-road"></i> + <i class="fa fa-rocket"></i> + <i class="fa fa-rotate-left"></i> + <i class="fa fa-rotate-right"></i> + <i class="fa fa-rouble"></i> + <i class="fa fa-rss"></i> + <i class="fa fa-rss-square"></i> + <i class="fa fa-rub"></i> + <i class="fa fa-ruble"></i> + <i class="fa fa-rupee"></i> + <i class="fa fa-safari"></i> + <i class="fa fa-save"></i> + <i class="fa fa-scissors"></i> + <i class="fa fa-scribd"></i> + <i class="fa fa-search"></i> + <i class="fa fa-search-minus"></i> + <i class="fa fa-search-plus"></i> + <i class="fa fa-sellsy"></i> + <i class="fa fa-send"></i> + <i class="fa fa-send-o"></i> + <i class="fa fa-server"></i> + <i class="fa fa-share"></i> + <i class="fa fa-share-alt"></i> + <i class="fa fa-share-alt-square"></i> + <i class="fa fa-share-square"></i> + <i class="fa fa-share-square-o"></i> + <i class="fa fa-shekel"></i> + <i class="fa fa-sheqel"></i> + <i class="fa fa-shield"></i> + <i class="fa fa-ship"></i> + <i class="fa fa-shirtsinbulk"></i> + <i class="fa fa-shopping-bag"></i> + <i class="fa fa-shopping-basket"></i> + <i class="fa fa-shopping-cart"></i> + <i class="fa fa-sign-in"></i> + <i class="fa fa-sign-language"></i> + <i class="fa fa-sign-out"></i> + <i class="fa fa-signal"></i> + <i class="fa fa-signing"></i> + <i class="fa fa-simplybuilt"></i> + <i class="fa fa-sitemap"></i> + <i class="fa fa-skyatlas"></i> + <i class="fa fa-skype"></i> + <i class="fa fa-slack"></i> + <i class="fa fa-sliders"></i> + <i class="fa fa-slideshare"></i> + <i class="fa fa-smile-o"></i> + <i class="fa fa-snapchat"></i> + <i class="fa fa-snapchat-ghost"></i> + <i class="fa fa-snapchat-square"></i> + <i class="fa fa-soccer-ball-o"></i> + <i class="fa fa-sort"></i> + <i class="fa fa-sort-alpha-asc"></i> + <i class="fa fa-sort-alpha-desc"></i> + <i class="fa fa-sort-amount-asc"></i> + <i class="fa fa-sort-amount-desc"></i> + <i class="fa fa-sort-asc"></i> + <i class="fa fa-sort-desc"></i> + <i class="fa fa-sort-down"></i> + <i class="fa fa-sort-numeric-asc"></i> + <i class="fa fa-sort-numeric-desc"></i> + <i class="fa fa-sort-up"></i> + <i class="fa fa-soundcloud"></i> + <i class="fa fa-space-shuttle"></i> + <i class="fa fa-spinner"></i> + <i class="fa fa-spoon"></i> + <i class="fa fa-spotify"></i> + <i class="fa fa-square"></i> + <i class="fa fa-square-o"></i> + <i class="fa fa-stack-exchange"></i> + <i class="fa fa-stack-overflow"></i> + <i class="fa fa-star"></i> + <i class="fa fa-star-half"></i> + <i class="fa fa-star-half-empty"></i> + <i class="fa fa-star-half-full"></i> + <i class="fa fa-star-half-o"></i> + <i class="fa fa-star-o"></i> + <i class="fa fa-steam"></i> + <i class="fa fa-steam-square"></i> + <i class="fa fa-step-backward"></i> + <i class="fa fa-step-forward"></i> + <i class="fa fa-stethoscope"></i> + <i class="fa fa-sticky-note"></i> + <i class="fa fa-sticky-note-o"></i> + <i class="fa fa-stop"></i> + <i class="fa fa-stop-circle"></i> + <i class="fa fa-stop-circle-o"></i> + <i class="fa fa-street-view"></i> + <i class="fa fa-strikethrough"></i> + <i class="fa fa-stumbleupon"></i> + <i class="fa fa-stumbleupon-circle"></i> + <i class="fa fa-subscript"></i> + <i class="fa fa-subway"></i> + <i class="fa fa-suitcase"></i> + <i class="fa fa-sun-o"></i> + <i class="fa fa-superscript"></i> + <i class="fa fa-support"></i> + <i class="fa fa-table"></i> + <i class="fa fa-tablet"></i> + <i class="fa fa-tachometer"></i> + <i class="fa fa-tag"></i> + <i class="fa fa-tags"></i> + <i class="fa fa-tasks"></i> + <i class="fa fa-taxi"></i> + <i class="fa fa-television"></i> + <i class="fa fa-tencent-weibo"></i> + <i class="fa fa-terminal"></i> + <i class="fa fa-text-height"></i> + <i class="fa fa-text-width"></i> + <i class="fa fa-th"></i> + <i class="fa fa-th-large"></i> + <i class="fa fa-th-list"></i> + <i class="fa fa-themeisle"></i> + <i class="fa fa-thumb-tack"></i> + <i class="fa fa-thumbs-down"></i> + <i class="fa fa-thumbs-o-down"></i> + <i class="fa fa-thumbs-o-up"></i> + <i class="fa fa-thumbs-up"></i> + <i class="fa fa-ticket"></i> + <i class="fa fa-times"></i> + <i class="fa fa-times-circle"></i> + <i class="fa fa-times-circle-o"></i> + <i class="fa fa-tint"></i> + <i class="fa fa-toggle-down"></i> + <i class="fa fa-toggle-left"></i> + <i class="fa fa-toggle-off"></i> + <i class="fa fa-toggle-on"></i> + <i class="fa fa-toggle-right"></i> + <i class="fa fa-toggle-up"></i> + <i class="fa fa-trademark"></i> + <i class="fa fa-train"></i> + <i class="fa fa-transgender"></i> + <i class="fa fa-transgender-alt"></i> + <i class="fa fa-trash"></i> + <i class="fa fa-trash-o"></i> + <i class="fa fa-tree"></i> + <i class="fa fa-trello"></i> + <i class="fa fa-tripadvisor"></i> + <i class="fa fa-trophy"></i> + <i class="fa fa-truck"></i> + <i class="fa fa-try"></i> + <i class="fa fa-tty"></i> + <i class="fa fa-tumblr"></i> + <i class="fa fa-tumblr-square"></i> + <i class="fa fa-turkish-lira"></i> + <i class="fa fa-tv"></i> + <i class="fa fa-twitch"></i> + <i class="fa fa-twitter"></i> + <i class="fa fa-twitter-square"></i> + <i class="fa fa-umbrella"></i> + <i class="fa fa-underline"></i> + <i class="fa fa-undo"></i> + <i class="fa fa-universal-access"></i> + <i class="fa fa-university"></i> + <i class="fa fa-unlink"></i> + <i class="fa fa-unlock"></i> + <i class="fa fa-unlock-alt"></i> + <i class="fa fa-unsorted"></i> + <i class="fa fa-upload"></i> + <i class="fa fa-usb"></i> + <i class="fa fa-usd"></i> + <i class="fa fa-user"></i> + <i class="fa fa-user-md"></i> + <i class="fa fa-user-plus"></i> + <i class="fa fa-user-secret"></i> + <i class="fa fa-user-times"></i> + <i class="fa fa-users"></i> + <i class="fa fa-venus"></i> + <i class="fa fa-venus-double"></i> + <i class="fa fa-venus-mars"></i> + <i class="fa fa-viacoin"></i> + <i class="fa fa-viadeo"></i> + <i class="fa fa-viadeo-square"></i> + <i class="fa fa-video-camera"></i> + <i class="fa fa-vimeo"></i> + <i class="fa fa-vimeo-square"></i> + <i class="fa fa-vine"></i> + <i class="fa fa-vk"></i> + <i class="fa fa-volume-control-phone"></i> + <i class="fa fa-volume-down"></i> + <i class="fa fa-volume-off"></i> + <i class="fa fa-volume-up"></i> + <i class="fa fa-warning"></i> + <i class="fa fa-wechat"></i> + <i class="fa fa-weibo"></i> + <i class="fa fa-weixin"></i> + <i class="fa fa-whatsapp"></i> + <i class="fa fa-wheelchair"></i> + <i class="fa fa-wheelchair-alt"></i> + <i class="fa fa-wifi"></i> + <i class="fa fa-wikipedia-w"></i> + <i class="fa fa-windows"></i> + <i class="fa fa-won"></i> + <i class="fa fa-wordpress"></i> + <i class="fa fa-wpbeginner"></i> + <i class="fa fa-wpforms"></i> + <i class="fa fa-wrench"></i> + <i class="fa fa-xing"></i> + <i class="fa fa-xing-square"></i> + <i class="fa fa-y-combinator"></i> + <i class="fa fa-y-combinator-square"></i> + <i class="fa fa-yahoo"></i> + <i class="fa fa-yc"></i> + <i class="fa fa-yc-square"></i> + <i class="fa fa-yelp"></i> + <i class="fa fa-yen"></i> + <i class="fa fa-yoast"></i> + <i class="fa fa-youtube"></i> + <i class="fa fa-youtube-play"></i> + <i class="fa fa-youtube-square"></i> + </div> <p class="help-block text-center"> For a full list of icons, please consult: diff --git a/src/views/partials/requirejs-config.tpl b/src/views/partials/requirejs-config.tpl deleted file mode 100644 index 9ad5ea48fc..0000000000 --- a/src/views/partials/requirejs-config.tpl +++ /dev/null @@ -1,13 +0,0 @@ -<script> - require.config({ - baseUrl: "{relative_path}/src/modules", - waitSeconds: 3, - urlArgs: "{config.cache-buster}", - paths: { - 'forum': '../client', - 'admin': '../admin', - 'vendor': '../../vendor', - 'mousetrap': '../../bower/mousetrap/mousetrap' - } - }); -</script> \ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index 057c9a5e8a..3436f575c8 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -1,24 +1,35 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - express = require('express'), - app = express(), - server, - winston = require('winston'), - async = require('async'), - - emailer = require('./emailer'), - meta = require('./meta'), - logger = require('./logger'), - plugins = require('./plugins'), - middleware = require('./middleware'), - routes = require('./routes'), - emitter = require('./emitter'), - - helpers = require('../public/src/modules/helpers'); +var fs = require('fs'); +var path = require('path'); +var nconf = require('nconf'); +var express = require('express'); +var app = express(); +var server; +var winston = require('winston'); +var async = require('async'); +var flash = require('connect-flash'); +var compression = require('compression'); +var bodyParser = require('body-parser'); +var cookieParser = require('cookie-parser'); +var session = require('express-session'); +var useragent = require('express-useragent'); +var favicon = require('serve-favicon'); + +var db = require('./database'); +var file = require('./file'); +var emailer = require('./emailer'); +var meta = require('./meta'); +var languages = require('./languages'); +var logger = require('./logger'); +var plugins = require('./plugins'); +var routes = require('./routes'); +var auth = require('./routes/authentication'); +var emitter = require('./emitter'); +var templates = require('templates.js'); + +var helpers = require('../public/src/modules/helpers'); if (nconf.get('ssl')) { server = require('https').createServer({ @@ -31,7 +42,7 @@ if (nconf.get('ssl')) { module.exports.server = server; -server.on('error', function(err) { +server.on('error', function (err) { winston.error(err); if (err.code === 'EADDRINUSE') { winston.error('NodeBB address in use, exiting...'); @@ -42,22 +53,22 @@ server.on('error', function(err) { }); -module.exports.listen = function() { +module.exports.listen = function () { emailer.registerApp(app); - middleware = middleware(app); + setupExpressApp(app); helpers.register(); logger.init(app); - emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function() { + emitter.all(['templates:compiled', 'meta:js.compiled', 'meta:css.compiled'], function () { winston.info('NodeBB Ready'); emitter.emit('nodebb:ready'); listen(); }); - initializeNodeBB(function(err) { + initializeNodeBB(function (err) { if (err) { winston.error(err); process.exit(); @@ -70,8 +81,85 @@ module.exports.listen = function() { }); }; +function setupExpressApp(app) { + var middleware = require('./middleware'); + + var relativePath = nconf.get('relative_path'); + + app.engine('tpl', templates.__express); + app.set('view engine', 'tpl'); + app.set('views', nconf.get('views_dir')); + app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); + app.use(flash()); + + app.enable('view cache'); + + if (global.env !== 'development') { + app.enable('cache'); + app.enable('minification'); + } + + app.use(compression()); + + setupFavicon(app); + + app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon); + + app.use(bodyParser.urlencoded({extended: true})); + app.use(bodyParser.json()); + app.use(cookieParser()); + app.use(useragent.express()); + + app.use(session({ + store: db.sessionStore, + secret: nconf.get('secret'), + key: nconf.get('sessionKey'), + cookie: setupCookie(), + resave: true, + saveUninitialized: true + })); + + app.use(middleware.addHeaders); + app.use(middleware.processRender); + auth.initialize(app, middleware); + + var toobusy = require('toobusy-js'); + toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); + toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); +} + +function setupFavicon(app) { + var faviconPath = path.join(nconf.get('base_dir'), 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico'); + if (file.existsSync(faviconPath)) { + app.use(nconf.get('relative_path'), favicon(faviconPath)); + } +} + +function setupCookie() { + var cookie = { + maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14) + }; + + if (meta.config.cookieDomain) { + cookie.domain = meta.config.cookieDomain; + } + + if (nconf.get('secure')) { + cookie.secure = true; + } + + var relativePath = nconf.get('relative_path'); + if (relativePath !== '') { + cookie.path = relativePath; + } + + return cookie; +} + function initializeNodeBB(callback) { - var skipJS, skipLess, fromFile = nconf.get('from-file') || ''; + var skipJS; + var fromFile = nconf.get('from-file') || ''; + var middleware = require('./middleware'); if (fromFile.match('js')) { winston.info('[minifier] Minifying client-side JS skipped'); @@ -79,46 +167,41 @@ function initializeNodeBB(callback) { } async.waterfall([ - async.apply(cacheStaticFiles), async.apply(meta.themes.setupPaths), - function(next) { + function (next) { plugins.init(app, middleware, next); }, - function(next) { + async.apply(plugins.fireHook, 'static:assets.prepare', {}), + async.apply(meta.js.bridgeModules, app), + function (next) { async.series([ async.apply(meta.templates.compile), async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'nodebb.min.js'), async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'acp.min.js'), async.apply(meta.css.minify), async.apply(meta.sounds.init), + async.apply(languages.init), async.apply(meta.blacklist.load) ], next); }, - function(results, next) { + function (results, next) { plugins.fireHook('static:app.preload', { app: app, middleware: middleware }, next); }, - function(next) { - routes(app, middleware); + async.apply(plugins.fireHook, 'filter:hotswap.prepare', []), + function (hotswapIds, next) { + routes(app, middleware, hotswapIds); next(); } ], callback); } -function cacheStaticFiles(callback) { - if (global.env === 'development') { - return callback(); - } - - app.enable('cache'); - app.enable('minification'); - callback(); -} - -function listen(callback) { +function listen() { var port = parseInt(nconf.get('port'), 10); + var isSocket = isNaN(port); + var socketPath = isSocket ? nconf.get('port') : ''; if (Array.isArray(port)) { if (!port.length) { @@ -144,18 +227,18 @@ function listen(callback) { winston.info('Using ports 80 and 443 is not recommend; use a proxy instead. See README.md'); } - var isSocket = isNaN(port), - args = isSocket ? [port] : [port, nconf.get('bind_address')], - bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port, - oldUmask; - args.push(function(err) { + var args = isSocket ? [socketPath] : [port, nconf.get('bind_address')]; + var bind_address = ((nconf.get('bind_address') === "0.0.0.0" || !nconf.get('bind_address')) ? '0.0.0.0' : nconf.get('bind_address')) + ':' + port; + var oldUmask; + + args.push(function (err) { if (err) { winston.info('[startup] NodeBB was unable to listen on: ' + bind_address); process.exit(); } - winston.info('NodeBB is now listening on: ' + (isSocket ? port : bind_address)); + winston.info('NodeBB is now listening on: ' + (isSocket ? socketPath : bind_address)); if (oldUmask) { process.umask(oldUmask); } @@ -164,11 +247,11 @@ function listen(callback) { // Alter umask if necessary if (isSocket) { oldUmask = process.umask('0000'); - module.exports.testSocket(port, function(err) { + module.exports.testSocket(socketPath, function (err) { if (!err) { server.listen.apply(server, args); } else { - winston.error('[startup] NodeBB was unable to secure domain socket access (' + port + ')'); + winston.error('[startup] NodeBB was unable to secure domain socket access (' + socketPath + ')'); winston.error('[startup] ' + err.message); process.exit(); } @@ -178,15 +261,15 @@ function listen(callback) { } } -module.exports.testSocket = function(socketPath, callback) { +module.exports.testSocket = function (socketPath, callback) { if (typeof socketPath !== 'string') { return callback(new Error('invalid socket path : ' + socketPath)); } var net = require('net'); var file = require('./file'); async.series([ - function(next) { - file.exists(socketPath, function(exists) { + function (next) { + file.exists(socketPath, function (exists) { if (exists) { next(); } else { @@ -194,12 +277,12 @@ module.exports.testSocket = function(socketPath, callback) { } }); }, - function(next) { + function (next) { var testSocket = new net.Socket(); - testSocket.on('error', function(err) { + testSocket.on('error', function (err) { next(err.code !== 'ECONNREFUSED' ? err : null); }); - testSocket.connect({ path: socketPath }, function() { + testSocket.connect({ path: socketPath }, function () { // Something's listening here, abort callback(new Error('port-in-use')); }); diff --git a/src/widgets/admin.js b/src/widgets/admin.js index 110f523e21..fc380804c6 100644 --- a/src/widgets/admin.js +++ b/src/widgets/admin.js @@ -1,14 +1,15 @@ "use strict"; - -var async = require('async'), - plugins = require('../plugins'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var plugins = require('../plugins'); var admin = {}; -admin.get = function(callback) { +admin.get = function (callback) { async.parallel({ - areas: function(next) { + areas: function (next) { var defaultAreas = [ { name: 'Global Sidebar', template: 'global', location: 'sidebar' }, { name: 'Global Header', template: 'global', location: 'header' }, @@ -20,36 +21,36 @@ admin.get = function(callback) { plugins.fireHook('filter:widgets.getAreas', defaultAreas, next); }, - widgets: function(next) { + widgets: function (next) { plugins.fireHook('filter:widgets.getWidgets', [], next); + }, + adminTemplate: function (next) { + fs.readFile(path.resolve(__dirname, '../../public/templates/admin/partials/widget-settings.tpl'), 'utf8', next); } - }, function(err, widgetData) { + }, function (err, widgetData) { if (err) { return callback(err); } widgetData.areas.push({ name: 'Draft Zone', template: 'global', location: 'drafts' }); - async.each(widgetData.areas, function(area, next) { - require('./index').getArea(area.template, area.location, function(err, areaData) { + async.each(widgetData.areas, function (area, next) { + require('./index').getArea(area.template, area.location, function (err, areaData) { area.data = areaData; next(err); }); - - }, function(err) { + }, function (err) { if (err) { return callback(err); } - for (var w in widgetData.widgets) { - if (widgetData.widgets.hasOwnProperty(w)) { - // if this gets anymore complicated, it needs to be a template - widgetData.widgets[w].content += "<br /><label>Title:</label><input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Title (only shown on some containers)\" /><br /><label>Container:</label><textarea rows=\"4\" class=\"form-control container-html\" name=\"container\" placeholder=\"Drag and drop a container or enter HTML here.\"></textarea><div class=\"checkbox\"><label><input name=\"hide-guests\" type=\"checkbox\"> Hide from anonymous users?</label></div><div class=\"checkbox\"><label><input name=\"hide-registered\" type=\"checkbox\"> Hide from registered users?</input></label></div>"; - } - } + + widgetData.widgets.forEach(function (w) { + w.content += widgetData.adminTemplate; + }); var templates = [], list = {}, index = 0; - widgetData.areas.forEach(function(area) { + widgetData.areas.forEach(function (area) { if (typeof list[area.template] === 'undefined') { list[area.template] = index; templates.push({ diff --git a/src/widgets/index.js b/src/widgets/index.js index c1421b44b6..eac58e46e3 100644 --- a/src/widgets/index.js +++ b/src/widgets/index.js @@ -1,36 +1,39 @@ "use strict"; -var async = require('async'), - winston = require('winston'), - templates = require('templates.js'), +var async = require('async'); +var winston = require('winston'); +var templates = require('templates.js'); - plugins = require('../plugins'), - translator = require('../../public/src/modules/translator'), - db = require('../database'); +var plugins = require('../plugins'); +var translator = require('../../public/src/modules/translator'); +var db = require('../database'); var widgets = {}; -widgets.render = function(uid, area, req, res, callback) { +widgets.render = function (uid, area, req, res, callback) { if (!area.locations || !area.template) { return callback(new Error('[[error:invalid-data]]')); } - widgets.getAreas(['global', area.template], area.locations, function(err, data) { + widgets.getAreas(['global', area.template], area.locations, function (err, data) { if (err) { return callback(err); } var widgetsByLocation = {}; - async.map(area.locations, function(location, done) { + async.map(area.locations, function (location, done) { widgetsByLocation[location] = data.global[location].concat(data[area.template][location]); if (!widgetsByLocation[location].length) { return done(null, {location: location, widgets: []}); } - async.map(widgetsByLocation[location], function(widget, next) { - if (!widget || !widget.data || (!!widget.data['hide-registered'] && uid !== 0) || (!!widget.data['hide-guests'] && uid === 0)) { + async.map(widgetsByLocation[location], function (widget, next) { + if (!widget || !widget.data || + (!!widget.data['hide-registered'] && uid !== 0) || + (!!widget.data['hide-guests'] && uid === 0) || + (!!widget.data['hide-mobile'] && area.isMobile)) { return next(); } @@ -40,8 +43,8 @@ widgets.render = function(uid, area, req, res, callback) { data: widget.data, req: req, res: res - }, function(err, html) { - if (err) { + }, function (err, html) { + if (err || html === null) { return next(err); } @@ -50,7 +53,7 @@ widgets.render = function(uid, area, req, res, callback) { } if (widget.data.container && widget.data.container.match('{body}')) { - translator.translate(widget.data.title, function(title) { + translator.translate(widget.data.title, function (title) { html = templates.parse(widget.data.container, { title: title, body: html @@ -62,27 +65,27 @@ widgets.render = function(uid, area, req, res, callback) { next(null, {html: html}); } }); - }, function(err, result) { + }, function (err, result) { done(err, {location: location, widgets: result.filter(Boolean)}); }); }, callback); }); }; -widgets.getAreas = function(templates, locations, callback) { - var keys = templates.map(function(tpl) { +widgets.getAreas = function (templates, locations, callback) { + var keys = templates.map(function (tpl) { return 'widgets:' + tpl; }); - db.getObjectsFields(keys, locations, function(err, data) { + db.getObjectsFields(keys, locations, function (err, data) { if (err) { return callback(err); } var returnData = {}; - templates.forEach(function(template, index) { + templates.forEach(function (template, index) { returnData[template] = returnData[template] || {}; - locations.forEach(function(location) { + locations.forEach(function (location) { if (data && data[index] && data[index][location]) { try { returnData[template][location] = JSON.parse(data[index][location]); @@ -100,8 +103,8 @@ widgets.getAreas = function(templates, locations, callback) { }); }; -widgets.getArea = function(template, location, callback) { - db.getObjectField('widgets:' + template, location, function(err, result) { +widgets.getArea = function (template, location, callback) { + db.getObjectField('widgets:' + template, location, function (err, result) { if (err) { return callback(err); } @@ -118,7 +121,7 @@ widgets.getArea = function(template, location, callback) { }); }; -widgets.setArea = function(area, callback) { +widgets.setArea = function (area, callback) { if (!area.location || !area.template) { return callback(new Error('Missing location and template data')); } @@ -126,21 +129,29 @@ widgets.setArea = function(area, callback) { db.setObjectField('widgets:' + area.template, area.location, JSON.stringify(area.widgets), callback); }; -widgets.reset = function(callback) { +widgets.reset = function (callback) { var defaultAreas = [ { name: 'Draft Zone', template: 'global', location: 'header' }, { name: 'Draft Zone', template: 'global', location: 'footer' }, { name: 'Draft Zone', template: 'global', location: 'sidebar' } ]; - plugins.fireHook('filter:widgets.getAreas', defaultAreas, function(err, areas) { + async.parallel({ + areas: function (next) { + plugins.fireHook('filter:widgets.getAreas', defaultAreas, next); + }, + drafts: function (next) { + widgets.getArea('global', 'drafts', next); + } + }, function (err, results) { if (err) { return callback(err); } - var drafts = []; - async.each(areas, function(area, next) { - widgets.getArea(area.template, area.location, function(err, areaData) { + var drafts = results.drafts || []; + + async.each(results.areas, function (area, next) { + widgets.getArea(area.template, area.location, function (err, areaData) { if (err) { return next(err); } @@ -149,7 +160,7 @@ widgets.reset = function(callback) { area.widgets = []; widgets.setArea(area, next); }); - }, function(err) { + }, function (err) { if (err) { return callback(err); } diff --git a/tests/.jshintrc b/test/.jshintrc similarity index 100% rename from tests/.jshintrc rename to test/.jshintrc diff --git a/test/categories.js b/test/categories.js new file mode 100644 index 0000000000..86c158eb3e --- /dev/null +++ b/test/categories.js @@ -0,0 +1,179 @@ +'use strict'; +/*global require, after, before*/ + + +var async = require('async'); +var assert = require('assert'); + +var db = require('./mocks/databasemock'); +var Categories = require('../src/categories'); +var Topics = require('../src/topics'); +var User = require('../src/user'); + +describe('Categories', function () { + var categoryObj; + var posterUid; + + before(function (done) { + User.create({username: 'poster'}, function (err, _posterUid) { + if (err) { + return done(err); + } + + posterUid = _posterUid; + + done(); + }); + }); + + describe('.create', function () { + it('should create a new category', function (done) { + + Categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + icon: 'fa-check', + blockclass: 'category-blue', + order: '5' + }, function (err, category) { + assert.equal(err, null); + + categoryObj = category; + done.apply(this, arguments); + }); + }); + }); + + describe('.getCategoryById', function () { + it('should retrieve a newly created category by its ID', function (done) { + Categories.getCategoryById({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: -1, + uid: 0 + }, function (err, categoryData) { + assert.equal(err, null); + + assert(categoryData); + assert.equal(categoryObj.name, categoryData.name); + assert.equal(categoryObj.description, categoryData.description); + + done(); + }); + }); + }); + + describe('Categories.getRecentTopicReplies', function () { + it('should not throw', function (done) { + Categories.getCategoryById({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: -1, + uid: 0 + }, function (err, categoryData) { + assert.ifError(err); + Categories.getRecentTopicReplies(categoryData, 0, function (err) { + assert.ifError(err); + done(); + }); + }); + }); + }); + + describe('.getCategoryTopics', function () { + it('should return a list of topics', function (done) { + Categories.getCategoryTopics({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':tids', + reverse: true, + start: 0, + stop: 10, + uid: 0 + }, function (err, result) { + assert.equal(err, null); + + assert(Array.isArray(result.topics)); + assert(result.topics.every(function (topic) { + return topic instanceof Object; + })); + + done(); + }); + }); + + it('should return a list of topics by a specific user', function (done) { + Categories.getCategoryTopics({ + cid: categoryObj.cid, + set: 'cid:' + categoryObj.cid + ':uid:' + 1 + ':tids', + reverse: true, + start: 0, + stop: 10, + uid: 0, + targetUid: 1 + }, function (err, result) { + assert.equal(err, null); + assert(Array.isArray(result.topics)); + assert(result.topics.every(function (topic) { + return topic instanceof Object && topic.uid === '1'; + })); + + done(); + }); + }); + }); + + describe('Categories.moveRecentReplies', function () { + var moveCid; + var moveTid; + before(function (done) { + async.parallel({ + category: function (next) { + Categories.create({ + name: 'Test Category 2', + description: 'Test category created by testing script' + }, next); + }, + topic: function (next) { + Topics.post({ + uid: posterUid, + cid: categoryObj.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }, next); + } + }, function (err, results) { + if (err) { + return done(err); + } + moveCid = results.category.cid; + moveTid = results.topic.topicData.tid; + Topics.reply({uid: posterUid, content: 'test post', tid: moveTid}, function (err) { + done(err); + }); + }); + }); + + it('should move posts from one category to another', function (done) { + Categories.moveRecentReplies(moveTid, categoryObj.cid, moveCid, function (err) { + assert.ifError(err); + db.getSortedSetRange('cid:' + categoryObj.cid + ':pids', 0, -1, function (err, pids) { + assert.ifError(err); + assert.equal(pids.length, 0); + db.getSortedSetRange('cid:' + moveCid + ':pids', 0, -1, function (err, pids) { + assert.ifError(err); + assert.equal(pids.length, 2); + done(); + }); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/tests/database.js b/test/database.js similarity index 74% rename from tests/database.js rename to test/database.js index 3f8c119002..2750973a0c 100644 --- a/tests/database.js +++ b/test/database.js @@ -5,9 +5,9 @@ var assert = require('assert'), db = require('./mocks/databasemock'); -describe('Test database', function() { - it('should work', function(){ - assert.doesNotThrow(function(){ +describe('Test database', function () { + it('should work', function (){ + assert.doesNotThrow(function (){ var db = require('./mocks/databasemock'); }); }); diff --git a/tests/database/hash.js b/test/database/hash.js similarity index 61% rename from tests/database/hash.js rename to test/database/hash.js index 469172ce70..b1fd87390d 100644 --- a/tests/database/hash.js +++ b/test/database/hash.js @@ -5,20 +5,20 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Hash methods', function() { +describe('Hash methods', function () { var testData = { name: 'baris', lastname: 'usakli', age: 99 }; - beforeEach(function(done) { + beforeEach(function (done) { db.setObject('hashTestObject', testData, done); }); - describe('setObject()', function() { - it('should create a object', function(done) { - db.setObject('testObject1', {foo: 'baris', bar: 99}, function(err) { + describe('setObject()', function () { + it('should create a object', function (done) { + db.setObject('testObject1', {foo: 'baris', bar: 99}, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -26,17 +26,17 @@ describe('Hash methods', function() { }); }); - describe('setObjectField()', function() { - it('should create a new object with field', function(done) { - db.setObjectField('testObject2', 'name', 'ginger', function(err) { + describe('setObjectField()', function () { + it('should create a new object with field', function (done) { + db.setObjectField('testObject2', 'name', 'ginger', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add a new field to an object', function(done) { - db.setObjectField('testObject2', 'type', 'cat', function(err) { + it('should add a new field to an object', function (done) { + db.setObjectField('testObject2', 'type', 'cat', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -44,9 +44,9 @@ describe('Hash methods', function() { }); }); - describe('getObject()', function() { - it('should return falsy if object does not exist', function(done) { - db.getObject('doesnotexist', function(err, data) { + describe('getObject()', function () { + it('should return falsy if object does not exist', function (done) { + db.getObject('doesnotexist', function (err, data) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!data, false); @@ -54,8 +54,8 @@ describe('Hash methods', function() { }); }); - it('should retrieve an object', function(done) { - db.getObject('hashTestObject', function(err, data) { + it('should retrieve an object', function (done) { + db.getObject('hashTestObject', function (err, data) { assert.equal(err, null); assert.equal(data.name, testData.name); assert.equal(data.age, testData.age); @@ -65,16 +65,16 @@ describe('Hash methods', function() { }); }); - describe('getObjects()', function() { - before(function(done) { + describe('getObjects()', function () { + before(function (done) { async.parallel([ async.apply(db.setObject, 'testObject4', {name: 'baris'}), async.apply(db.setObjectField, 'testObject5', 'name', 'ginger') ], done); }); - it('should return 3 objects with correct data', function(done) { - db.getObjects(['testObject4', 'testObject5', 'doesnotexist'], function(err, objects) { + it('should return 3 objects with correct data', function (done) { + db.getObjects(['testObject4', 'testObject5', 'doesnotexist'], function (err, objects) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(objects) && objects.length === 3, true); @@ -86,9 +86,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectField()', function() { - it('should return falsy if object does not exist', function(done) { - db.getObjectField('doesnotexist', 'fieldName', function(err, value) { + describe('getObjectField()', function () { + it('should return falsy if object does not exist', function (done) { + db.getObjectField('doesnotexist', 'fieldName', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!value, false); @@ -96,8 +96,8 @@ describe('Hash methods', function() { }); }); - it('should return falsy if field does not exist', function(done) { - db.getObjectField('hashTestObject', 'fieldName', function(err, value) { + it('should return falsy if field does not exist', function (done) { + db.getObjectField('hashTestObject', 'fieldName', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!value, false); @@ -105,8 +105,8 @@ describe('Hash methods', function() { }); }); - it('should get an objects field', function(done) { - db.getObjectField('hashTestObject', 'lastname', function(err, value) { + it('should get an objects field', function (done) { + db.getObjectField('hashTestObject', 'lastname', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, 'usakli'); @@ -115,9 +115,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectFields()', function() { - it('should return an object with falsy values', function(done) { - db.getObjectFields('doesnotexist', ['field1', 'field2'], function(err, object) { + describe('getObjectFields()', function () { + it('should return an object with falsy values', function (done) { + db.getObjectFields('doesnotexist', ['field1', 'field2'], function (err, object) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(typeof object, 'object'); @@ -127,8 +127,8 @@ describe('Hash methods', function() { }); }); - it('should return an object with correct fields', function(done) { - db.getObjectFields('hashTestObject', ['lastname', 'age', 'field1'], function(err, object) { + it('should return an object with correct fields', function (done) { + db.getObjectFields('hashTestObject', ['lastname', 'age', 'field1'], function (err, object) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(typeof object, 'object'); @@ -140,16 +140,16 @@ describe('Hash methods', function() { }); }); - describe('getObjectsFields()', function() { - before(function(done) { + describe('getObjectsFields()', function () { + before(function (done) { async.parallel([ async.apply(db.setObject, 'testObject8', {name: 'baris', age:99}), async.apply(db.setObject, 'testObject9', {name: 'ginger', age: 3}) ], done); }); - it('should return an array of objects with correct values', function(done) { - db.getObjectsFields(['testObject8', 'testObject9', 'doesnotexist'], ['name', 'age'], function(err, objects) { + it('should return an array of objects with correct values', function (done) { + db.getObjectsFields(['testObject8', 'testObject9', 'doesnotexist'], ['name', 'age'], function (err, objects) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(objects), true); @@ -164,9 +164,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectKeys()', function() { - it('should return an empty array for a object that does not exist', function(done) { - db.getObjectKeys('doesnotexist', function(err, keys) { + describe('getObjectKeys()', function () { + it('should return an empty array for a object that does not exist', function (done) { + db.getObjectKeys('doesnotexist', function (err, keys) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(keys) && keys.length === 0, true); @@ -174,12 +174,12 @@ describe('Hash methods', function() { }); }); - it('should return an array of keys for the object\'s fields', function(done) { - db.getObjectKeys('hashTestObject', function(err, keys) { + it('should return an array of keys for the object\'s fields', function (done) { + db.getObjectKeys('hashTestObject', function (err, keys) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(keys) && keys.length === 3, true); - keys.forEach(function(key) { + keys.forEach(function (key) { assert.notEqual(['name', 'lastname', 'age'].indexOf(key), -1); }); done(); @@ -187,9 +187,9 @@ describe('Hash methods', function() { }); }); - describe('getObjectValues()', function() { - it('should return an empty array for a object that does not exist', function(done) { - db.getObjectValues('doesnotexist', function(err, values) { + describe('getObjectValues()', function () { + it('should return an empty array for a object that does not exist', function (done) { + db.getObjectValues('doesnotexist', function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(values) && values.length === 0, true); @@ -197,23 +197,20 @@ describe('Hash methods', function() { }); }); - it('should return an array of values for the object\'s fields', function(done) { - db.getObjectValues('hashTestObject', function(err, values) { + it('should return an array of values for the object\'s fields', function (done) { + db.getObjectValues('hashTestObject', function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(values) && values.length === 3, true); - values.forEach(function(value) { - assert.notEqual(['baris', 'usakli', 99].indexOf(value), -1); - }); - + assert.deepEqual(['baris', 'usakli', 99].sort(), values.sort()); done(); }); }); }); - describe('isObjectField()', function() { - it('should return false if object does not exist', function(done) { - db.isObjectField('doesnotexist', 'field1', function(err, value) { + describe('isObjectField()', function () { + it('should return false if object does not exist', function (done) { + db.isObjectField('doesnotexist', 'field1', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, false); @@ -221,8 +218,8 @@ describe('Hash methods', function() { }); }); - it('should return false if field does not exist', function(done) { - db.isObjectField('hashTestObject', 'field1', function(err, value) { + it('should return false if field does not exist', function (done) { + db.isObjectField('hashTestObject', 'field1', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, false); @@ -230,8 +227,8 @@ describe('Hash methods', function() { }); }); - it('should return true if field exists', function(done) { - db.isObjectField('hashTestObject', 'name', function(err, value) { + it('should return true if field exists', function (done) { + db.isObjectField('hashTestObject', 'name', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(value, true); @@ -241,9 +238,9 @@ describe('Hash methods', function() { }); - describe('isObjectFields()', function() { - it('should return an array of false if object does not exist', function(done) { - db.isObjectFields('doesnotexist', ['field1', 'field2'], function(err, values) { + describe('isObjectFields()', function () { + it('should return an array of false if object does not exist', function (done) { + db.isObjectFields('doesnotexist', ['field1', 'field2'], function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [false, false]); @@ -251,8 +248,8 @@ describe('Hash methods', function() { }); }); - it('should return false if field does not exist', function(done) { - db.isObjectFields('hashTestObject', ['name', 'age', 'field1'], function(err, values) { + it('should return false if field does not exist', function (done) { + db.isObjectFields('hashTestObject', ['name', 'age', 'field1'], function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [true, true, false]); @@ -261,16 +258,16 @@ describe('Hash methods', function() { }); }); - describe('deleteObjectField()', function() { - before(function(done) { + describe('deleteObjectField()', function () { + before(function (done) { db.setObject('testObject10', {foo: 'bar', delete: 'this', delete1: 'this', delete2: 'this'}, done); }); - it('should delete an objects field', function(done) { - db.deleteObjectField('testObject10', 'delete', function(err) { + it('should delete an objects field', function (done) { + db.deleteObjectField('testObject10', 'delete', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isObjectField('testObject10', 'delete', function(err, isField) { + db.isObjectField('testObject10', 'delete', function (err, isField) { assert.equal(err, null); assert.equal(isField, false); done(); @@ -278,14 +275,14 @@ describe('Hash methods', function() { }); }); - it('should delete multiple fields of the object', function(done) { - db.deleteObjectFields('testObject10', ['delete1', 'delete2'], function(err) { + it('should delete multiple fields of the object', function (done) { + db.deleteObjectFields('testObject10', ['delete1', 'delete2'], function (err) { assert.ifError(err); assert.equal(arguments.length, 1); async.parallel({ delete1: async.apply(db.isObjectField, 'testObject10', 'delete1'), delete2: async.apply(db.isObjectField, 'testObject10', 'delete2') - }, function(err, results) { + }, function (err, results) { assert.ifError(err); assert.equal(results.delete1, false); assert.equal(results.delete2, false); @@ -295,13 +292,13 @@ describe('Hash methods', function() { }); }); - describe('incrObjectField()', function() { - before(function(done) { + describe('incrObjectField()', function () { + before(function (done) { db.setObject('testObject11', {age: 99}, done); }); - it('should set an objects field to 1 if object does not exist', function(done) { - db.incrObjectField('testObject12', 'field1', function(err, newValue) { + it('should set an objects field to 1 if object does not exist', function (done) { + db.incrObjectField('testObject12', 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 1); @@ -309,8 +306,8 @@ describe('Hash methods', function() { }); }); - it('should increment an object fields by 1 and return it', function(done) { - db.incrObjectField('testObject11', 'age', function(err, newValue) { + it('should increment an object fields by 1 and return it', function (done) { + db.incrObjectField('testObject11', 'age', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 100); @@ -319,13 +316,13 @@ describe('Hash methods', function() { }); }); - describe('decrObjectField()', function() { - before(function(done) { + describe('decrObjectField()', function () { + before(function (done) { db.setObject('testObject13', {age: 99}, done); }); - it('should set an objects field to -1 if object does not exist', function(done) { - db.decrObjectField('testObject14', 'field1', function(err, newValue) { + it('should set an objects field to -1 if object does not exist', function (done) { + db.decrObjectField('testObject14', 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, -1); @@ -333,8 +330,8 @@ describe('Hash methods', function() { }); }); - it('should decrement an object fields by 1 and return it', function(done) { - db.decrObjectField('testObject13', 'age', function(err, newValue) { + it('should decrement an object fields by 1 and return it', function (done) { + db.decrObjectField('testObject13', 'age', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 98); @@ -343,33 +340,41 @@ describe('Hash methods', function() { }); }); - describe('incrObjectFieldBy()', function() { - before(function(done) { + describe('incrObjectFieldBy()', function () { + before(function (done) { db.setObject('testObject15', {age: 100}, done); }); - it('should set an objects field to 5 if object does not exist', function(done) { - db.incrObjectFieldBy('testObject16', 'field1', 5, function(err, newValue) { - assert.equal(err, null); + it('should set an objects field to 5 if object does not exist', function (done) { + db.incrObjectFieldBy('testObject16', 'field1', 5, function (err, newValue) { + assert.ifError(err); assert.equal(arguments.length, 2); assert.equal(newValue, 5); done(); }); }); - it('should increment an object fields by passed in value and return it', function(done) { - db.incrObjectFieldBy('testObject15', 'age', 11, function(err, newValue) { - assert.equal(err, null); + it('should increment an object fields by passed in value and return it', function (done) { + db.incrObjectFieldBy('testObject15', 'age', 11, function (err, newValue) { + assert.ifError(err); assert.equal(arguments.length, 2); assert.equal(newValue, 111); done(); }); }); + + it('should increment an object fields by passed in value and return it', function (done) { + db.incrObjectFieldBy('testObject15', 'age', '11', function (err, newValue) { + assert.ifError(err); + assert.equal(newValue, 122); + done(); + }); + }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/keys.js b/test/database/keys.js similarity index 54% rename from tests/database/keys.js rename to test/database/keys.js index 36e5fe5989..0166ced52d 100644 --- a/tests/database/keys.js +++ b/test/database/keys.js @@ -5,22 +5,22 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Key methods', function() { +describe('Key methods', function () { - beforeEach(function(done) { + beforeEach(function (done) { db.set('testKey', 'testValue', done); }); - it('should set a key without error', function(done) { - db.set('testKey', 'testValue', function(err) { + it('should set a key without error', function (done) { + db.set('testKey', 'testValue', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should get a key without error', function(done) { - db.get('testKey', function(err, value) { + it('should get a key without error', function (done) { + db.get('testKey', function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(value, 'testValue'); @@ -28,8 +28,8 @@ describe('Key methods', function() { }); }); - it('should return true if key exist', function(done) { - db.exists('testKey', function(err, exists) { + it('should return true if key exist', function (done) { + db.exists('testKey', function (err, exists) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(exists, true); @@ -37,8 +37,8 @@ describe('Key methods', function() { }); }); - it('should return false if key does not exist', function(done) { - db.exists('doesnotexist', function(err, exists) { + it('should return false if key does not exist', function (done) { + db.exists('doesnotexist', function (err, exists) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(exists, false); @@ -46,12 +46,12 @@ describe('Key methods', function() { }); }); - it('should delete a key without error', function(done) { - db.delete('testKey', function(err) { + it('should delete a key without error', function (done) { + db.delete('testKey', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.get('testKey', function(err, value) { + db.get('testKey', function (err, value) { assert.equal(err, null); assert.equal(false, !!value); done(); @@ -59,11 +59,11 @@ describe('Key methods', function() { }); }); - it('should return false if key was deleted', function(done) { - db.delete('testKey', function(err) { + it('should return false if key was deleted', function (done) { + db.delete('testKey', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.exists('testKey', function(err, exists) { + db.exists('testKey', function (err, exists) { assert.equal(err, null); assert.strictEqual(exists, false); done(); @@ -71,29 +71,29 @@ describe('Key methods', function() { }); }); - it('should delete all keys passed in', function(done) { + it('should delete all keys passed in', function (done) { async.parallel([ - function(next) { + function (next) { db.set('key1', 'value1', next); }, - function(next) { + function (next) { db.set('key2', 'value2', next); } - ], function(err) { + ], function (err) { if (err) { return done(err); } - db.deleteAll(['key1', 'key2'], function(err) { + db.deleteAll(['key1', 'key2'], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); async.parallel({ - key1exists: function(next) { + key1exists: function (next) { db.exists('key1', next); }, - key2exists: function(next) { + key2exists: function (next) { db.exists('key2', next); } - }, function(err, results) { + }, function (err, results) { assert.equal(err, null); assert.equal(results.key1exists, false); assert.equal(results.key2exists, false); @@ -103,17 +103,17 @@ describe('Key methods', function() { }); }); - describe('increment', function() { - it('should initialize key to 1', function(done) { - db.increment('keyToIncrement', function(err, value) { + describe('increment', function () { + it('should initialize key to 1', function (done) { + db.increment('keyToIncrement', function (err, value) { assert.equal(err, null); assert.strictEqual(parseInt(value, 10), 1); done(); }); }); - it('should increment key to 2', function(done) { - db.increment('keyToIncrement', function(err, value) { + it('should increment key to 2', function (done) { + db.increment('keyToIncrement', function (err, value) { assert.equal(err, null); assert.strictEqual(parseInt(value, 10), 2); done(); @@ -121,17 +121,17 @@ describe('Key methods', function() { }); }); - describe('rename', function() { - it('should rename key to new name', function(done) { - db.set('keyOldName', 'renamedKeyValue', function(err) { + describe('rename', function () { + it('should rename key to new name', function (done) { + db.set('keyOldName', 'renamedKeyValue', function (err) { if (err) { return done(err); } - db.rename('keyOldName', 'keyNewName', function(err) { + db.rename('keyOldName', 'keyNewName', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.get('keyNewName', function(err, value) { + db.get('keyNewName', function (err, value) { assert.equal(err, null); assert.equal(value, 'renamedKeyValue'); done(); @@ -142,7 +142,7 @@ describe('Key methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/list.js b/test/database/list.js similarity index 58% rename from tests/database/list.js rename to test/database/list.js index 59fa82aa58..706fc2ef40 100644 --- a/tests/database/list.js +++ b/test/database/list.js @@ -5,11 +5,11 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('List methods', function() { +describe('List methods', function () { - describe('listAppend()', function() { - it('should append to a list', function(done) { - db.listAppend('testList1', 5, function(err) { + describe('listAppend()', function () { + it('should append to a list', function (done) { + db.listAppend('testList1', 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -17,47 +17,47 @@ describe('List methods', function() { }); }); - describe('listPrepend()', function() { - it('should prepend to a list', function(done) { - db.listPrepend('testList2', 3, function(err) { + describe('listPrepend()', function () { + it('should prepend to a list', function (done) { + db.listPrepend('testList2', 3, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should prepend 2 more elements to a list', function(done) { + it('should prepend 2 more elements to a list', function (done) { async.series([ - function(next) { + function (next) { db.listPrepend('testList2', 2, next); }, - function(next) { + function (next) { db.listPrepend('testList2', 1, next); } - ], function(err) { + ], function (err) { assert.equal(err, null); done(); }); }); }); - describe('getListRange()', function() { - before(function(done) { + describe('getListRange()', function () { + before(function (done) { async.series([ - function(next) { + function (next) { db.listAppend('testList3', 7, next); }, - function(next) { + function (next) { db.listPrepend('testList3', 3, next); }, - function(next) { + function (next) { db.listAppend('testList4', 5, next); } ], done); }); - it('should return an empty list', function(done) { - db.getListRange('doesnotexist', 0, -1, function(err, list) { + it('should return an empty list', function (done) { + db.getListRange('doesnotexist', 0, -1, function (err, list) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(list), true); @@ -66,8 +66,8 @@ describe('List methods', function() { }); }); - it('should return a list with one element', function(done) { - db.getListRange('testList4', 0, 0, function(err, list) { + it('should return a list with one element', function (done) { + db.getListRange('testList4', 0, 0, function (err, list) { assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list[0], 5); @@ -75,8 +75,8 @@ describe('List methods', function() { }); }); - it('should return a list with 2 elements 3, 7', function(done) { - db.getListRange('testList3', 0, -1, function(err, list) { + it('should return a list with 2 elements 3, 7', function (done) { + db.getListRange('testList3', 0, -1, function (err, list) { assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list.length, 2); @@ -86,20 +86,20 @@ describe('List methods', function() { }); }); - describe('listRemoveLast()', function() { - before(function(done) { + describe('listRemoveLast()', function () { + before(function (done) { async.series([ - function(next) { + function (next) { db.listAppend('testList4', 12, next); }, - function(next) { + function (next) { db.listPrepend('testList4', 9, next); } ], done); }); - it('should remove the last element of list and return it', function(done) { - db.listRemoveLast('testList4', function(err, lastElement) { + it('should remove the last element of list and return it', function (done) { + db.listRemoveLast('testList4', function (err, lastElement) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(lastElement, '12'); @@ -108,8 +108,8 @@ describe('List methods', function() { }); }); - describe('listRemoveAll()', function() { - before(function(done) { + describe('listRemoveAll()', function () { + before(function (done) { async.series([ async.apply(db.listAppend, 'testList5', 1), async.apply(db.listAppend, 'testList5', 1), @@ -119,12 +119,13 @@ describe('List methods', function() { ], done); }); - it('should remove all the matching elements of list', function(done) { - db.listRemoveAll('testList5', '1', function(err) { + it('should remove all the matching elements of list', function (done) { + db.listRemoveAll('testList5', '1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getListRange('testList5', 0, -1, function(err, list) { + db.getListRange('testList5', 0, -1, function (err, list) { + assert.equal(err, null); assert.equal(Array.isArray(list), true); assert.equal(list.length, 2); assert.equal(list.indexOf('1'), -1); @@ -134,20 +135,21 @@ describe('List methods', function() { }); }); - describe('listTrim()', function() { - it('should trim list to a certain range', function(done) { + describe('listTrim()', function () { + it('should trim list to a certain range', function (done) { var list = ['1', '2', '3', '4', '5']; - async.eachSeries(list, function(value, next) { + async.eachSeries(list, function (value, next) { db.listAppend('testList6', value, next); - }, function(err) { + }, function (err) { if (err) { return done(err); } - db.listTrim('testList6', 0, 2, function(err) { + db.listTrim('testList6', 0, 2, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getListRange('testList6', 0, -1, function(err, list) { + db.getListRange('testList6', 0, -1, function (err, list) { + assert.equal(err, null); assert.equal(list.length, 3); assert.deepEqual(list, ['1', '2', '3']); done(); @@ -158,7 +160,7 @@ describe('List methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/sets.js b/test/database/sets.js similarity index 56% rename from tests/database/sets.js rename to test/database/sets.js index a7a23f4dd8..4e899c8265 100644 --- a/tests/database/sets.js +++ b/test/database/sets.js @@ -5,19 +5,19 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Set methods', function() { +describe('Set methods', function () { - describe('setAdd()', function() { - it('should add to a set', function(done) { - db.setAdd('testSet1', 5, function(err) { + describe('setAdd()', function () { + it('should add to a set', function (done) { + db.setAdd('testSet1', 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add an array to a set', function(done) { - db.setAdd('testSet1', [1, 2, 3, 4], function(err) { + it('should add an array to a set', function (done) { + db.setAdd('testSet1', [1, 2, 3, 4], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -25,13 +25,13 @@ describe('Set methods', function() { }); }); - describe('getSetMembers()', function() { - before(function(done) { + describe('getSetMembers()', function () { + before(function (done) { db.setAdd('testSet2', [1,2,3,4,5], done); }); - it('should return an empty set', function(done) { - db.getSetMembers('doesnotexist', function(err, set) { + it('should return an empty set', function (done) { + db.getSetMembers('doesnotexist', function (err, set) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(set), true); @@ -40,11 +40,11 @@ describe('Set methods', function() { }); }); - it('should return a set with all elements', function(done) { - db.getSetMembers('testSet2', function(err, set) { + it('should return a set with all elements', function (done) { + db.getSetMembers('testSet2', function (err, set) { assert.equal(err, null); assert.equal(set.length, 5); - set.forEach(function(value) { + set.forEach(function (value) { assert.notEqual(['1', '2', '3', '4', '5'].indexOf(value), -1); }); @@ -53,9 +53,9 @@ describe('Set methods', function() { }); }); - describe('setsAdd()', function() { - it('should add to multiple sets', function(done) { - db.setsAdd(['set1', 'set2'], 'value', function(err) { + describe('setsAdd()', function () { + it('should add to multiple sets', function (done) { + db.setsAdd(['set1', 'set2'], 'value', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -63,13 +63,13 @@ describe('Set methods', function() { }); }); - describe('getSetsMembers()', function() { - before(function(done) { + describe('getSetsMembers()', function () { + before(function (done) { db.setsAdd(['set3', 'set4'], 'value', done); }); - it('should return members of two sets', function(done) { - db.getSetsMembers(['set3', 'set4'], function(err, sets) { + it('should return members of two sets', function (done) { + db.getSetsMembers(['set3', 'set4'], function (err, sets) { assert.equal(err, null); assert.equal(Array.isArray(sets), true); assert.equal(arguments.length, 2); @@ -81,13 +81,13 @@ describe('Set methods', function() { }); }); - describe('isSetMember()', function() { - before(function(done) { + describe('isSetMember()', function () { + before(function (done) { db.setAdd('testSet3', 5, done); }); - it('should return false if element is not member of set', function(done) { - db.isSetMember('testSet3', 10, function(err, isMember) { + it('should return false if element is not member of set', function (done) { + db.isSetMember('testSet3', 10, function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -95,8 +95,8 @@ describe('Set methods', function() { }); }); - it('should return true if element is a member of set', function(done) { - db.isSetMember('testSet3', 5, function(err, isMember) { + it('should return true if element is a member of set', function (done) { + db.isSetMember('testSet3', 5, function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, true); @@ -105,13 +105,13 @@ describe('Set methods', function() { }); }); - describe('isSetMembers()', function() { - before(function(done) { + describe('isSetMembers()', function () { + before(function (done) { db.setAdd('testSet4', [1, 2, 3, 4, 5], done); }); - it('should return an array of booleans', function(done) { - db.isSetMembers('testSet4', ['1', '2', '10', '3'], function(err, members) { + it('should return an array of booleans', function (done) { + db.isSetMembers('testSet4', ['1', '2', '10', '3'], function (err, members) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(members), true); @@ -121,13 +121,13 @@ describe('Set methods', function() { }); }); - describe('isMemberOfSets()', function() { - before(function(done) { + describe('isMemberOfSets()', function () { + before(function (done) { db.setsAdd(['set1', 'set2'], 'value', done); }); - it('should return an array of booleans', function(done) { - db.isMemberOfSets(['set1', 'testSet1', 'set2', 'doesnotexist'], 'value', function(err, members) { + it('should return an array of booleans', function (done) { + db.isMemberOfSets(['set1', 'testSet1', 'set2', 'doesnotexist'], 'value', function (err, members) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(members), true); @@ -137,13 +137,13 @@ describe('Set methods', function() { }); }); - describe('setCount()', function() { - before(function(done) { + describe('setCount()', function () { + before(function (done) { db.setAdd('testSet5', [1,2,3,4,5], done); }); - it('should return the element count of set', function(done) { - db.setCount('testSet5', function(err, count) { + it('should return the element count of set', function (done) { + db.setCount('testSet5', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.strictEqual(count, 5); @@ -152,8 +152,8 @@ describe('Set methods', function() { }); }); - describe('setsCount()', function() { - before(function(done) { + describe('setsCount()', function () { + before(function (done) { async.parallel([ async.apply(db.setAdd, 'set5', [1,2,3,4,5]), async.apply(db.setAdd, 'set6', 1), @@ -161,8 +161,8 @@ describe('Set methods', function() { ], done); }); - it('should return the element count of sets', function(done) { - db.setsCount(['set5', 'set6', 'set7', 'doesnotexist'], function(err, counts) { + it('should return the element count of sets', function (done) { + db.setsCount(['set5', 'set6', 'set7', 'doesnotexist'], function (err, counts) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(Array.isArray(counts), true); @@ -172,17 +172,17 @@ describe('Set methods', function() { }); }); - describe('setRemove()', function() { - before(function(done) { + describe('setRemove()', function () { + before(function (done) { db.setAdd('testSet6', [1, 2], done); }); - it('should remove a element from set', function(done) { - db.setRemove('testSet6', '2', function(err) { + it('should remove a element from set', function (done) { + db.setRemove('testSet6', '2', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isSetMember('testSet6', '2', function(err, isMember) { + db.isSetMember('testSet6', '2', function (err, isMember) { assert.equal(err, null); assert.equal(isMember, false); done(); @@ -191,16 +191,16 @@ describe('Set methods', function() { }); }); - describe('setsRemove()', function() { - before(function(done) { + describe('setsRemove()', function () { + before(function (done) { db.setsAdd(['set1', 'set2'], 'value', done); }); - it('should remove a element from multiple sets', function(done) { - db.setsRemove(['set1', 'set2'], 'value', function(err) { + it('should remove a element from multiple sets', function (done) { + db.setsRemove(['set1', 'set2'], 'value', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isMemberOfSets(['set1', 'set2'], 'value', function(err, members) { + db.isMemberOfSets(['set1', 'set2'], 'value', function (err, members) { assert.equal(err, null); assert.deepEqual(members, [false, false]); done(); @@ -209,17 +209,17 @@ describe('Set methods', function() { }); }); - describe('setRemoveRandom()', function() { - before(function(done) { + describe('setRemoveRandom()', function () { + before(function (done) { db.setAdd('testSet7', [1,2,3,4,5], done); }); - it('should remove a random element from set', function(done) { - db.setRemoveRandom('testSet7', function(err, element) { + it('should remove a random element from set', function (done) { + db.setRemoveRandom('testSet7', function (err, element) { assert.equal(err, null); assert.equal(arguments.length, 2); - db.isSetMember('testSet', element, function(err, ismember) { + db.isSetMember('testSet', element, function (err, ismember) { assert.equal(err, null); assert.equal(ismember, false); done(); @@ -229,7 +229,7 @@ describe('Set methods', function() { }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/database/sorted.js b/test/database/sorted.js similarity index 50% rename from tests/database/sorted.js rename to test/database/sorted.js index 5c34c380b8..29ed4cf95e 100644 --- a/tests/database/sorted.js +++ b/test/database/sorted.js @@ -5,33 +5,33 @@ var async = require('async'), assert = require('assert'), db = require('../mocks/databasemock'); -describe('Sorted Set methods', function() { +describe('Sorted Set methods', function () { - before(function(done) { + before(function (done) { async.parallel([ - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest1', [1, 2, 3], ['value1', 'value2', 'value3'], next); }, - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest2', [1, 4], ['value1', 'value4'], next); }, - function(next) { + function (next) { db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4'], next); } ], done); }); - describe('sortedSetAdd()', function() { - it('should add an element to a sorted set', function(done) { - db.sortedSetAdd('sorted1', 1, 'value1', function(err) { + describe('sortedSetAdd()', function () { + it('should add an element to a sorted set', function (done) { + db.sortedSetAdd('sorted1', 1, 'value1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); }); }); - it('should add two elements to a sorted set', function(done) { - db.sortedSetAdd('sorted2', [1, 2], ['value1', 'value2'], function(err) { + it('should add two elements to a sorted set', function (done) { + db.sortedSetAdd('sorted2', [1, 2], ['value1', 'value2'], function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -39,9 +39,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsAdd()', function() { - it('should add an element to two sorted sets', function(done) { - db.sortedSetsAdd(['sorted1', 'sorted2'], 3, 'value3', function(err) { + describe('sortedSetsAdd()', function () { + it('should add an element to two sorted sets', function (done) { + db.sortedSetsAdd(['sorted1', 'sorted2'], 3, 'value3', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); done(); @@ -49,18 +49,18 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRange()', function() { - it('should return the lowest scored element', function(done) { - db.getSortedSetRange('sortedSetTest1', 0, 0, function(err, value) { + describe('getSortedSetRange()', function () { + it('should return the lowest scored element', function (done) { + db.getSortedSetRange('sortedSetTest1', 0, 0, function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); - assert.equal(value, 'value1'); + assert.deepEqual(value, ['value1']); done(); }); }); - it('should return elements sorted by score lowest to highest', function(done) { - db.getSortedSetRange('sortedSetTest1', 0, -1, function(err, values) { + it('should return elements sorted by score lowest to highest', function (done) { + db.getSortedSetRange('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2', 'value3']); @@ -69,18 +69,18 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRange()', function() { - it('should return the highest scored element', function(done) { - db.getSortedSetRevRange('sortedSetTest1', 0, 0, function(err, value) { + describe('getSortedSetRevRange()', function () { + it('should return the highest scored element', function (done) { + db.getSortedSetRevRange('sortedSetTest1', 0, 0, function (err, value) { assert.equal(err, null); assert.equal(arguments.length, 2); - assert.equal(value, 'value3'); + assert.deepEqual(value, ['value3']); done(); }); }); - it('should return elements sorted by score highest to lowest', function(done) { - db.getSortedSetRevRange('sortedSetTest1', 0, -1, function(err, values) { + it('should return elements sorted by score highest to lowest', function (done) { + db.getSortedSetRevRange('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value3', 'value2', 'value1']); @@ -89,9 +89,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeWithScores()', function() { - it('should return array of elements sorted by score lowest to highest with scores', function(done) { - db.getSortedSetRangeWithScores('sortedSetTest1', 0, -1, function(err, values) { + describe('getSortedSetRangeWithScores()', function () { + it('should return array of elements sorted by score lowest to highest with scores', function (done) { + db.getSortedSetRangeWithScores('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value1', score: 1}, {value: 'value2', score: 2}, {value: 'value3', score: 3}]); @@ -100,9 +100,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeWithScores()', function() { - it('should return array of elements sorted by score highest to lowest with scores', function(done) { - db.getSortedSetRevRangeWithScores('sortedSetTest1', 0, -1, function(err, values) { + describe('getSortedSetRevRangeWithScores()', function () { + it('should return array of elements sorted by score highest to lowest with scores', function (done) { + db.getSortedSetRevRangeWithScores('sortedSetTest1', 0, -1, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value3', score: 3}, {value: 'value2', score: 2}, {value: 'value1', score: 1}]); @@ -111,9 +111,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeByScore()', function() { - it('should get count elements with score between min max sorted by score lowest to highest', function(done) { - db.getSortedSetRangeByScore('sortedSetTest1', 0, -1, '-inf', 2, function(err, values) { + describe('getSortedSetRangeByScore()', function () { + it('should get count elements with score between min max sorted by score lowest to highest', function (done) { + db.getSortedSetRangeByScore('sortedSetTest1', 0, -1, '-inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2']); @@ -122,9 +122,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeByScore()', function() { - it('should get count elements with score between max min sorted by score highest to lowest', function(done) { - db.getSortedSetRevRangeByScore('sortedSetTest1', 0, -1, '+inf', 2, function(err, values) { + describe('getSortedSetRevRangeByScore()', function () { + it('should get count elements with score between max min sorted by score highest to lowest', function (done) { + db.getSortedSetRevRangeByScore('sortedSetTest1', 0, -1, '+inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value3', 'value2']); @@ -133,9 +133,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRangeByScoreWithScores()', function() { - it('should get count elements with score between min max sorted by score lowest to highest with scores', function(done) { - db.getSortedSetRangeByScoreWithScores('sortedSetTest1', 0, -1, '-inf', 2, function(err, values) { + describe('getSortedSetRangeByScoreWithScores()', function () { + it('should get count elements with score between min max sorted by score lowest to highest with scores', function (done) { + db.getSortedSetRangeByScoreWithScores('sortedSetTest1', 0, -1, '-inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value1', score: 1}, {value: 'value2', score: 2}]); @@ -144,9 +144,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevRangeByScoreWithScores()', function() { - it('should get count elements with score between max min sorted by score highest to lowest', function(done) { - db.getSortedSetRevRangeByScoreWithScores('sortedSetTest1', 0, -1, '+inf', 2, function(err, values) { + describe('getSortedSetRevRangeByScoreWithScores()', function () { + it('should get count elements with score between max min sorted by score highest to lowest', function (done) { + db.getSortedSetRevRangeByScoreWithScores('sortedSetTest1', 0, -1, '+inf', 2, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, [{value: 'value3', score: 3}, {value: 'value2', score: 2}]); @@ -155,9 +155,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetCount()', function() { - it('should return 0 for a sorted set that does not exist', function(done) { - db.sortedSetCount('doesnotexist', 0, 10, function(err, count) { + describe('sortedSetCount()', function () { + it('should return 0 for a sorted set that does not exist', function (done) { + db.sortedSetCount('doesnotexist', 0, 10, function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 0); @@ -165,8 +165,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements between scores min max inclusive', function(done) { - db.sortedSetCount('sortedSetTest1', '-inf', 2, function(err, count) { + it('should return number of elements between scores min max inclusive', function (done) { + db.sortedSetCount('sortedSetTest1', '-inf', 2, function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 2); @@ -174,8 +174,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements between scores -inf +inf inclusive', function(done) { - db.sortedSetCount('sortedSetTest1', '-inf', '+inf', function(err, count) { + it('should return number of elements between scores -inf +inf inclusive', function (done) { + db.sortedSetCount('sortedSetTest1', '-inf', '+inf', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 3); @@ -184,9 +184,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetCard()', function() { - it('should return 0 for a sorted set that does not exist', function(done) { - db.sortedSetCard('doesnotexist', function(err, count) { + describe('sortedSetCard()', function () { + it('should return 0 for a sorted set that does not exist', function (done) { + db.sortedSetCard('doesnotexist', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 0); @@ -194,8 +194,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return number of elements in a sorted set', function(done) { - db.sortedSetCard('sortedSetTest1', function(err, count) { + it('should return number of elements in a sorted set', function (done) { + db.sortedSetCard('sortedSetTest1', function (err, count) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(count, 3); @@ -204,9 +204,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsCard()', function() { - it('should return the number of elements in sorted sets', function(done) { - db.sortedSetsCard(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], function(err, counts) { + describe('sortedSetsCard()', function () { + it('should return the number of elements in sorted sets', function (done) { + db.sortedSetsCard(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], function (err, counts) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(counts, [3, 2, 0]); @@ -215,9 +215,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRank()', function() { - it('should return falsy if sorted set does not exist', function(done) { - db.sortedSetRank('doesnotexist', 'value1', function(err, rank) { + describe('sortedSetRank()', function () { + it('should return falsy if sorted set does not exist', function (done) { + db.sortedSetRank('doesnotexist', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -225,8 +225,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element isnt in sorted set', function(done) { - db.sortedSetRank('sortedSetTest1', 'value5', function(err, rank) { + it('should return falsy if element isnt in sorted set', function (done) { + db.sortedSetRank('sortedSetTest1', 'value5', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -234,8 +234,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the rank of the element in the sorted set sorted by lowest to highest score', function(done) { - db.sortedSetRank('sortedSetTest1', 'value1', function(err, rank) { + it('should return the rank of the element in the sorted set sorted by lowest to highest score', function (done) { + db.sortedSetRank('sortedSetTest1', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(rank, 0); @@ -244,9 +244,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRevRank()', function() { - it('should return falsy if sorted set doesnot exist', function(done) { - db.sortedSetRevRank('doesnotexist', 'value1', function(err, rank) { + describe('sortedSetRevRank()', function () { + it('should return falsy if sorted set doesnot exist', function (done) { + db.sortedSetRevRank('doesnotexist', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -254,8 +254,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element isnt in sorted set', function(done) { - db.sortedSetRevRank('sortedSetTest1', 'value5', function(err, rank) { + it('should return falsy if element isnt in sorted set', function (done) { + db.sortedSetRevRank('sortedSetTest1', 'value5', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!rank, false); @@ -263,8 +263,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the rank of the element in the sorted set sorted by highest to lowest score', function(done) { - db.sortedSetRevRank('sortedSetTest1', 'value1', function(err, rank) { + it('should return the rank of the element in the sorted set sorted by highest to lowest score', function (done) { + db.sortedSetRevRank('sortedSetTest1', 'value1', function (err, rank) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(rank, 2); @@ -273,9 +273,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRanks()', function() { - it('should return the ranks of values in sorted sets', function(done) { - db.sortedSetsRanks(['sortedSetTest1', 'sortedSetTest2'], ['value1', 'value4'], function(err, ranks) { + describe('sortedSetsRanks()', function () { + it('should return the ranks of values in sorted sets', function (done) { + db.sortedSetsRanks(['sortedSetTest1', 'sortedSetTest2'], ['value1', 'value4'], function (err, ranks) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(ranks, [0, 1]); @@ -284,9 +284,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetRanks()', function() { - it('should return the ranks of values in a sorted set', function(done) { - db.sortedSetRanks('sortedSetTest1', ['value2', 'value1', 'value3', 'value4'], function(err, ranks) { + describe('sortedSetRanks()', function () { + it('should return the ranks of values in a sorted set', function (done) { + db.sortedSetRanks('sortedSetTest1', ['value2', 'value1', 'value3', 'value4'], function (err, ranks) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(ranks, [1, 0, 2, null]); @@ -295,9 +295,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetScore()', function() { - it('should return falsy if sorted set does not exist', function(done) { - db.sortedSetScore('doesnotexist', 'value1', function(err, score) { + describe('sortedSetScore()', function () { + it('should return falsy if sorted set does not exist', function (done) { + db.sortedSetScore('doesnotexist', 'value1', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); @@ -305,8 +305,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return falsy if element is not in sorted set', function(done) { - db.sortedSetScore('sortedSetTest1', 'value5', function(err, score) { + it('should return falsy if element is not in sorted set', function (done) { + db.sortedSetScore('sortedSetTest1', 'value5', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); @@ -314,8 +314,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return the score of an element', function(done) { - db.sortedSetScore('sortedSetTest1', 'value2', function(err, score) { + it('should return the score of an element', function (done) { + db.sortedSetScore('sortedSetTest1', 'value2', function (err, score) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(score, 2); @@ -324,9 +324,9 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsScore()', function() { - it('should return the scores of value in sorted sets', function(done) { - db.sortedSetsScore(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], 'value1', function(err, scores) { + describe('sortedSetsScore()', function () { + it('should return the scores of value in sorted sets', function (done) { + db.sortedSetsScore(['sortedSetTest1', 'sortedSetTest2', 'doesnotexist'], 'value1', function (err, scores) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(scores, [1, 1, null]); @@ -335,9 +335,21 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetScores()', function() { - it('should return the scores of value in sorted sets', function(done) { - db.sortedSetScores('sortedSetTest1', ['value2', 'value1', 'doesnotexist'], function(err, scores) { + describe('sortedSetScores()', function () { + before(function (done) { + db.sortedSetAdd('zeroScore', 0, 'value1', done); + }); + + it('should return 0 if score is 0', function (done) { + db.sortedSetScores('zeroScore', ['value1'], function (err, scores) { + assert.ifError(err); + assert.equal(0, scores[0]); + done(); + }); + }); + + it('should return the scores of value in sorted sets', function (done) { + db.sortedSetScores('sortedSetTest1', ['value2', 'value1', 'doesnotexist'], function (err, scores) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(scores, [2, 1, null]); @@ -346,13 +358,13 @@ describe('Sorted Set methods', function() { }); }); - describe('isSortedSetMember()', function() { - before(function(done) { + describe('isSortedSetMember()', function () { + before(function (done) { db.sortedSetAdd('zeroscore', 0, 'itemwithzeroscore', done); }); - it('should return false if sorted set does not exist', function(done) { - db.isSortedSetMember('doesnotexist', 'value1', function(err, isMember) { + it('should return false if sorted set does not exist', function (done) { + db.isSortedSetMember('doesnotexist', 'value1', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -360,8 +372,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return false if element is not in sorted set', function(done) { - db.isSortedSetMember('sorted2', 'value5', function(err, isMember) { + it('should return false if element is not in sorted set', function (done) { + db.isSortedSetMember('sorted2', 'value5', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(isMember, false); @@ -369,8 +381,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return true if element is in sorted set', function(done) { - db.isSortedSetMember('sortedSetTest1', 'value2', function(err, isMember) { + it('should return true if element is in sorted set', function (done) { + db.isSortedSetMember('sortedSetTest1', 'value2', function (err, isMember) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMember, true); @@ -378,8 +390,8 @@ describe('Sorted Set methods', function() { }); }); - it('should return true if element is in sorted set with score 0', function(done) { - db.isSortedSetMember('zeroscore', 'itemwithzeroscore', function(err, isMember) { + it('should return true if element is in sorted set with score 0', function (done) { + db.isSortedSetMember('zeroscore', 'itemwithzeroscore', function (err, isMember) { assert.ifError(err); assert.deepEqual(isMember, true); done(); @@ -387,9 +399,9 @@ describe('Sorted Set methods', function() { }); }); - describe('isSortedSetMembers()', function() { - it('should return an array of booleans indicating membership', function(done) { - db.isSortedSetMembers('sortedSetTest1', ['value1', 'value2', 'value5'], function(err, isMembers) { + describe('isSortedSetMembers()', function () { + it('should return an array of booleans indicating membership', function (done) { + db.isSortedSetMembers('sortedSetTest1', ['value1', 'value2', 'value5'], function (err, isMembers) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMembers, [true, true, false]); @@ -398,9 +410,9 @@ describe('Sorted Set methods', function() { }); }); - describe('isMemberOfSortedSets', function() { - it('should return true for members false for non members', function(done) { - db.isMemberOfSortedSets(['doesnotexist', 'sortedSetTest1', 'sortedSetTest2'], 'value2', function(err, isMembers) { + describe('isMemberOfSortedSets', function () { + it('should return true for members false for non members', function (done) { + db.isMemberOfSortedSets(['doesnotexist', 'sortedSetTest1', 'sortedSetTest2'], 'value2', function (err, isMembers) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(isMembers, [false, true, false]); @@ -409,13 +421,13 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetsMembers', function() { - it('should return members of multiple sorted sets', function(done) { - db.getSortedSetsMembers(['doesnotexist', 'sortedSetTest1'], function(err, sortedSets) { + describe('getSortedSetsMembers', function () { + it('should return members of multiple sorted sets', function (done) { + db.getSortedSetsMembers(['doesnotexist', 'sortedSetTest1'], function (err, sortedSets) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(sortedSets[0], []); - sortedSets[0].forEach(function(element) { + sortedSets[0].forEach(function (element) { assert.notEqual(['value1', 'value2', 'value3'].indexOf(element), -1); }); @@ -424,9 +436,19 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetUnion()', function() { - it('should return an array of values from both sorted sets sorted by scores lowest to highest', function(done) { - db.getSortedSetUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + describe('sortedSetUnionCard', function () { + it('should return the number of elements in the union', function (done) { + db.sortedSetUnionCard(['sortedSetTest2', 'sortedSetTest3'], function (err, count) { + assert.ifError(err); + assert.equal(count, 3); + done(); + }); + }); + }); + + describe('getSortedSetUnion()', function () { + it('should return an array of values from both sorted sets sorted by scores lowest to highest', function (done) { + db.getSortedSetUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value1', 'value2', 'value4']); @@ -435,9 +457,9 @@ describe('Sorted Set methods', function() { }); }); - describe('getSortedSetRevUnion()', function() { - it('should return an array of values from both sorted sets sorted by scores highest to lowest', function(done) { - db.getSortedSetRevUnion(['sortedSetTest2', 'sortedSetTest3'], 0, -1, function(err, values) { + describe('getSortedSetRevUnion()', function () { + it('should return an array of values from both sorted sets sorted by scores highest to lowest', function (done) { + db.getSortedSetRevUnion({sets: ['sortedSetTest2', 'sortedSetTest3'], start: 0, stop: -1}, function (err, values) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.deepEqual(values, ['value4', 'value2', 'value1']); @@ -446,13 +468,13 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetIncrBy()', function() { - it('should create a sorted set with a field set to 1', function(done) { - db.sortedSetIncrBy('sortedIncr', 1, 'field1', function(err, newValue) { + describe('sortedSetIncrBy()', function () { + it('should create a sorted set with a field set to 1', function (done) { + db.sortedSetIncrBy('sortedIncr', 1, 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 1); - db.sortedSetScore('sortedIncr', 'field1', function(err, score) { + db.sortedSetScore('sortedIncr', 'field1', function (err, score) { assert.equal(err, null); assert.equal(score, 1); done(); @@ -460,12 +482,12 @@ describe('Sorted Set methods', function() { }); }); - it('should increment a field of a sorted set by 5', function(done) { - db.sortedSetIncrBy('sortedIncr', 5, 'field1', function(err, newValue) { + it('should increment a field of a sorted set by 5', function (done) { + db.sortedSetIncrBy('sortedIncr', 5, 'field1', function (err, newValue) { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(newValue, 6); - db.sortedSetScore('sortedIncr', 'field1', function(err, score) { + db.sortedSetScore('sortedIncr', 'field1', function (err, score) { assert.equal(err, null); assert.equal(score, 6); done(); @@ -475,16 +497,16 @@ describe('Sorted Set methods', function() { }); - describe('sortedSetRemove()', function() { - before(function(done) { + describe('sortedSetRemove()', function () { + before(function (done) { db.sortedSetAdd('sorted3', [1, 2], ['value1', 'value2'], done); }); - it('should remove an element from a sorted set', function(done) { - db.sortedSetRemove('sorted3', 'value2', function(err) { + it('should remove an element from a sorted set', function (done) { + db.sortedSetRemove('sorted3', 'value2', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.isSortedSetMember('sorted3', 'value2', function(err, isMember) { + db.isSortedSetMember('sorted3', 'value2', function (err, isMember) { assert.equal(err, null); assert.equal(isMember, false); done(); @@ -493,19 +515,19 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRemove()', function() { - before(function(done) { + describe('sortedSetsRemove()', function () { + before(function (done) { async.parallel([ async.apply(db.sortedSetAdd, 'sorted4', [1,2], ['value1', 'value2']), async.apply(db.sortedSetAdd, 'sorted5', [1,2], ['value1', 'value3']), ], done); }); - it('should remove element from multiple sorted sets', function(done) { - db.sortedSetsRemove(['sorted4', 'sorted5'], 'value1', function(err) { + it('should remove element from multiple sorted sets', function (done) { + db.sortedSetsRemove(['sorted4', 'sorted5'], 'value1', function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.sortedSetsScore(['sorted4', 'sorted5'], 'value1', function(err, scores) { + db.sortedSetsScore(['sorted4', 'sorted5'], 'value1', function (err, scores) { assert.equal(err, null); assert.deepEqual(scores, [null, null]); done(); @@ -514,16 +536,16 @@ describe('Sorted Set methods', function() { }); }); - describe('sortedSetsRemoveRangeByScore()', function() { - before(function(done) { + describe('sortedSetsRemoveRangeByScore()', function () { + before(function (done) { db.sortedSetAdd('sorted6', [1,2,3,4,5], ['value1','value2','value3','value4','value5'], done); }); - it('should remove elements with scores between min max inclusive', function(done) { - db.sortedSetsRemoveRangeByScore(['sorted6'], 4, 5, function(err) { + it('should remove elements with scores between min max inclusive', function (done) { + db.sortedSetsRemoveRangeByScore(['sorted6'], 4, 5, function (err) { assert.equal(err, null); assert.equal(arguments.length, 1); - db.getSortedSetRange('sorted6', 0, -1, function(err, values) { + db.getSortedSetRange('sorted6', 0, -1, function (err, values) { assert.equal(err, null); assert.deepEqual(values, ['value1', 'value2', 'value3']); done(); @@ -532,8 +554,148 @@ describe('Sorted Set methods', function() { }); }); + describe('getSortedSetIntersect', function () { + before(function (done) { + async.parallel([ + function (next) { + db.sortedSetAdd('interSet1', [1,2,3], ['value1', 'value2', 'value3'], next); + }, + function (next) { + db.sortedSetAdd('interSet2', [4,5,6], ['value2', 'value3', 'value5'], next); + } + ], done); + }); + + it('should return the intersection of two sets', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.deepEqual(['value2', 'value3'], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 6}, {value: 'value3', score: 8}], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores aggregate MIN', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + aggregate: 'MIN' + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 2}, {value: 'value3', score: 3}], data); + done(); + }); + }); + + it('should return the intersection of two sets with scores aggregate MAX', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + aggregate: 'MAX' + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 4}, {value: 'value3', score: 5}], data); + done(); + }); + }); + + it('should return the intersection with scores modified by weights', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet2'], + start: 0, + stop: -1, + withScores: true, + weights: [1, 0.5] + }, function (err, data) { + assert.ifError(err); + assert.deepEqual([{value: 'value2', score: 4}, {value: 'value3', score: 5.5}], data); + done(); + }); + }); + + it('should return empty array if sets do not exist', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet10', 'interSet12'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.equal(data.length, 0); + done(); + }); + }); + + it('should return empty array if one set does not exist', function (done) { + db.getSortedSetIntersect({ + sets: ['interSet1', 'interSet12'], + start: 0, + stop: -1 + }, function (err, data) { + assert.ifError(err); + assert.equal(data.length, 0); + done(); + }); + }); + + }); + + describe('sortedSetIntersectCard', function () { + before(function (done) { + async.parallel([ + function (next) { + db.sortedSetAdd('interCard1', [0, 0, 0], ['value1', 'value2', 'value3'], next); + }, + function (next) { + db.sortedSetAdd('interCard2', [0, 0, 0], ['value2', 'value3', 'value4'], next); + }, + function (next) { + db.sortedSetAdd('interCard3', [0, 0, 0], ['value3', 'value4', 'value5'], next); + }, + function (next) { + db.sortedSetAdd('interCard4', [0, 0, 0], ['value4', 'value5', 'value6'], next); + } + ], done); + }); + + it('should return # of elements in intersection', function (done) { + db.sortedSetIntersectCard(['interCard1', 'interCard2', 'interCard3'], function (err, count) { + assert.ifError(err); + assert.strictEqual(count, 1); + done(); + }); + }); + + it('should return 0 if intersection is empty', function (done) { + db.sortedSetIntersectCard(['interCard1', 'interCard4'], function (err, count) { + assert.ifError(err); + assert.strictEqual(count, 0); + done(); + }); + }); + }); + - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/groups.js b/test/groups.js similarity index 52% rename from tests/groups.js rename to test/groups.js index 81fb253433..31195719b6 100644 --- a/tests/groups.js +++ b/test/groups.js @@ -1,55 +1,55 @@ 'use strict'; /*global require, before, after*/ -var assert = require('assert'), - async = require('async'), +var assert = require('assert'); +var async = require('async'); - db = require('./mocks/databasemock'), - Groups = require('../src/groups'), - User = require('../src/user'); +var db = require('./mocks/databasemock'); +var Groups = require('../src/groups'); +var User = require('../src/user'); -describe('Groups', function() { - before(function(done) { +describe('Groups', function () { + before(function (done) { + Groups.resetCache(); async.parallel([ - function(next) { + function (next) { // Create a group to play around with Groups.create({ name: 'Test', description: 'Foobar!' }, next); }, - function(next) { + function (next) { // Create a new user User.create({ username: 'testuser', email: 'b@c.com' }, next); }, - function(next) { + function (next) { // Also create a hidden group Groups.join('Hidden', 'Test', next); } ], done); }); - describe('.list()', function() { - it('should list the groups present', function(done) { - Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function(err, groups) { - if (err) return done(err); - + describe('.list()', function () { + it('should list the groups present', function (done) { + Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function (err, groups) { + assert.ifError(err); assert.equal(groups.length, 3); done(); }); }); }); - describe('.get()', function() { - before(function(done) { + describe('.get()', function () { + before(function (done) { Groups.join('Test', 1, done); }); - it('with no options, should show group information', function(done) { - Groups.get('Test', {}, function(err, groupObj) { + it('with no options, should show group information', function (done) { + Groups.get('Test', {}, function (err, groupObj) { if (err) return done(err); assert.equal(typeof groupObj, 'object'); @@ -64,9 +64,9 @@ describe('Groups', function() { }); }); - describe('.search()', function() { - it('should return the "Test" group when searched for', function(done) { - Groups.search('test', {}, function(err, groups) { + describe('.search()', function () { + it('should return the "Test" group when searched for', function (done) { + Groups.search('test', {}, function (err, groups) { if (err) return done(err); assert.equal(1, groups.length); assert.strictEqual('Test', groups[0].name); @@ -75,9 +75,9 @@ describe('Groups', function() { }); }); - describe('.isMember()', function() { - it('should return boolean true when a user is in a group', function(done) { - Groups.isMember(1, 'Test', function(err, isMember) { + describe('.isMember()', function () { + it('should return boolean true when a user is in a group', function (done) { + Groups.isMember(1, 'Test', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, true); @@ -86,8 +86,8 @@ describe('Groups', function() { }); }); - it('should return boolean false when a user is not in a group', function(done) { - Groups.isMember(2, 'Test', function(err, isMember) { + it('should return boolean false when a user is not in a group', function (done) { + Groups.isMember(2, 'Test', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, false); @@ -97,9 +97,9 @@ describe('Groups', function() { }); }); - describe('.isMemberOfGroupList', function() { - it('should report that a user is part of a groupList, if they are', function(done) { - Groups.isMemberOfGroupList(1, 'Hidden', function(err, isMember) { + describe('.isMemberOfGroupList', function () { + it('should report that a user is part of a groupList, if they are', function (done) { + Groups.isMemberOfGroupList(1, 'Hidden', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, true); @@ -108,8 +108,8 @@ describe('Groups', function() { }); }); - it('should report that a user is not part of a groupList, if they are not', function(done) { - Groups.isMemberOfGroupList(2, 'Hidden', function(err, isMember) { + it('should report that a user is not part of a groupList, if they are not', function (done) { + Groups.isMemberOfGroupList(2, 'Hidden', function (err, isMember) { if (err) return done(err); assert.strictEqual(isMember, false); @@ -119,9 +119,9 @@ describe('Groups', function() { }); }); - describe('.exists()', function() { - it('should verify that the test group exists', function(done) { - Groups.exists('Test', function(err, exists) { + describe('.exists()', function () { + it('should verify that the test group exists', function (done) { + Groups.exists('Test', function (err, exists) { if (err) return done(err); assert.strictEqual(exists, true); @@ -130,8 +130,8 @@ describe('Groups', function() { }); }); - it('should verify that a fake group does not exist', function(done) { - Groups.exists('Derp', function(err, exists) { + it('should verify that a fake group does not exist', function (done) { + Groups.exists('Derp', function (err, exists) { if (err) return done(err); assert.strictEqual(exists, false); @@ -140,8 +140,8 @@ describe('Groups', function() { }); }); - it('should check if group exists using an array', function(done) { - Groups.exists(['Test', 'Derp'], function(err, groupsExists) { + it('should check if group exists using an array', function (done) { + Groups.exists(['Test', 'Derp'], function (err, groupsExists) { if (err) return done(err); assert.strictEqual(groupsExists[0], true); @@ -151,25 +151,33 @@ describe('Groups', function() { }); }); - describe('.create()', function() { - it('should create another group', function(done) { + describe('.create()', function () { + it('should create another group', function (done) { Groups.create({ name: 'foo', description: 'bar' - }, function(err) { + }, function (err) { if (err) return done(err); Groups.get('foo', {}, done); }); }); + + it('should fail to create group with duplicate group name', function (done) { + Groups.create({name: 'foo'}, function (err) { + assert(err); + assert.equal(err.message, '[[error:group-already-exists]]'); + done(); + }); + }); }); - describe('.hide()', function() { - it('should mark the group as hidden', function(done) { - Groups.hide('foo', function(err) { + describe('.hide()', function () { + it('should mark the group as hidden', function (done) { + Groups.hide('foo', function (err) { if (err) return done(err); - Groups.get('foo', {}, function(err, groupObj) { + Groups.get('foo', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual(true, groupObj.hidden); @@ -180,8 +188,8 @@ describe('Groups', function() { }); }); - describe('.update()', function() { - before(function(done) { + describe('.update()', function () { + before(function (done) { Groups.create({ name: 'updateTestGroup', description: 'bar', @@ -190,13 +198,13 @@ describe('Groups', function() { }, done); }); - it('should change an aspect of a group', function(done) { + it('should change an aspect of a group', function (done) { Groups.update('updateTestGroup', { description: 'baz' - }, function(err) { + }, function (err) { if (err) return done(err); - Groups.get('updateTestGroup', {}, function(err, groupObj) { + Groups.get('updateTestGroup', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual('baz', groupObj.description); @@ -206,13 +214,13 @@ describe('Groups', function() { }); }); - it('should rename a group if the name was updated', function(done) { + it('should rename a group if the name was updated', function (done) { Groups.update('updateTestGroup', { name: 'updateTestGroup?' - }, function(err) { + }, function (err) { if (err) return done(err); - Groups.get('updateTestGroup?', {}, function(err, groupObj) { + Groups.get('updateTestGroup?', {}, function (err, groupObj) { if (err) return done(err); assert.strictEqual('updateTestGroup?', groupObj.name); @@ -224,16 +232,16 @@ describe('Groups', function() { }); }); - describe('.destroy()', function() { - before(function(done) { + describe('.destroy()', function () { + before(function (done) { Groups.join('foobar?', 1, done); }); - it('should destroy a group', function(done) { - Groups.destroy('foobar?', function(err) { + it('should destroy a group', function (done) { + Groups.destroy('foobar?', function (err) { if (err) return done(err); - Groups.get('foobar?', {}, function(err) { + Groups.get('foobar?', {}, function (err) { assert(err, 'Group still exists!'); done(); @@ -241,8 +249,8 @@ describe('Groups', function() { }); }); - it('should also remove the members set', function(done) { - db.exists('group:foo:members', function(err, exists) { + it('should also remove the members set', function (done) { + db.exists('group:foo:members', function (err, exists) { if (err) return done(err); assert.strictEqual(false, exists); @@ -252,16 +260,17 @@ describe('Groups', function() { }); }); - describe('.join()', function() { - before(function(done) { + describe('.join()', function () { + before(function (done) { Groups.leave('Test', 1, done); }); - it('should add a user to a group', function(done) { - Groups.join('Test', 1, function(err) { + it('should add a user to a group', function (done) { + Groups.join('Test', 1, function (err) { if (err) return done(err); - Groups.isMember(1, 'Test', function(err, isMember) { + Groups.isMember(1, 'Test', function (err, isMember) { + assert.equal(err, null); assert.strictEqual(true, isMember); done(); @@ -270,12 +279,13 @@ describe('Groups', function() { }); }); - describe('.leave()', function() { - it('should remove a user from a group', function(done) { - Groups.leave('Test', 1, function(err) { + describe('.leave()', function () { + it('should remove a user from a group', function (done) { + Groups.leave('Test', 1, function (err) { if (err) return done(err); - Groups.isMember(1, 'Test', function(err, isMember) { + Groups.isMember(1, 'Test', function (err, isMember) { + assert.equal(err, null); assert.strictEqual(false, isMember); done(); @@ -284,20 +294,20 @@ describe('Groups', function() { }); }); - describe('.leaveAllGroups()', function() { - it('should remove a user from all groups', function(done) { - Groups.leaveAllGroups(1, function(err) { + describe('.leaveAllGroups()', function () { + it('should remove a user from all groups', function (done) { + Groups.leaveAllGroups(1, function (err) { if (err) return done(err); var groups = ['Test', 'Hidden']; - async.every(groups, function(group, next) { - Groups.isMember(1, group, function(err, isMember) { + async.every(groups, function (group, next) { + Groups.isMember(1, group, function (err, isMember) { if (err) done(err); else { next(!isMember); } }); - }, function(result) { + }, function (result) { assert(result); done(); @@ -306,12 +316,12 @@ describe('Groups', function() { }); }); - describe('.show()', function() { - it('should make a group visible', function(done) { - Groups.show('Test', function(err) { + describe('.show()', function () { + it('should make a group visible', function (done) { + Groups.show('Test', function (err) { assert.ifError(err); assert.equal(arguments.length, 1); - db.isSortedSetMember('groups:visible:createtime', 'Test', function(err, isMember) { + db.isSortedSetMember('groups:visible:createtime', 'Test', function (err, isMember) { assert.ifError(err); assert.strictEqual(isMember, true); done(); @@ -320,12 +330,12 @@ describe('Groups', function() { }); }); - describe('.hide()', function() { - it('should make a group hidden', function(done) { - Groups.hide('Test', function(err) { + describe('.hide()', function () { + it('should make a group hidden', function (done) { + Groups.hide('Test', function (err) { assert.ifError(err); assert.equal(arguments.length, 1); - db.isSortedSetMember('groups:visible:createtime', 'Test', function(err, isMember) { + db.isSortedSetMember('groups:visible:createtime', 'Test', function (err, isMember) { assert.ifError(err); assert.strictEqual(isMember, false); done(); @@ -334,7 +344,7 @@ describe('Groups', function() { }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/tests/messaging.js b/test/messaging.js similarity index 59% rename from tests/messaging.js rename to test/messaging.js index 4758fde40b..9c85259ede 100644 --- a/tests/messaging.js +++ b/test/messaging.js @@ -9,14 +9,18 @@ var assert = require('assert'), Messaging = require('../src/messaging'), testUids; -describe('Messaging Library', function() { - before(function(done) { +describe('Messaging Library', function () { + before(function (done) { // Create 3 users: 1 admin, 2 regular async.parallel([ async.apply(User.create, { username: 'foo', password: 'bar' }), // admin async.apply(User.create, { username: 'baz', password: 'quux' }), // restricted user async.apply(User.create, { username: 'herp', password: 'derp' }) // regular user - ], function(err, uids) { + ], function (err, uids) { + if (err) { + return done(err); + } + testUids = uids; async.parallel([ async.apply(Groups.join, 'administrators', uids[0]), @@ -25,40 +29,40 @@ describe('Messaging Library', function() { }); }); - describe('.canMessage()', function() { - it('should not error out', function(done) { - Messaging.canMessageUser(testUids[1], testUids[2], function(err) { + describe('.canMessage()', function () { + it('should not error out', function (done) { + Messaging.canMessageUser(testUids[1], testUids[2], function (err) { assert.ifError(err); done(); }); }); - it('should allow messages to be sent to an unrestricted user', function(done) { - Messaging.canMessageUser(testUids[1], testUids[2], function(err) { + it('should allow messages to be sent to an unrestricted user', function (done) { + Messaging.canMessageUser(testUids[1], testUids[2], function (err) { assert.ifError(err); done(); }); }); - it('should NOT allow messages to be sent to a restricted user', function(done) { - User.setSetting(testUids[1], 'restrictChat', '1', function() { - Messaging.canMessageUser(testUids[2], testUids[1], function(err) { + it('should NOT allow messages to be sent to a restricted user', function (done) { + User.setSetting(testUids[1], 'restrictChat', '1', function () { + Messaging.canMessageUser(testUids[2], testUids[1], function (err) { assert.strictEqual(err.message, '[[error:chat-restricted]]'); done(); }); }); }); - it('should always allow admins through', function(done) { - Messaging.canMessageUser(testUids[0], testUids[1], function(err) { + it('should always allow admins through', function (done) { + Messaging.canMessageUser(testUids[0], testUids[1], function (err) { assert.ifError(err); done(); }); }); - it('should allow messages to be sent to a restricted user if restricted user follows sender', function(done) { - User.follow(testUids[1], testUids[2], function() { - Messaging.canMessageUser(testUids[2], testUids[1], function(err) { + it('should allow messages to be sent to a restricted user if restricted user follows sender', function (done) { + User.follow(testUids[1], testUids[2], function () { + Messaging.canMessageUser(testUids[2], testUids[1], function (err) { assert.ifError(err); done(); }); @@ -66,7 +70,7 @@ describe('Messaging Library', function() { }); }); - after(function() { - db.flushdb(); + after(function (done) { + db.flushdb(done); }); }); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000000..b0a5a2aa02 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--reporter dot +--timeout 10000 diff --git a/tests/mocks/databasemock.js b/test/mocks/databasemock.js similarity index 91% rename from tests/mocks/databasemock.js rename to test/mocks/databasemock.js index a895d25c8a..36e696f4d2 100644 --- a/tests/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -3,7 +3,7 @@ * ATTENTION: testing db is flushed before every use! */ -(function(module) { +(function (module) { 'use strict'; /*global require, before*/ @@ -29,22 +29,22 @@ if(!testDbConfig){ errorText = 'test_database is not defined'; winston.info( - '\n===========================================================\n'+ - 'Please, add parameters for test database in config.json\n'+ - 'For example (redis):\n'+ + '\n===========================================================\n' + + 'Please, add parameters for test database in config.json\n' + + 'For example (redis):\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1",' + '\n' + ' "port": "6379",' + '\n' + ' "password": "",' + '\n' + ' "database": "1"' + '\n' + - '}\n'+ + '}\n' + ' or (mongo):\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1",' + '\n' + ' "port": "27017",' + '\n' + ' "password": "",' + '\n' + ' "database": "1"' + '\n' + - '}\n'+ + '}\n' + ' or (mongo) in a replicaset' + '\n' + '"test_database": {' + '\n' + ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",' + '\n' + @@ -52,7 +52,7 @@ ' "username": "",' + '\n' + ' "password": "",' + '\n' + ' "database": "nodebb_test"' + '\n' + - '}\n'+ + '}\n' + '===========================================================' ); winston.error(errorText); @@ -73,10 +73,14 @@ var db = require('../../src/database'), meta = require('../../src/meta'); - before(function(done) { - db.init(function(err) { + before(function (done) { + db.init(function (err) { + if (err) { + return done(err); + } + //Clean up - db.flushdb(function(err) { + db.flushdb(function (err) { if(err) { winston.error(err); throw new Error(err); diff --git a/test/notifications.js b/test/notifications.js new file mode 100644 index 0000000000..482af2d9bb --- /dev/null +++ b/test/notifications.js @@ -0,0 +1,102 @@ +'use strict'; +/*global require, after, before*/ + + +var assert = require('assert'); + +var db = require('./mocks/databasemock'); +var user = require('../src/user'); +var notifications = require('../src/notifications'); + +describe('Notifications', function () { + var uid; + var notification; + + before(function (done) { + user.create({username: 'poster'}, function (err, _uid) { + if (err) { + return done(err); + } + + uid = _uid; + done(); + }); + }); + + it('should create a notification', function (done) { + notifications.create({ + bodyShort: 'bodyShort', + nid: 'notification_id' + }, function (err, _notification) { + notification = _notification; + assert.ifError(err); + assert(notification); + db.exists('notifications:' + notification.nid, function (err, exists) { + assert.ifError(err); + assert(exists); + db.isSortedSetMember('notifications', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }); + }); + }); + + it('should get notifications', function (done) { + notifications.getMultiple([notification.nid], function (err, notificationsData) { + assert.ifError(err); + assert(Array.isArray(notificationsData)); + assert(notificationsData[0]); + assert.equal(notification.nid, notificationsData[0].nid); + done(); + }); + }); + + it('should push a notification to uid', function (done) { + notifications.push(notification, [uid], function (err) { + assert.ifError(err); + setTimeout(function () { + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }, 2000); + }); + }); + + it('should mark a notification read', function (done) { + notifications.markRead(notification.nid, uid, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, false); + db.isSortedSetMember('uid:' + uid + ':notifications:read', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, true); + done(); + }); + }); + }); + }); + + it('should mark a notification unread', function (done) { + notifications.markUnread(notification.nid, uid, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + uid + ':notifications:unread', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, true); + db.isSortedSetMember('uid:' + uid + ':notifications:read', notification.nid, function (err, isMember) { + assert.ifError(err); + assert.equal(isMember, false); + done(); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/posts.js b/test/posts.js new file mode 100644 index 0000000000..00c9aa01a3 --- /dev/null +++ b/test/posts.js @@ -0,0 +1,136 @@ +'use strict'; +/*global require, before, after*/ + +var assert = require('assert'); +var async = require('async'); + +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var posts = require('../src/posts'); +var categories = require('../src/categories'); +var user = require('../src/user'); + +describe('Post\'s', function () { + var voterUid; + var voteeUid; + var postData; + + before(function (done) { + async.parallel({ + voterUid: function (next) { + user.create({username: 'upvoter'}, next); + }, + voteeUid: function (next) { + user.create({username: 'upvotee'}, next); + }, + category: function (next) { + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script' + }, next); + } + }, function (err, results) { + if (err) { + return done(err); + } + + voterUid = results.voterUid; + voteeUid = results.voteeUid; + + topics.post({ + uid: results.voteeUid, + cid: results.category.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }, function (err, data) { + if (err) { + return done(err); + } + postData = data.postData; + done(); + }); + }); + }); + + describe('voting', function () { + + it('should upvote a post', function (done) { + posts.upvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 1); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 1); + assert.equal(result.user.reputation, 1); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, true); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should unvote a post', function (done) { + posts.unvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 0); + assert.equal(result.user.reputation, 0); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should downvote a post', function (done) { + posts.downvote(postData.pid, voterUid, function (err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 1); + assert.equal(result.post.votes, -1); + assert.equal(result.user.reputation, -1); + posts.hasVoted(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, true); + done(); + }); + }); + }); + }); + + describe('bookmarking', function () { + it('should bookmark a post', function (done) { + posts.bookmark(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, true); + posts.hasBookmarked(postData.pid, voterUid, function (err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked, true); + done(); + }); + }); + }); + + it('should unbookmark a post', function (done) { + posts.unbookmark(postData.pid, voterUid, function (err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, false); + posts.hasBookmarked([postData.pid], voterUid, function (err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked[0], false); + done(); + }); + }); + }); + }); + + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/topics.js b/test/topics.js new file mode 100644 index 0000000000..d0af967a0f --- /dev/null +++ b/test/topics.js @@ -0,0 +1,451 @@ +'use strict'; +/*global require, before, beforeEach, after*/ + +var assert = require('assert'); +var validator = require('validator'); +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var categories = require('../src/categories'); +var User = require('../src/user'); +var groups = require('../src/groups'); +var async = require('async'); + +describe('Topic\'s', function () { + var topic, + categoryObj; + + before(function (done) { + var userData = { + username: 'John Smith', + password: 'swordfish', + email: 'john@example.com', + callback: undefined + }; + + User.create({username: userData.username, password: userData.password, email: userData.email}, function (err, uid) { + if (err) { + return done(err); + } + + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + icon: 'fa-check', + blockclass: 'category-blue', + order: '5' + }, function (err, category) { + if (err) { + return done(err); + } + + categoryObj = category; + + topic = { + userId: uid, + categoryId: categoryObj.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }; + done(); + }); + }); + + + }); + + describe('.post', function () { + + it('should create a new topic with proper parameters', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + + done(); + }); + }); + + it('should fail to create new topic with invalid user id', function (done) { + topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should fail to create new topic with empty title', function (done) { + topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new topic with empty content', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new topic with non-existant category id', function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function (err) { + assert.equal(err.message, '[[error:no-category]]', 'received no error'); + done(); + }); + }); + }); + + describe('.reply', function () { + var newTopic; + var newPost; + + before(function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newPost = result.postData; + done(); + }); + }); + + it('should create a new reply with proper parameters', function (done) { + topics.reply({uid: topic.userId, content: 'test post', tid: newTopic.tid}, function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + + done(); + }); + }); + + it('should fail to create new reply with invalid user id', function (done) { + topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should fail to create new reply with empty content', function (done) { + topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function (err) { + assert.ok(err); + done(); + }); + }); + + it('should fail to create new reply with invalid topic id', function (done) { + topics.reply({uid: null, content: 'test post', tid: 99}, function (err) { + assert.equal(err.message, '[[error:no-topic]]'); + done(); + }); + }); + }); + + describe('Get methods', function () { + var newTopic; + var newPost; + + before(function (done) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newPost = result.postData; + done(); + }); + }); + + describe('.getTopicData', function () { + it('should not receive errors', function (done) { + topics.getTopicData(newTopic.tid, done); + }); + }); + + describe('.getTopicWithPosts', function () { + it('should get a topic with posts and other data', function (done) { + topics.getTopicData(newTopic.tid, function (err, topicData) { + if (err) { + return done(err); + } + topics.getTopicWithPosts(topicData, 'tid:' + newTopic.tid + ':posts', topic.userId, 0, -1, false, function (err, data) { + if (err) { + return done(err); + } + assert(data); + assert.equal(data.category.cid, topic.categoryId); + assert.equal(data.unreplied, true); + assert.equal(data.deleted, false); + assert.equal(data.locked, false); + assert.equal(data.pinned, false); + done(); + }); + }); + }); + }); + }); + + describe('Title escaping', function () { + + it('should properly escape topic title', function (done) { + var title = '"<script>alert(\'ok1\');</script> new topic test'; + var titleEscaped = validator.escape(title); + topics.post({uid: topic.userId, title: title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError(err); + topics.getTopicData(result.topicData.tid, function (err, topicData) { + assert.ifError(err); + assert.strictEqual(topicData.titleRaw, title); + assert.strictEqual(topicData.title, titleEscaped); + done(); + }); + }); + }); + }); + + describe('.purge/.delete', function () { + var newTopic; + var followerUid; + before(function (done) { + async.waterfall([ + function (next) { + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError(err); + newTopic = result.topicData; + next(); + }); + }, + function (next) { + User.create({username: 'topicFollower', password: '123456'}, next); + }, + function (_uid, next) { + followerUid = _uid; + topics.follow(newTopic.tid, _uid, next); + } + ], done); + }); + + it('should delete the topic', function (done) { + topics.delete(newTopic.tid, 1, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should purge the topic', function (done) { + topics.purge(newTopic.tid, 1, function (err) { + assert.ifError(err); + db.isSortedSetMember('uid:' + followerUid + ':followed_tids', newTopic.tid, function (err, isMember) { + assert.ifError(err); + assert.strictEqual(false, isMember); + done(); + }); + }); + }); + }); + + describe('.ignore', function (){ + var newTid; + var uid; + var newTopic; + before(function (done){ + uid = topic.userId; + async.waterfall([ + function (done){ + topics.post({uid: topic.userId, title: 'Topic to be ignored', content: 'Just ignore me, please!', cid: topic.categoryId}, function (err, result) { + if (err) { + return done(err); + } + + newTopic = result.topicData; + newTid = newTopic.tid; + done(); + }); + }, + function (done){ + topics.markUnread( newTid, uid, done ); + } + ],done); + }); + + it('should not appear in the unread list', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.equal(tids.indexOf(newTid), -1, 'The topic appeared in the unread list.'); + done(); + } + ], done); + }); + + it('should not appear as unread in the recent list', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.getLatestTopics( uid, 0, -1, 'year', done ); + }, + function (results, done){ + var topics = results.topics; + var topic; + var i; + for(i = 0; i < topics.length; ++i){ + if( topics[i].tid == newTid ){ + assert.equal(false, topics[i].unread, 'ignored topic was marked as unread in recent list'); + return done(); + } + } + assert.ok(topic, 'topic didn\'t appear in the recent list'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as reading', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.follow( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + + it('should appear as unread again when marked as following', function (done){ + async.waterfall([ + function (done){ + topics.ignore( newTid, uid, done ); + }, + function (done){ + topics.follow( newTid, uid, done ); + }, + function (done){ + topics.getUnreadTopics(0, uid, 0, -1, '', done ); + }, + function (results, done){ + var topics = results.topics; + var tids = topics.map( function (topic){ return topic.tid; } ); + assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); + done(); + } + ], done); + }); + }); + + + + describe('.fork', function (){ + var newTopic; + var replies = []; + var topicPids; + var originalBookmark = 5; + function postReply( next ){ + topics.reply({uid: topic.userId, content: 'test post ' + replies.length, tid: newTopic.tid}, + function (err, result) { + assert.equal(err, null, 'was created with error'); + assert.ok(result); + replies.push( result ); + next(); + } + ); + } + + before( function (done) { + async.waterfall( + [ + function (next){ + groups.join('administrators', topic.userId, next); + }, + function ( next ){ + topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function (err, result) { + assert.ifError( err ); + newTopic = result.topicData; + next(); + }); + }, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ postReply( next );}, + function ( next ){ + topicPids = replies.map( function ( reply ){ return reply.pid; } ); + topics.setUserBookmark( newTopic.tid, topic.userId, originalBookmark, next ); + }], + done ); + }); + + it('should have 12 replies', function (done) { + assert.equal( 12, replies.length ); + done(); + }); + + it('should not update the user\'s bookmark', function (done){ + async.waterfall([ + function (next){ + topics.createTopicFromPosts( + topic.userId, + 'Fork test, no bookmark update', + topicPids.slice( -2 ), + newTopic.tid, + next ); + }, + function ( forkedTopicData, next){ + topics.getUserBookmark( newTopic.tid, topic.userId, next ); + }, + function ( bookmark, next ){ + assert.equal( originalBookmark, bookmark ); + next(); + } + ],done); + }); + + it('should update the user\'s bookmark ', function (done){ + async.waterfall([ + function (next){ + topics.createTopicFromPosts( + topic.userId, + 'Fork test, no bookmark update', + topicPids.slice( 1, 3 ), + newTopic.tid, + next ); + }, + function ( forkedTopicData, next){ + topics.getUserBookmark( newTopic.tid, topic.userId, next ); + }, + function ( bookmark, next ){ + assert.equal( originalBookmark - 2, bookmark ); + next(); + } + ],done); + }); + }); + + after(function (done) { + db.flushdb(done); + }); +}); diff --git a/test/translator.js b/test/translator.js new file mode 100644 index 0000000000..5d8c234747 --- /dev/null +++ b/test/translator.js @@ -0,0 +1,188 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var shim = require('../public/src/modules/translator.js'); +var Translator = shim.Translator; + +require('../src/languages').init(function () {}); + +describe('translator shim', function (){ + describe('.translate()', function (){ + it('should translate correctly', function (done) { + shim.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]', function (translated) { + assert.strictEqual(translated, '(foobar) out of Home'); + done(); + }); + }); + }); +}); + +describe('new Translator(language)', function (){ + describe('.translate()', function (){ + it('should handle basic translations', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Home'); + done(); + }); + }); + + it('should handle language keys in regular text', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('Let\'s go [[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Let\'s go Home'); + done(); + }); + }); + + it('should accept a language parameter and adjust accordingly', function (done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]]').then(function (translated) { + assert.strictEqual(translated, 'Übersicht'); + done(); + }); + }); + + it('should handle language keys in regular text with another language specified', function (done) { + var translator = new Translator('de'); + + translator.translate('[[global:home]] test').then(function (translated) { + assert.strictEqual(translated, 'Übersicht test'); + done(); + }); + }); + + it('should handle language keys with parameters', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, 1, 5]]').then(function (translated) { + assert.strictEqual(translated, '1 out of 5'); + done(); + }); + }); + + it('should handle language keys inside language keys', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]').then(function (translated) { + assert.strictEqual(translated, 'You are now leaving Guest'); + done(); + }); + }); + + it('should handle language keys inside language keys with multiple parameters', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]').then(function (translated) { + assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>My Topic</strong>'); + done(); + }); + }); + + it('should handle language keys inside language keys with all parameters as language keys', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]').then(function (translated) { + assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>Guest</strong>'); + done(); + }); + }); + + it('should properly handle parameters that contain square brackets', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]').then(function (translated) { + assert.strictEqual(translated, '[guest] out of Home'); + done(); + }); + }); + + it('should properly handle parameters that contain parentheses', function (done) { + var translator = new Translator('en_GB'); + + translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]').then(function (translated) { + assert.strictEqual(translated, '(foobar) out of Home'); + done(); + }); + }); + + it('should not translate language key parameters with HTML in them', function (done) { + var translator = new Translator('en_GB'); + + var key = '[[global:403.login, <strong>test</strong>]]'; + translator.translate(key).then(function (translated) { + assert.strictEqual(translated, 'Perhaps you should <a href=\'<strong>test</strong>/login\'>try logging in</a>?'); + done(); + }); + }); + + it('should properly escape % and ,', function (done) { + var translator = new Translator('en_GB'); + + var title = 'Test 1, 2, 3 % salmon'; + title = title.replace(/%/g, '%').replace(/,/g, ','); + var key = "[[topic:composer.replying_to, " + title + "]]"; + translator.translate(key).then(function (translated) { + assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon'); + done(); + }); + }); + + it('should throw if not passed a language', function (done) { + assert.throws(function () { + new Translator(); + }, /language string/); + done(); + }); + + it('should not translate [[derp] some text', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[derp] some text').then(function (translated) { + assert.strictEqual('[[derp] some text', translated); + done(); + }); + }); + + it('should not translate [[derp:xyz] some text', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[derp:xyz] some text').then(function (translated) { + assert.strictEqual('[[derp:xyz] some text', translated); + done(); + }); + }); + + it('should translate [[pages:users/latest]] properly', function (done) { + var translator = new Translator('en_GB'); + translator.translate('[[pages:users/latest]]').then(function (translated) { + assert.strictEqual(translated, 'Latest Users'); + done(); + }); + }); + }); +}); + +describe('Translator.create()', function (){ + it('should return an instance of Translator', function (done) { + var translator = Translator.create('en_GB'); + + assert(translator instanceof Translator); + done(); + }); + it('should return the same object for the same language', function (done) { + var one = Translator.create('de'); + var two = Translator.create('de'); + + assert.strictEqual(one, two); + done(); + }); + it('should default to defaultLang', function (done) { + var translator = Translator.create(); + + assert.strictEqual(translator.lang, 'en_GB'); + done(); + }); +}); diff --git a/tests/user.js b/test/user.js similarity index 52% rename from tests/user.js rename to test/user.js index 570b002808..5e30d2fb5a 100644 --- a/tests/user.js +++ b/test/user.js @@ -17,23 +17,30 @@ var User = require('../src/user'), Meta = require('../src/meta'), Password = require('../src/password'); -describe('User', function() { +describe('User', function () { var userData, testUid, testCid; - before(function(done) { + before(function (done) { + var groups = require('../src/groups'); + groups.resetCache(); + Categories.create({ name: 'Test Category', description: 'A test', order: 1 - }, function(err, categoryObj) { + }, function (err, categoryObj) { + if (err) { + return done(err); + } + testCid = categoryObj.cid; done(); }); }); - beforeEach(function(){ + beforeEach(function () { userData = { username: 'John Smith', fullname: 'John Smith McNamara', @@ -44,9 +51,9 @@ describe('User', function() { }); - describe('.create(), when created', function() { - it('should be created properly', function(done) { - User.create({username: userData.username, password: userData.password, email: userData.email}, function(error,userId){ + describe('.create(), when created', function () { + it('should be created properly', function (done) { + User.create({username: userData.username, password: userData.password, email: userData.email}, function (error,userId){ assert.equal(error, null, 'was created with error'); assert.ok(userId); @@ -55,8 +62,8 @@ describe('User', function() { }); }); - it('should have a valid email, if using an email', function(done) { - User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function(err) { + it('should have a valid email, if using an email', function (done) { + User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function (err) { assert(err); assert.equal(err.message, '[[error:invalid-email]]'); done(); @@ -64,24 +71,27 @@ describe('User', function() { }); }); - describe('.isModerator()', function() { - it('should return false', function(done) { - User.isModerator(testUid, testCid, function(err, isModerator) { + describe('.isModerator()', function () { + it('should return false', function (done) { + User.isModerator(testUid, testCid, function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator, false); done(); }); }); - it('should return two false results', function(done) { - User.isModerator([testUid, testUid], testCid, function(err, isModerator) { + it('should return two false results', function (done) { + User.isModerator([testUid, testUid], testCid, function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator[0], false); assert.equal(isModerator[1], false); done(); }); }); - it('should return two false results', function(done) { - User.isModerator(testUid, [testCid, testCid], function(err, isModerator) { + it('should return two false results', function (done) { + User.isModerator(testUid, [testCid, testCid], function (err, isModerator) { + assert.equal(err, null); assert.equal(isModerator[0], false); assert.equal(isModerator[1], false); done(); @@ -89,8 +99,8 @@ describe('User', function() { }); }); - describe('.isReadyToPost()', function() { - it('should error when a user makes two posts in quick succession', function(done) { + describe('.isReadyToPost()', function () { + it('should error when a user makes two posts in quick succession', function (done) { Meta.config = Meta.config || {}; Meta.config.postDelay = '10'; @@ -107,54 +117,54 @@ describe('User', function() { content: 'lorem ipsum', cid: testCid }) - ], function(err) { + ], function (err) { assert(err); done(); }); }); - it('should allow a post if the last post time is > 10 seconds', function(done) { - User.setUserField(testUid, 'lastposttime', +new Date()-(11*1000), function() { + it('should allow a post if the last post time is > 10 seconds', function (done) { + User.setUserField(testUid, 'lastposttime', +new Date() - (11 * 1000), function () { Topics.post({ uid: testUid, title: 'Topic 3', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert.ifError(err); done(); }); }); }); - it('should error when a new user posts if the last post time is 10 < 30 seconds', function(done) { + it('should error when a new user posts if the last post time is 10 < 30 seconds', function (done) { Meta.config.newbiePostDelay = 30; Meta.config.newbiePostDelayThreshold = 3; - User.setUserField(testUid, 'lastposttime', +new Date()-(20*1000), function() { + User.setUserField(testUid, 'lastposttime', +new Date() - (20 * 1000), function () { Topics.post({ uid: testUid, title: 'Topic 4', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert(err); done(); }); }); }); - it('should not error if a non-newbie user posts if the last post time is 10 < 30 seconds', function(done) { + it('should not error if a non-newbie user posts if the last post time is 10 < 30 seconds', function (done) { User.setUserFields(testUid, { - lastposttime: +new Date()-(20*1000), + lastposttime: +new Date() - (20 * 1000), reputation: 10 - }, function() { + }, function () { Topics.post({ uid: testUid, title: 'Topic 5', content: 'lorem ipsum', cid: testCid - }, function(err) { + }, function (err) { assert.ifError(err); done(); }); @@ -162,9 +172,9 @@ describe('User', function() { }); }); - describe('.search()', function() { - it('should return an object containing an array of matching users', function(done) { - User.search({query: 'john'}, function(err, searchData) { + describe('.search()', function () { + it('should return an object containing an array of matching users', function (done) { + User.search({query: 'john'}, function (err, searchData) { assert.ifError(err); assert.equal(Array.isArray(searchData.users) && searchData.users.length > 0, true); assert.equal(searchData.users[0].username, 'John Smith'); @@ -173,20 +183,20 @@ describe('User', function() { }); }); - describe('.delete()', function() { + describe('.delete()', function () { var uid; - before(function(done) { - User.create({username: 'usertodelete', password: '123456', email: 'delete@me.com'}, function(err, newUid) { + before(function (done) { + User.create({username: 'usertodelete', password: '123456', email: 'delete@me.com'}, function (err, newUid) { assert.ifError(err); uid = newUid; done(); }); }); - it('should delete a user account', function(done) { - User.delete(1, uid, function(err) { + it('should delete a user account', function (done) { + User.delete(1, uid, function (err) { assert.ifError(err); - User.existsBySlug('usertodelete', function(err, exists) { + User.existsBySlug('usertodelete', function (err, exists) { assert.ifError(err); assert.equal(exists, false); done(); @@ -195,19 +205,19 @@ describe('User', function() { }); }); - describe('passwordReset', function() { + describe('passwordReset', function () { var uid, code; - before(function(done) { - User.create({username: 'resetuser', password: '123456', email: 'reset@me.com'}, function(err, newUid) { + before(function (done) { + User.create({username: 'resetuser', password: '123456', email: 'reset@me.com'}, function (err, newUid) { assert.ifError(err); uid = newUid; done(); }); }); - it('.generate() should generate a new reset code', function(done) { - User.reset.generate(uid, function(err, _code) { + it('.generate() should generate a new reset code', function (done) { + User.reset.generate(uid, function (err, _code) { assert.ifError(err); assert(_code); @@ -216,36 +226,36 @@ describe('User', function() { }); }); - it('.validate() should ensure that this new code is valid', function(done) { - User.reset.validate(code, function(err, valid) { + it('.validate() should ensure that this new code is valid', function (done) { + User.reset.validate(code, function (err, valid) { assert.ifError(err); assert.strictEqual(valid, true); done(); }); }); - it('.validate() should correctly identify an invalid code', function(done) { - User.reset.validate(code + 'abcdef', function(err, valid) { + it('.validate() should correctly identify an invalid code', function (done) { + User.reset.validate(code + 'abcdef', function (err, valid) { assert.ifError(err); assert.strictEqual(valid, false); done(); }); }); - it('.send() should create a new reset code and reset password', function(done) { - User.reset.send('reset@me.com', function(err, code) { + it('.send() should create a new reset code and reset password', function (done) { + User.reset.send('reset@me.com', function (err, code) { assert.ifError(err); done(); }); }); - it('.commit() should update the user\'s password', function(done) { - User.reset.commit(code, 'newpassword', function(err) { + it('.commit() should update the user\'s password', function (done) { + User.reset.commit(code, 'newpassword', function (err) { assert.ifError(err); - db.getObjectField('user:' + uid, 'password', function(err, newPassword) { + db.getObjectField('user:' + uid, 'password', function (err, newPassword) { assert.ifError(err); - Password.compare('newpassword', newPassword, function(err, match) { + Password.compare('newpassword', newPassword, function (err, match) { assert.ifError(err); assert(match); done(); @@ -255,26 +265,26 @@ describe('User', function() { }); }); - describe('hash methods', function() { + describe('hash methods', function () { - it('should return uid from email', function(done) { - User.getUidByEmail('john@example.com', function(err, uid) { + it('should return uid from email', function (done) { + User.getUidByEmail('john@example.com', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); }); }); - it('should return uid from username', function(done) { - User.getUidByUsername('John Smith', function(err, uid) { + it('should return uid from username', function (done) { + User.getUidByUsername('John Smith', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); }); }); - it('should return uid from userslug', function(done) { - User.getUidByUserslug('john-smith', function(err, uid) { + it('should return uid from userslug', function (done) { + User.getUidByUserslug('john-smith', function (err, uid) { assert.ifError(err); assert.equal(parseInt(uid, 10), parseInt(testUid, 10)); done(); @@ -282,7 +292,43 @@ describe('User', function() { }); }); - after(function() { - db.flushdb(); + describe('updateProfile', function () { + var uid; + before(function (done) { + User.create({username: 'updateprofile', email: 'update@me.com'}, function (err, newUid) { + assert.ifError(err); + uid = newUid; + done(); + }); + }); + + it('should update a user\'s profile', function (done) { + var data = { + username: 'updatedUserName', + email: 'updatedEmail@me.com', + fullname: 'updatedFullname', + website: 'http://nodebb.org', + location: 'izmir', + groupTitle: 'testGroup', + birthday: '01/01/1980', + signature: 'nodebb is good' + }; + + User.updateProfile(uid, data, function (err, result) { + assert.ifError(err); + assert(result); + db.getObject('user:' + uid, function (err, userData) { + assert.ifError(err); + Object.keys(data).forEach(function (key) { + assert.equal(data[key], userData[key]); + }); + done(); + }); + }); + }); + }); + + after(function (done) { + db.flushdb(done); }); }); \ No newline at end of file diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000000..d342eca7a5 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,55 @@ +'use strict'; +/*global require*/ + +var assert = require('assert'); +var utils = require('./../public/src/utils.js'); + + +describe('Utility Methods', function () { + describe('username validation', function () { + it('accepts latin-1 characters', function (){ + var username = "John\"'-. Doeäâèéë1234"; + assert(utils.isUserNameValid(username), 'invalid username'); + }); + it('rejects empty string', function () { + var username = ''; + assert.ifError(utils.isUserNameValid(username), 'accepted as valid username'); + }); + }); + + describe('email validation', function () { + it('accepts sample address', function () { + var email = 'sample@example.com'; + assert(utils.isEmailValid(email), 'invalid email'); + }); + it('rejects empty address', function () { + var email = ''; + assert.ifError(utils.isEmailValid(email), 'accepted as valid email'); + }); + }); + + describe('UUID generation', function () { + it('return unique random value every time', function (){ + var uuid1 = utils.generateUUID(), + uuid2 = utils.generateUUID(); + assert.notEqual(uuid1, uuid2, 'matches'); + }); + }); + + describe('cleanUpTag', function () { + it('should cleanUp a tag', function (done) { + var cleanedTag = utils.cleanUpTag(',\/#!$%\^\*;TaG1:{}=_`<>\'"~()?\|'); + assert.equal(cleanedTag, 'tag1'); + done(); + }); + + it('should return empty string for invalid tags', function (done) { + assert.strictEqual(utils.cleanUpTag(undefined), ''); + assert.strictEqual(utils.cleanUpTag(null), ''); + assert.strictEqual(utils.cleanUpTag(false), ''); + assert.strictEqual(utils.cleanUpTag(1), ''); + assert.strictEqual(utils.cleanUpTag(0), ''); + done(); + }); + }); +}); diff --git a/tests/categories.js b/tests/categories.js deleted file mode 100644 index 7115b88597..0000000000 --- a/tests/categories.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; -/*global require, process, after*/ - -var winston = require('winston'); - -process.on('uncaughtException', function (err) { - winston.error('Encountered error while running test suite: ' + err.message); -}); - -var assert = require('assert'), - db = require('./mocks/databasemock'); - -var Categories = require('../src/categories'); - -describe('Categories', function() { - var categoryObj; - - describe('.create', function() { - it('should create a new category', function(done) { - - Categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - icon: 'fa-check', - blockclass: 'category-blue', - order: '5' - }, function(err, category) { - categoryObj = category; - done.apply(this, arguments); - }); - }); - }); - - describe('.getCategoryById', function() { - it('should retrieve a newly created category by its ID', function(done) { - Categories.getCategoryById({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':tids', - reverse: true, - start: 0, - end: -1, - uid: 0 - }, function(err, categoryData) { - assert(categoryData); - assert.equal(categoryObj.name, categoryData.name); - assert.equal(categoryObj.description, categoryData.description); - - done(); - }); - }); - }); - - describe('.getCategoryTopics', function() { - it('should return a list of topics', function(done) { - Categories.getCategoryTopics({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':tids', - reverse: true, - start: 0, - stop: 10, - uid: 0 - }, function(err, result) { - assert(Array.isArray(result.topics)); - assert(result.topics.every(function(topic) { - return topic instanceof Object; - })); - - done(); - }); - }); - - it('should return a list of topics by a specific user', function(done) { - Categories.getCategoryTopics({ - cid: categoryObj.cid, - set: 'cid:' + categoryObj.cid + ':uid:' + 1 + ':tids', - reverse: true, - start: 0, - stop: 10, - uid: 0, - targetUid: 1 - }, function(err, result) { - assert(Array.isArray(result.topics)); - assert(result.topics.every(function(topic) { - return topic instanceof Object && topic.uid === '1'; - })); - - done(); - }); - }); - }); - - after(function() { - db.flushdb(); - }); -}); diff --git a/tests/topics.js b/tests/topics.js deleted file mode 100644 index fe00db7646..0000000000 --- a/tests/topics.js +++ /dev/null @@ -1,194 +0,0 @@ -'use strict'; -/*global require, before, beforeEach, after*/ - -var assert = require('assert'); -var validator = require('validator'); -var db = require('./mocks/databasemock'); -var topics = require('../src/topics'); -var categories = require('../src/categories'); -var User = require('../src/user'); - -describe('Topic\'s', function() { - var topic, - categoryObj; - - before(function(done) { - var userData = { - username: 'John Smith', - password: 'swordfish', - email: 'john@example.com', - callback: undefined - }; - - User.create({username: userData.username, password: userData.password, email: userData.email}, function(err, uid) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - icon: 'fa-check', - blockclass: 'category-blue', - order: '5' - }, function(err, category) { - categoryObj = category; - - topic = { - userId: uid, - categoryId: categoryObj.cid, - title: 'Test Topic Title', - content: 'The content of test topic' - }; - done(); - }); - }); - - - }); - - describe('.post', function() { - - it('should create a new topic with proper parameters', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.equal(err, null, 'was created with error'); - assert.ok(result); - - done(); - }); - }); - - it('should fail to create new topic with invalid user id', function(done) { - topics.post({uid: null, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.equal(err.message, '[[error:no-privileges]]'); - done(); - }); - }); - - it('should fail to create new topic with empty title', function(done) { - topics.post({uid: topic.userId, title: '', content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new topic with empty content', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: '', cid: topic.categoryId}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new topic with non-existant category id', function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: 99}, function(err, result) { - assert.equal(err.message, '[[error:no-category]]', 'received no error'); - done(); - }); - }); - }); - - describe('.reply', function() { - var newTopic; - var newPost; - - before(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - newPost = result.postData; - done(); - }); - }); - - it('should create a new reply with proper parameters', function(done) { - topics.reply({uid: topic.userId, content: 'test post', tid: newTopic.tid}, function(err, result) { - assert.equal(err, null, 'was created with error'); - assert.ok(result); - - done(); - }); - }); - - it('should fail to create new reply with invalid user id', function(done) { - topics.reply({uid: null, content: 'test post', tid: newTopic.tid}, function(err, result) { - assert.equal(err.message, '[[error:no-privileges]]'); - done(); - }); - }); - - it('should fail to create new reply with empty content', function(done) { - topics.reply({uid: topic.userId, content: '', tid: newTopic.tid}, function(err, result) { - assert.ok(err); - done(); - }); - }); - - it('should fail to create new reply with invalid topic id', function(done) { - topics.reply({uid: null, content: 'test post', tid: 99}, function(err, result) { - assert.equal(err.message, '[[error:no-topic]]'); - done(); - }); - }); - }); - - describe('Get methods', function() { - var newTopic; - var newPost; - - beforeEach(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - newPost = result.postData; - done(); - }); - }); - - describe('.getTopicData', function() { - it('should not receive errors', function(done) { - topics.getTopicData(newTopic.tid, done); - }); - }); - }); - - describe('Title escaping', function() { - - it('should properly escape topic title', function(done) { - var title = '"<script>alert(\'ok1\');</script> new topic test'; - var titleEscaped = validator.escape(title); - topics.post({uid: topic.userId, title: title, content: topic.content, cid: topic.categoryId}, function(err, result) { - assert.ifError(err); - topics.getTopicData(result.topicData.tid, function(err, topicData) { - assert.ifError(err); - assert.strictEqual(topicData.titleRaw, title); - assert.strictEqual(topicData.title, titleEscaped); - done(); - }); - }); - }); - }); - - describe('.purge/.delete', function() { - var newTopic; - - before(function(done) { - topics.post({uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId}, function(err, result) { - newTopic = result.topicData; - done(); - }); - }); - - it('should delete the topic', function(done) { - topics.delete(newTopic.tid, 1, function(err) { - assert.ifError(err); - done(); - }); - }); - - it('should purge the topic', function(done) { - topics.purge(newTopic.tid, 1, function(err) { - assert.ifError(err); - done(); - }); - }); - }); - - - after(function() { - db.flushdb(); - }); -}); diff --git a/tests/translator.js b/tests/translator.js deleted file mode 100644 index 7546ec1d90..0000000000 --- a/tests/translator.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; -/*global require*/ - -var assert = require('assert'); -var translator = require('../public/src/modules/translator.js'); - - -describe('Translator', function(){ - describe('.translate()', function(){ - it('should handle basic translations', function(done) { - translator.translate('[[global:home]]', function(translated) { - assert.strictEqual(translated, 'Home'); - done(); - }); - }); - - it('should handle language keys in regular text', function(done) { - translator.translate('Let\'s go [[global:home]]', function(translated) { - assert.strictEqual(translated, 'Let\'s go Home'); - done(); - }); - }); - - it('should accept a language parameter and adjust accordingly', function(done) { - translator.translate('[[global:home]]', 'de', function(translated) { - assert.strictEqual(translated, 'Übersicht'); - done(); - }); - }); - - it('should handle language keys in regular text with another language specified', function(done) { - translator.translate('[[global:home]] test', 'de', function(translated) { - assert.strictEqual(translated, 'Übersicht test'); - done(); - }); - }); - - it('should handle language keys with parameters', function(done) { - translator.translate('[[global:pagination.out_of, 1, 5]]', function(translated) { - assert.strictEqual(translated, '1 out of 5'); - done(); - }); - }); - - it('should handle language keys inside language keys', function(done) { - translator.translate('[[notifications:outgoing_link_message, [[global:guest]]]]', function(translated) { - assert.strictEqual(translated, 'You are now leaving Guest'); - done(); - }); - }); - - it('should handle language keys inside language keys with multiple parameters', function(done) { - translator.translate('[[notifications:user_posted_to, [[global:guest]], My Topic]]', function(translated) { - assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>My Topic</strong>'); - done(); - }); - }); - - it('should handle language keys inside language keys with all parameters as language keys', function(done) { - translator.translate('[[notifications:user_posted_to, [[global:guest]], [[global:guest]]]]', function(translated) { - assert.strictEqual(translated, '<strong>Guest</strong> has posted a reply to: <strong>Guest</strong>'); - done(); - }); - }); - - it('should properly handle parameters that contain square brackets', function(done) { - translator.translate('[[global:pagination.out_of, [guest], [[global:home]]]]', function(translated) { - assert.strictEqual(translated, '[guest] out of Home'); - done(); - }); - }); - - it('should properly handle parameters that contain parentheses', function(done) { - translator.translate('[[global:pagination.out_of, (foobar), [[global:home]]]]', function(translated) { - assert.strictEqual(translated, '(foobar) out of Home'); - done(); - }); - }); - - it('should not translate language key parameters with HTML in them', function(done) { - var key = '[[global:403.login, <strong>test</strong>]]'; - translator.translate(key, function(translated) { - assert.strictEqual(translated, 'Perhaps you should <a href=\'<strong>test</strong>/login\'>try logging in</a>?'); - done(); - }); - }); - - it('should properly escape % and ,', function(done) { - var title = 'Test 1, 2, 3 % salmon'; - title = title.replace(/%/g, '%').replace(/,/g, ','); - var key = "[[topic:composer.replying_to, " + title + "]]"; - translator.translate(key, function(translated) { - assert.strictEqual(translated, 'Replying to Test 1, 2, 3 % salmon'); - done(); - }); - }); - - }); -}); diff --git a/tests/utils.js b/tests/utils.js deleted file mode 100644 index befb2f78d2..0000000000 --- a/tests/utils.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; -/*global require*/ - -var assert = require('assert'), - utils = require('./../public/src/utils.js'); - - -describe('Utility Methods', function(){ - describe('username validation', function(){ - it('accepts latin-1 characters', function(){ - var username = "John\"'-. Doeäâèéë1234"; - assert(utils.isUserNameValid(username), 'invalid username'); - }); - it('rejects empty string', function(){ - var username = ''; - assert.ifError(utils.isUserNameValid(username), 'accepted as valid username'); - }); - }); - - describe('email validation', function(){ - it('accepts sample address', function(){ - var email = 'sample@example.com'; - assert(utils.isEmailValid(email), 'invalid email'); - }); - it('rejects empty address', function(){ - var email = ''; - assert.ifError(utils.isEmailValid(email), 'accepted as valid email'); - }); - }); - - describe('UUID generation', function(){ - it('return unique random value every time', function(){ - var uuid1 = utils.generateUUID(), - uuid2 = utils.generateUUID(); - assert.notEqual(uuid1, uuid2, 'matches'); - }); - }); -});