From a3cfcd9a48f400fd57f18d1aff1980bee4a0c0a4 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 27 Nov 2015 16:55:31 -0700 Subject: [PATCH 01/59] Maximum invites, invites stored Added the ability for admins to restrict the maximum amount of invites a user can make. Invites are stored and displayed in the registration queue admin page. --- public/language/en@pirate/error.json | 6 ++- public/language/en_GB/error.json | 6 ++- public/language/en_US/error.json | 6 ++- public/src/client/register.js | 4 +- src/controllers/admin/users.js | 47 ++++++++++++++++++-- src/controllers/users.js | 20 +++++++-- src/socket.io/user.js | 29 +++++++++++- src/user/invite.js | 59 ++++++++++++++++++++++++- src/views/admin/manage/registration.tpl | 46 ++++++++++++++++--- src/views/admin/settings/user.tpl | 11 ++++- 10 files changed, 206 insertions(+), 28 deletions(-) diff --git a/public/language/en@pirate/error.json b/public/language/en@pirate/error.json index c9751317f1..88c55e7a49 100644 --- a/public/language/en@pirate/error.json +++ b/public/language/en@pirate/error.json @@ -87,5 +87,7 @@ "registration-error": "Registration Error", "parse-error": "Something went wrong while parsing server response", "wrong-login-type-email": "Please use your email to login", - "wrong-login-type-username": "Please use your username to login" -} \ No newline at end of file + "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)." +} diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 4159d211d8..7842356067 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -113,5 +113,7 @@ "registration-error": "Registration Error", "parse-error": "Something went wrong while parsing server response", "wrong-login-type-email": "Please use your email to login", - "wrong-login-type-username": "Please use your username to login" -} \ No newline at end of file + "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)." +} diff --git a/public/language/en_US/error.json b/public/language/en_US/error.json index 2789d31b2d..15c7ee3d72 100644 --- a/public/language/en_US/error.json +++ b/public/language/en_US/error.json @@ -87,5 +87,7 @@ "registration-error": "Registration Error", "parse-error": "Something went wrong while parsing server response", "wrong-login-type-email": "Please use your email to login", - "wrong-login-type-username": "Please use your username to login" -} \ No newline at end of file + "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)." +} diff --git a/public/src/client/register.js b/public/src/client/register.js index ef11e4a4bd..3d0f943da9 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -28,7 +28,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { var query = utils.params(); if (query.email && query.token) { - email.val(query.email); + email.val(decodeURIComponent(query.email)); $('#token').val(query.token); } @@ -160,7 +160,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { socket.emit('user.exists', { username: username }, function(err, exists) { - if(err) { + if (err) { return app.alertError(err.message); } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 6c10694ba6..dc5420b401 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -1,6 +1,7 @@ "use strict"; -var user = require('../../user'), +var async = require('async'), + user = require('../../user'), meta = require('../../meta'); @@ -31,12 +32,50 @@ usersController.banned = function(req, res, next) { }; usersController.registrationQueue = function(req, res, next) { - user.getRegistrationQueue(0, -1, function(err, data) { + var invitations; + async.parallel({ + users: function(next) { + user.getRegistrationQueue(0, -1, next); + }, + invites: function(next) { + async.waterfall([ + function(next) { + user.getAllInvites(next); + }, + function(_invitations, next) { + invitations = _invitations; + async.map(invitations, function(invites, next) { + user.getUserField(invites.uid, 'username', next); + }, next); + }, + function(usernames, next) { + invitations.forEach(function(invites, index) { + invites.username = usernames[index]; + }); + 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) { + return { + email: email, + username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i] + }; + }); + }); + next(null, invitations); + } + ], next); + } + }, function(err, data) { if (err) { return next(err); } - res.render('admin/manage/registration', {users: data}); - }) + res.render('admin/manage/registration', data); + }); + }; function getUsers(set, req, res, next) { diff --git a/src/controllers/users.js b/src/controllers/users.js index 0087232255..21d1490717 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -150,14 +150,26 @@ usersController.getUsersForSearch = function(req, res, next) { }; function render(req, res, data, next) { - plugins.fireHook('filter:users.build', {req: req, res: res, templateData: data}, function(err, data) { + plugins.fireHook('filter:users.build', { req: req, res: res, templateData: data }, function(err, data) { if (err) { return next(err); } - data.templateData.inviteOnly = meta.config.registrationType === 'invite-only'; - data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - res.render('users', data.templateData); + if (!req.uid) { + return next(new Error('[[error:no-privileges]]')); + } + + user.getInvitesNumber(req.uid, function(err, num) { + if (err) { + return next(err); + } + + data.templateData.invites = num; + data.templateData.maximumInvites = meta.config.maximumInvites; + data.templateData.inviteOnly = meta.config.registrationType === 'invite-only'; + data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; + res.render('users', data.templateData); + }); }); } diff --git a/src/socket.io/user.js b/src/socket.io/user.js index fcaaa51d30..3f2f72c6f4 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -267,14 +267,39 @@ SocketUser.loadMore = function(socket, data, callback) { SocketUser.invite = function(socket, email, callback) { if (!email || !socket.uid) { - return callback(new Error('[[error:invald-data]]')); + return callback(new Error('[[error:invalid-data]]')); } if (meta.config.registrationType !== 'invite-only') { return callback(new Error('[[error:forum-not-invite-only]]')); } - user.sendInvitationEmail(socket.uid, email, callback); + var max = meta.config.maximumInvites; + + if (max) { + async.waterfall([ + function(next) { + user.getInvitesNumber(socket.uid, next); + }, + function(invites, next) { + user.isAdministrator(socket.uid, function(err, admin) { + next(err, invites, admin); + }); + }, + function(invites, admin, next) { + console.log(admin, invites, max); + if (!admin && invites > max) { + return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]')); + } + next(); + }, + function(next) { + user.sendInvitationEmail(socket.uid, email, next); + } + ], callback); + } else { + user.sendInvitationEmail(socket.uid, email, callback); + } }; diff --git a/src/user/invite.js b/src/user/invite.js index 0fb8ae979b..ef3146b5f4 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -16,13 +16,68 @@ var async = require('async'), module.exports = function(User) { + User.getInvites = function(uid, callback) { + db.getSetMembers('invitation:uid:' + uid, callback); + }; + + User.getInvitesNumber = function(uid, callback) { + db.setCount('invitation:uid:' + uid, callback); + }; + + User.getInvitingUsers = function(callback) { + db.getSetMembers('invitation:uids', callback); + }; + + User.getAllInvites = function(callback) { + var uids; + async.waterfall([ + User.getInvitingUsers, + function(_uids, next) { + uids = _uids; + async.map(uids, User.getInvites, next); + }, + function(invitations, next) { + invitations = invitations.map(function(invites, index) { + return { + uid: uids[index], + invitations: invites + }; + }); + next(null, invitations); + } + ], callback); + }; + User.sendInvitationEmail = function(uid, email, callback) { callback = callback || function() {}; + var token = utils.generateUUID(); - var registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + email; + var registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + encodeURIComponent(email); var oneDay = 86400000; + async.waterfall([ + function(next) { + User.getUidByEmail(email, next); + }, + function(exists, next) { + if (exists) { + return next(new Error('[[error:email-taken]]')); + } + next(); + }, + function(next) { + async.parallel([ + function(next) { + db.setAdd('invitation:uid:' + uid, email, next); + }, + function(next) { + db.setAdd('invitation:uids', uid, next); + } + ], function(err) { + next(err); + }); + }, function(next) { db.set('invitation:email:' + email, token, next); }, @@ -73,4 +128,4 @@ module.exports = function(User) { db.delete('invitation:email:' + email, callback); }; -}; \ No newline at end of file +}; diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl index faa31d7535..278ba279b1 100644 --- a/src/views/admin/manage/registration.tpl +++ b/src/views/admin/manage/registration.tpl @@ -1,4 +1,14 @@ -
+
+
+ Queue +
+ +

+ There are no users in the registration queue.
+ To enable this feature, go to Settings -> User -> Authentication and set + Registration Type to "Admin Approval". +

+ @@ -7,11 +17,6 @@ - -

- There are no users in the registration queue. To enable this feature go to Settings -> User -> Authentication and set Registration Type to "Admin Approval". -

-
NameTime
@@ -50,4 +55,31 @@
-
\ No newline at end of file +
+ +
+
+ Invitations +
+

+ Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username. +

+ The username will be displayed to the right of the emails for users who have redeemed their invitations. +

+ + + + + + + + + + + + + + + +
Inviter UsernameInvitee EmailInvitee Username (if registered)
{invites.username}{invites.invitations.email}{invites.invitations.username}
+
diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 0b27b24175..6f6b83f78d 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -36,6 +36,15 @@ + +
+ + +

+ 0 for no restriction. Admins get infinite invitations
+ Only applicable for "Invite Only" +

+
@@ -267,4 +276,4 @@ - \ No newline at end of file + From af8e649246ab3d28456840a34be43eadf5f87712 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 27 Nov 2015 17:00:36 -0700 Subject: [PATCH 02/59] Fix indentation in translations --- public/language/en@pirate/error.json | 2 +- public/language/en_US/error.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/en@pirate/error.json b/public/language/en@pirate/error.json index 88c55e7a49..0933126bc8 100644 --- a/public/language/en@pirate/error.json +++ b/public/language/en@pirate/error.json @@ -89,5 +89,5 @@ "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)." + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2)." } diff --git a/public/language/en_US/error.json b/public/language/en_US/error.json index 15c7ee3d72..00f7ed9236 100644 --- a/public/language/en_US/error.json +++ b/public/language/en_US/error.json @@ -89,5 +89,5 @@ "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)." + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2)." } From 67905667a9e8c6f94bc0fb89087dbe65914518cc Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sat, 28 Nov 2015 15:33:17 -0700 Subject: [PATCH 03/59] Added admin-only invites --- src/controllers/index.js | 2 +- src/controllers/users.js | 12 +++---- src/socket.io/user.js | 53 +++++++++++++++++-------------- src/views/admin/settings/user.tpl | 1 + 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/controllers/index.js b/src/controllers/index.js index f418c16572..e4261730f0 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -108,7 +108,7 @@ Controllers.register = function(req, res, next) { async.waterfall([ function(next) { - if (registrationType === 'invite-only') { + if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { user.verifyInvitation(req.query, next); } else { next(); diff --git a/src/controllers/users.js b/src/controllers/users.js index 21d1490717..7207150d0d 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -155,9 +155,11 @@ function render(req, res, data, next) { return next(err); } - if (!req.uid) { - return next(new Error('[[error:no-privileges]]')); - } + var registrationType = meta.config.registrationType; + + data.templateData.maximumInvites = meta.config.maximumInvites; + data.templateData.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; user.getInvitesNumber(req.uid, function(err, num) { if (err) { @@ -165,11 +167,9 @@ function render(req, res, data, next) { } data.templateData.invites = num; - data.templateData.maximumInvites = meta.config.maximumInvites; - data.templateData.inviteOnly = meta.config.registrationType === 'invite-only'; - data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; res.render('users', data.templateData); }); + }); } diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 3f2f72c6f4..14b0c2c828 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -270,36 +270,41 @@ SocketUser.invite = function(socket, email, callback) { return callback(new Error('[[error:invalid-data]]')); } - if (meta.config.registrationType !== 'invite-only') { + var registrationType = meta.config.registrationType + + if (registrationType !== 'invite-only' && registrationType !== 'admin-invite-only') { return callback(new Error('[[error:forum-not-invite-only]]')); } var max = meta.config.maximumInvites; - if (max) { - async.waterfall([ - function(next) { - user.getInvitesNumber(socket.uid, next); - }, - function(invites, next) { - user.isAdministrator(socket.uid, function(err, admin) { - next(err, invites, admin); - }); - }, - function(invites, admin, next) { - console.log(admin, invites, max); - if (!admin && invites > max) { - return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]')); + user.isAdministrator(socket.uid, function(err, admin) { + if (err) { + return callback(err); + } + if (registrationType === 'admin-invite-only' && !admin) { + return callback(new Error('[[error:no-privileges]]')); + } + if (max) { + async.waterfall([ + function(next) { + user.getInvitesNumber(socket.uid, next); + }, + function(invites, next) { + if (!admin && invites > max) { + return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]')); + } + next(); + }, + function(next) { + user.sendInvitationEmail(socket.uid, email, next); } - next(); - }, - function(next) { - user.sendInvitationEmail(socket.uid, email, next); - } - ], callback); - } else { - user.sendInvitationEmail(socket.uid, email, callback); - } + ], callback); + } else { + user.sendInvitationEmail(socket.uid, email, callback); + } + }); + }; diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 6f6b83f78d..bd0e746c91 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -33,6 +33,7 @@ + From 259eb585d683957fd62e59d937d16811ceda2d24 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sat, 28 Nov 2015 15:38:17 -0700 Subject: [PATCH 04/59] Added template data for admin invite only --- src/controllers/users.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/users.js b/src/controllers/users.js index 7207150d0d..189a98df5c 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -159,6 +159,7 @@ function render(req, res, data, next) { 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) { From dcbf53bae357444464b3976d3ecba143a15434cd Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 10:57:13 +0200 Subject: [PATCH 05/59] moved search to dbsearch --- package.json | 1 - src/database/mongo.js | 23 ++++-------- src/database/mongo/main.js | 77 -------------------------------------- src/database/redis.js | 11 +----- src/database/redis/main.js | 27 ------------- 5 files changed, 8 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index 0609a3dc22..fee26f6053 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "passport": "^0.3.0", "passport-local": "1.0.0", "prompt": "^0.2.14", - "redisearch": "^0.0.6", "request": "^2.44.0", "rimraf": "~2.4.2", "rss": "^1.0.0", diff --git a/src/database/mongo.js b/src/database/mongo.js index 0563e02633..5282024f6d 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -124,7 +124,7 @@ require('./mongo/sorted')(db, module); require('./mongo/list')(db, module); - if(nconf.get('mongo:password') && nconf.get('mongo:username')) { + if (nconf.get('mongo:password') && nconf.get('mongo:username')) { db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) { if (err) { winston.error(err.stack); @@ -138,31 +138,22 @@ } function createIndices() { - winston.info('[database] Checking database indices.') + winston.info('[database] Checking database indices.'); async.parallel([ 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}), - - async.apply(createIndex, 'searchtopic', {content: 'text', uid: 1, cid: 1}, {background: true}), - async.apply(createIndex, 'searchtopic', {id: 1}, {background: true}), - - async.apply(createIndex, 'searchpost', {content: 'text', uid: 1, cid: 1}, {background: true}), - async.apply(createIndex, 'searchpost', {id: 1}, {background: true}) + async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true}) ], function(err) { - callback(err); - }); - } - - function createIndex(collection, index, options, callback) { - db.collection(collection).ensureIndex(index, options, function(err) { if (err) { winston.error('Error creating index ' + err.message); } callback(err); }); } + + function createIndex(collection, index, options, callback) { + db.collection(collection).ensureIndex(index, options, callback); + } }); }; diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index f09f13f173..777565df06 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -5,83 +5,6 @@ var winston = require('winston'); module.exports = function(db, module) { var helpers = module.helpers.mongo; - module.searchIndex = function(key, data, id, callback) { - callback = callback || function() {}; - id = parseInt(id, 10); - if (!id) { - return callback(); - } - var setData = { - id: id - }; - for(var field in data) { - if (data.hasOwnProperty(field) && data[field]) { - setData[field] = data[field].toString(); - } - } - - db.collection('search' + key).update({id: id}, {$set: setData}, {upsert:true, w: 1}, function(err) { - if (err) { - winston.error('Error indexing ' + err.message); - } - callback(err); - }); - }; - - module.search = function(key, data, limit, callback) { - var searchQuery = {}; - - if (data.content) { - searchQuery.$text = {$search: data.content}; - } - - if (Array.isArray(data.cid) && data.cid.length) { - data.cid = data.cid.filter(Boolean); - if (data.cid.length > 1) { - searchQuery.cid = {$in: data.cid.map(String)}; - } else if (data.cid[0]) { - searchQuery.cid = data.cid[0].toString(); - } - } - - if (Array.isArray(data.uid) && data.uid.length) { - data.uid = data.uid.filter(Boolean); - if (data.uid.length > 1) { - searchQuery.uid = {$in: data.uid.map(String)}; - } else if (data.uid[0]) { - searchQuery.uid = data.uid[0].toString(); - } - } - - db.collection('search' + key).find(searchQuery, {limit: limit, fields:{_id: 0, id: 1}}).toArray(function(err, results) { - if (err) { - return callback(err); - } - - if (!results || !results.length) { - return callback(null, []); - } - - var data = results.map(function(item) { - return item.id; - }); - - callback(null, data); - }); - }; - - module.searchRemove = function(key, id, callback) { - callback = callback || helpers.noop; - id = parseInt(id, 10); - if (!id) { - return callback(); - } - - db.collection('search' + key).remove({id: id}, function(err, res) { - callback(err); - }); - }; - module.flushdb = function(callback) { callback = callback || helpers.noop; db.dropDatabase(callback); diff --git a/src/database/redis.js b/src/database/redis.js index da9c49aaa6..239090aa95 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -4,16 +4,11 @@ var winston = require('winston'), nconf = require('nconf'), - path = require('path'), semver = require('semver'), session = require('express-session'), - utils = require('./../../public/src/utils.js'), redis, connectRedis, - redisSearch, - redisClient, - postSearch, - topicSearch; + redisClient; module.questions = [ { @@ -43,7 +38,6 @@ try { redis = require('redis'); connectRedis = require('connect-redis')(session); - redisSearch = require('redisearch'); } catch (err) { winston.error('Unable to initialize Redis! Is Redis installed? Error :' + err.message); process.exit(); @@ -58,9 +52,6 @@ ttl: 60 * 60 * 24 * 14 }); - module.postSearch = redisSearch.createSearch('nodebbpostsearch', redisClient); - module.topicSearch = redisSearch.createSearch('nodebbtopicsearch', redisClient); - require('./redis/main')(redisClient, module); require('./redis/hash')(redisClient, module); require('./redis/sets')(redisClient, module); diff --git a/src/database/redis/main.js b/src/database/redis/main.js index 503d515569..7fc2890d87 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -1,31 +1,6 @@ "use strict"; module.exports = function(redisClient, module) { - module.searchIndex = function(key, data, id, callback) { - var method = key === 'post' ? module.postSearch : module.topicSearch; - - method.index(data, id, function(err, res) { - callback(err); - }); - }; - - module.search = function(key, data, limit, callback) { - var method = key === 'post' ? module.postSearch : module.topicSearch; - - method.query(data, 0, limit - 1, callback); - }; - - module.searchRemove = function(key, id, callback) { - callback = callback || function() {}; - if (!id) { - return callback(); - } - var method = key === 'post' ? module.postSearch : module.topicSearch; - - method.remove(id, function(err, res) { - callback(err); - }); - }; module.flushdb = function(callback) { redisClient.send_command('flushdb', [], function(err) { @@ -35,8 +10,6 @@ module.exports = function(redisClient, module) { }); }; - - module.exists = function(key, callback) { redisClient.exists(key, function(err, exists) { callback(err, exists === 1); From fff5def99b416f57b030fd77bc98db9d2b6a24a0 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 11:00:50 +0200 Subject: [PATCH 06/59] up dbsearch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fee26f6053..86cb942c62 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "morgan": "^1.3.2", "nconf": "~0.8.2", "nodebb-plugin-composer-default": "1.0.24", - "nodebb-plugin-dbsearch": "0.2.18", + "nodebb-plugin-dbsearch": "0.3.0", "nodebb-plugin-emoji-extended": "0.4.17", "nodebb-plugin-markdown": "4.0.8", "nodebb-plugin-mentions": "1.0.12", From 60e186c8f36a6723000f4e0fdb06c69f62161bcb Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 11:07:37 +0200 Subject: [PATCH 07/59] refactor api/username api/email --- src/controllers/api.js | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/controllers/api.js b/src/controllers/api.js index 74c4d008c8..79cf9dd6d4 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -179,15 +179,9 @@ apiController.getObject = function(req, res, next) { apiController.getUserByUID = function(req, res, next) { var uid = req.params.uid ? req.params.uid : 0; - getUserByUID(uid, function(err, userData) { - if (err || !userData) { - return next(err); - } - res.json(userData); - }); + getUserByUID(uid, res, next); }; - apiController.getUserByUsername = function(req, res, next) { var username = req.params.username ? req.params.username : 0; @@ -196,17 +190,11 @@ apiController.getUserByUsername = function(req, res, next) { user.getUidByUsername(username, next); }, function(uid, next) { - getUserByUID(uid, next); + getUserByUID(uid, res, next); } - ], function(err, userData) { - if (err || !userData) { - return next(err); - } - res.json(userData); - }); + ], next); }; - apiController.getUserByEmail = function(req, res, next) { var email = req.params.email ? req.params.email : 0; @@ -215,30 +203,24 @@ apiController.getUserByEmail = function(req, res, next) { user.getUidByEmail(email, next); }, function(uid, next) { - getUserByUID(uid, next); + getUserByUID(uid, res, next); } - ], function(err, userData) { - if (err || !userData) { - return next(err); - } - res.json(userData); - }); + ], next); }; - -function getUserByUID(uid, callback) { +function getUserByUID(uid, res, next) { async.parallel({ userData: async.apply(user.getUserData, uid), settings: async.apply(user.getSettings, uid) }, function(err, results) { if (err || !results.userData) { - return callback(err, null); + return next(err); } results.userData.email = results.settings.showemail ? results.userData.email : undefined; results.userData.fullname = results.settings.showfullname ? results.userData.fullname : undefined; - callback(null, results.userData); + res.json(results.userData); }); } From 8dc947504165a3b217b2b5af6046c3f75505d985 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 14:57:47 +0200 Subject: [PATCH 08/59] closes #3941 --- src/controllers/accounts/edit.js | 1 + src/socket.io/user/profile.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index a6b2f7ad55..497564d88a 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -19,6 +19,7 @@ editController.get = function(req, res, callback) { return callback(err); } + userData['username:disableEdit'] = !userData.isAdmin && parseInt(meta.config['username:disableEdit'], 10) === 1; userData.title = '[[pages:account/edit, ' + userData.username + ']]'; userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]); diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index a245c6e29d..5a365d6ff2 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -117,12 +117,17 @@ module.exports = function(SocketUser) { return next(new Error('[[error:invalid-data]]')); } - if (parseInt(meta.config['username:disableEdit'], 10) === 1) { + user.isAdministrator(socket.uid, next); + }, + function(isAdmin, next) { + if (!isAdmin && socket.uid !== parseInt(data.uid, 10)) { + return next(new Error('[[error:no-privileges]]')); + } + + if (!isAdmin && parseInt(meta.config['username:disableEdit'], 10) === 1) { data.username = oldUserData.username; } - user.isAdminOrSelf(socket.uid, data.uid, next); - }, - function (next) { + user.updateProfile(data.uid, data, next); }, function (userData, next) { From 42cce3771b32354119c3c02c0953ae4e4e9c6fb3 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 15:17:05 +0200 Subject: [PATCH 09/59] closes #3846 --- src/user/admin.js | 48 +++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/user/admin.js b/src/user/admin.js index 95075b88f1..004e048c93 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -1,9 +1,9 @@ 'use strict'; -var async = require('async'), - db = require('./../database'); - +var async = require('async'); +var db = require('../database'); +var plugins = require('../plugins'); module.exports = function(User) { @@ -17,7 +17,7 @@ module.exports = function(User) { User.getIPs = function(uid, stop, callback) { db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function(err, ips) { - if(err) { + if (err) { return callback(err); } @@ -31,17 +31,17 @@ module.exports = function(User) { var csvContent = ''; async.waterfall([ - function(next) { + function (next) { db.getSortedSetRangeWithScores('username:uid', 0, -1, next); }, - function(users, next) { + function (users, next) { var uids = users.map(function(user) { return user.score; }); User.getUsersFields(uids, ['uid', 'email', 'username'], next); }, - function(usersData, next) { - usersData.forEach(function(user, index) { + function (usersData, next) { + usersData.forEach(function(user) { if (user) { csvContent += user.email + ',' + user.username + ',' + user.uid + '\n'; } @@ -53,22 +53,34 @@ module.exports = function(User) { }; User.ban = function(uid, callback) { - User.setUserField(uid, 'banned', 1, function(err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + User.setUserField(uid, 'banned', 1, next); + }, + function (next) { + db.sortedSetAdd('users:banned', Date.now(), uid, next); + }, + function (next) { + plugins.fireHook('action:user.banned', {uid: uid}); + next(); } - db.sortedSetAdd('users:banned', Date.now(), uid, callback); - }); + ], callback); }; User.unban = function(uid, callback) { db.delete('uid:' + uid + ':flagged_by'); - User.setUserField(uid, 'banned', 0, function(err) { - if (err) { - return callback(err); + 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(); } - db.sortedSetRemove('users:banned', uid, callback); - }); + ], callback); }; User.resetFlags = function(uids, callback) { From d12e8e1116a7ec935254a2b4b1b9559a282a1bb5 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 11 Dec 2015 15:24:49 +0200 Subject: [PATCH 10/59] closes #3814 --- src/controllers/accounts/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 77a71aba66..9a679cd49a 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -59,7 +59,7 @@ helpers.getUserDataByUserSlug = function(userslug, callerUID, callback) { userData.joindateISO = utils.toISOString(userData.joindate); userData.lastonlineISO = utils.toISOString(userData.lastonline || userData.joindate); - userData.age = userData.birthday ? Math.floor((new Date().getTime() - new Date(userData.birthday).getTime()) / 31536000000) : ''; + userData.age = Math.max(0, userData.birthday ? Math.floor((new Date().getTime() - new Date(userData.birthday).getTime()) / 31536000000) : 0); if (!(isAdmin || self || (userData.email && userSettings.showemail))) { userData.email = ''; From 2d203d7dcafe6eeb9ae2e22145ab829cd880d210 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Dec 2015 12:07:02 -0500 Subject: [PATCH 11/59] Squashed commit of the following: Closes #2668 commit 3d4f494ed3257bceda8f6f82057cab83f0f252b3 Author: Julian Lam Date: Fri Dec 11 12:06:42 2015 -0500 theme minvers for #2668 commit b608ce61854f8195143685bb9753b80d32b26e95 Author: Julian Lam Date: Fri Dec 11 12:01:03 2015 -0500 Allowing chat modal to edit and delete messages re: #2668 commit 0104db90a4070582f3938b6929dae35f985bac35 Author: Julian Lam Date: Fri Dec 11 11:51:23 2015 -0500 Fixed issue where newSet calculations were off ... sometimes. Also, rendering of edited messages now parses a template partial, instead of just replacing the content. commit 5cb6ca600425ca9320c599b32306e93dcc5aa4ce Author: Julian Lam Date: Fri Dec 11 11:07:12 2015 -0500 If edited content matches existing content... ... then edit is aborted. commit 6e7495247b1895589c716db29f919a934087b924 Author: Julian Lam Date: Fri Dec 11 11:05:08 2015 -0500 some linting and fixed issue where new msgs when deleted would crash server commit db4a9e40d6dff44569c2437378121db8fdf75cf8 Author: Julian Lam Date: Tue Dec 8 17:25:56 2015 -0500 Message deletion for #2668, and fixed bug Fixed bug where chat modal would spawn even though user was sitting on the /chats page. commit a5aa2498ab4a8bba02a6daa43a9dbed7b3e37976 Author: Julian Lam Date: Tue Dec 8 14:55:23 2015 -0500 wiring up the edit button, #2668 commit 5f2afdcf6f2b9eae6b5873ca100149e65e3d385d Author: Julian Lam Date: Tue Dec 8 14:20:39 2015 -0500 added indicator to show if and when a message had been edited commit e8301132d525c1b9fd46c98cdb282ac7ea7a0d7f Author: Julian Lam Date: Tue Dec 8 14:06:39 2015 -0500 Allowing editing of chat messages commit bfd991be1cb1769599f7d5d2b1638e313c3c2dcb Author: Julian Lam Date: Tue Dec 8 10:33:49 2015 -0500 Added messageId to messages object return commit 0306ee6657b3288dd4547c66869d7d4ece0b31ad Author: Julian Lam Date: Tue Dec 8 08:20:17 2015 -0500 WIP #2668 --- package.json | 4 +- public/language/en_GB/error.json | 1 + public/language/en_GB/modules.json | 1 + public/src/client/chats.js | 115 ++++++++++++++++--- public/src/modules/chat.js | 15 +++ public/src/modules/components.js | 7 ++ src/messaging.js | 176 ++++++++++++++++++++++++++--- src/socket.io/modules.js | 34 ++++++ 8 files changed, 322 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 86cb942c62..da7883cacc 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "nodebb-plugin-spam-be-gone": "0.4.5", "nodebb-rewards-essentials": "0.0.6", "nodebb-theme-lavender": "3.0.2", - "nodebb-theme-persona": "4.0.40", - "nodebb-theme-vanilla": "5.0.15", + "nodebb-theme-persona": "4.0.41", + "nodebb-theme-vanilla": "5.0.16", "nodebb-widget-essentials": "2.0.5", "nodemailer": "0.7.1", "npm": "^2.1.4", diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 4159d211d8..4e00dd3470 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -101,6 +101,7 @@ "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", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json index 6bb97b68a4..5dc56fa8c2 100644 --- a/public/language/en_GB/modules.json +++ b/public/language/en_GB/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 Days", "chat.thirty_days": "30 Days", "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 437c97978c..0531c40de9 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -63,6 +63,17 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }); }); + 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); + }) + .on('click', '[data-action="delete"]', function() { + var messageId = $(this).parents('[data-mid]').attr('data-mid'); + Chats.delete(messageId); + }); + $('.recent-chats').on('scroll', function() { var $this = $(this); var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; @@ -95,6 +106,49 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', $('[component="chat/input"]').focus(); }); + 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); + } + }); + }; + + Chats.prepEdit = function(inputEl, messageId) { + socket.emit('modules.chats.getRaw', { mid: messageId }, function(err, raw) { + // 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) { + translator.translate('[[modules:chat.delete_message_confirm]]', function(translated) { + bootbox.confirm(translated, function(ok) { + if (ok) { + socket.emit('modules.chats.delete', { + messageId: messageId + }, function(err) { + if (err) { + return app.alertError(err.message); + } + + // Remove message from list + components.get('chat/message', messageId).slideUp('slow', function() { + $(this).remove(); + }); + }); + } + }); + }); }; Chats.addSinceHandler = function(toUid, chatContentEl, sinceEl) { @@ -253,6 +307,22 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', socket.on('event:user_status_change', function(data) { app.updateUserStatus($('.chats-list [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); }); + + socket.on('event:chats.edit', function(data) { + var message; + + data.messages.forEach(function(message) { + templates.parse('partials/chat_message', { + messages: message + }, function(html) { + 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() { @@ -278,7 +348,9 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }; Chats.sendMessage = function(toUid, inputEl) { - var msg = inputEl.val(); + var msg = inputEl.val(), + mid = inputEl.attr('data-mid'); + if (msg.length > config.maximumChatMessageLength) { return app.alertError('[[error:chat-message-too-long]]'); } @@ -288,20 +360,35 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', } inputEl.val(''); - socket.emit('modules.chats.send', { - touid: toUid, - message: msg - }, function(err) { - if (err) { - if (err.message === '[[error:email-not-confirmed-chat]]') { - return app.showEmailConfirmWarning(err); - } - return app.alertError(err.message); - } + inputEl.removeAttr('data-mid'); - sounds.play('chat-outgoing'); - Chats.notifyTyping(toUid, false); - }); + if (!mid) { + socket.emit('modules.chats.send', { + touid: toUid, + 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'); + Chats.notifyTyping(toUid, false); + }); + } else { + socket.emit('modules.chats.edit', { + mid: mid, + message: msg + }, function(err) { + if (err) { + return app.alertError(err.message); + } + + Chats.notifyTyping(toUid, false); + }); + } }; Chats.scrollToBottom = function(containerEl) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index a9363836cd..27d7b9548e 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -19,6 +19,10 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra }); socket.on('event:chats.receive', function(data) { + if (ajaxify.currentPage.match(/^chats/)) { + return; + } + var username = data.message.fromUser.username; var isSelf = parseInt(data.message.fromUser.uid, 10) === parseInt(app.user.uid, 10); data.message.self = data.self; @@ -252,6 +256,17 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra } }); + 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); + }) + .on('click', '[data-action="delete"]', function() { + var messageId = $(this).parents('[data-mid]').attr('data-mid'); + Chats.delete(messageId); + }); + Chats.addSinceHandler(chatModal.attr('touid'), chatModal.find('.chat-content'), chatModal.find('[data-since]')); Chats.addSendHandlers(chatModal.attr('touid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); diff --git a/public/src/modules/components.js b/public/src/modules/components.js index da03bacd16..f4de069e01 100644 --- a/public/src/modules/components.js +++ b/public/src/modules/components.js @@ -35,6 +35,13 @@ define('components', function() { 'categories/category': function(name, value) { return $('[component="categories/category"][data-' + name + '="' + value + '"]'); + }, + + 'chat/message': function(messageId) { + return $('[component="chat/message"][data-mid="' + messageId + '"]'); + }, + 'chat/message/body': function(messageId) { + return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]'); } }; diff --git a/src/messaging.js b/src/messaging.js index e0a39eac59..7d3a89316e 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -106,6 +106,81 @@ var db = require('./database'), }); }; + Messaging.editMessage = function(mid, content, callback) { + async.series([ + function(next) { + // Verify that the message actually changed + Messaging.getMessageField(mid, 'content', function(err, raw) { + if (raw === content) { + // No dice. + return callback(); + } + + next(); + }); + }, + async.apply(Messaging.setMessageFields, mid, { + content: content, + edited: Date.now() + }), + function(next) { + Messaging.getMessageFields(mid, ['fromuid', 'touid'], function(err, data) { + getMessages([mid], data.fromuid, data.touid, true, function(err, messages) { + sockets.in('uid_' + data.fromuid).emit('event:chats.edit', { + messages: messages + }); + sockets.in('uid_' + data.touid).emit('event:chats.edit', { + messages: messages + }); + next(); + }); + }); + } + ], callback); + }; + + Messaging.deleteMessage = function(mid, callback) { + var uids = []; + async.series([ + function(next) { + db.getObject('message:' + mid, function(err, messageObj) { + messageObj.fromuid = parseInt(messageObj.fromuid, 10); + messageObj.touid = parseInt(messageObj.touid, 10); + uids.push(messageObj.fromuid, messageObj.touid); + uids.sort(function(a, b) { + return a > b ? 1 : -1; + }); + next(); + }); + }, + function(next) { + next(); + }, + function(next) { + db.sortedSetRemove('messages:uid:' + uids[0] + ':to:' + uids[1], mid, next); + }, + async.apply(db.delete, 'message:' + mid) + ], callback); + }; + + Messaging.getMessageField = function(mid, field, callback) { + Messaging.getMessageFields(mid, [field], function(err, fields) { + callback(err, fields[field]); + }); + }; + + Messaging.getMessageFields = function(mid, fields, callback) { + db.getObjectFields('message:' + mid, fields, callback); + }; + + Messaging.setMessageField = function(mid, field, content, callback) { + db.setObjectField('message:' + mid, field, content, callback); + }; + + Messaging.setMessageFields = function(mid, data, callback) { + db.setObject('message:' + mid, data, callback); + }; + Messaging.getMessages = function(params, callback) { var fromuid = params.fromuid, touid = params.touid, @@ -160,7 +235,12 @@ var db = require('./database'), async.waterfall([ async.apply(db.getObjects, keys), function(messages, next) { - messages = messages.filter(Boolean); + messages = messages.map(function(msg, idx) { + if (msg) { + msg.messageId = parseInt(mids[idx], 10); + } + return msg; + }).filter(Boolean); async.map(messages, function(message, next) { var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10); message.fromUser = self ? userData[0] : userData[1]; @@ -169,6 +249,10 @@ var db = require('./database'), message.self = self ? 1 : 0; message.newSet = false; + if (message.hasOwnProperty('edited')) { + message.editedISO = new Date(parseInt(message.edited, 10)).toISOString(); + } + Messaging.parse(message.content, message.fromuid, fromuid, userData[1], userData[0], isNew, function(result) { message.content = result; message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s; @@ -177,21 +261,50 @@ var db = require('./database'), }, next); }, function(messages, next) { - // Add a spacer in between messages with time gaps between them - 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 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) { - // If the previous message was from the other person, this is also a new set - message.newSet = true; - } + if (messages.length > 1) { + // Add a spacer in between messages with time gaps between them + 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 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) { + // If the previous message was from the other person, this is also a new set + message.newSet = true; + } - return message; - }); + return message; + }); - next(undefined, messages); + next(undefined, messages); + } else { + // For single messages, we don't know the context, so look up the previous message and compare + var uids = [fromuid, touid].sort(function(a, b) { return a > b ? 1 : -1 }); + var key = 'messages:uid:' + uids[0] + ':to:' + uids[1]; + async.waterfall([ + async.apply(db.sortedSetRank, key, messages[0].messageId), + function(index, next) { + db.getSortedSetRange(key, index-1, index-1, next); + }, + function(mid, next) { + Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next); + } + ], function(err, fields) { + if (err) { + return next(err); + } + + if ( + (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 + messages[0].newSet = true; + } + + next(undefined, messages); + }); + } } ], callback); }); @@ -277,7 +390,7 @@ var db = require('./database'), count: 1, markRead: false }, function(err, teaser) { - var teaser = teaser[0]; + teaser = teaser[0]; teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s; next(err, teaser); }); @@ -415,6 +528,39 @@ var db = require('./database'), ], callback); }; + Messaging.canEdit = function(messageId, uid, callback) { + if (parseInt(meta.config.disableChat) === 1) { + return callback(null, false); + } + + async.waterfall([ + function (next) { + user.getUserFields(uid, ['banned', 'email:confirmed'], next); + }, + function (userData, next) { + if (parseInt(userData.banned, 10) === 1) { + return callback(null, false); + } + + if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) { + return callback(null, false); + } + + Messaging.getMessageField(messageId, 'fromuid', next); + }, + function(fromUid, next) { + if (parseInt(fromUid, 10) === parseInt(uid, 10)) { + return callback(null, true); + } + + user.isAdministrator(uid, next); + }, + function(isAdmin, next) { + next(null, isAdmin); + } + ], callback); + }; + function sendNotifications(fromuid, touid, messageObj, callback) { user.isOnline(touid, function(err, isOnline) { if (err || isOnline) { diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 08b8a4a26a..7b6ed82778 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -29,6 +29,14 @@ SocketModules.chats.get = function(socket, data, callback) { }, callback); }; +SocketModules.chats.getRaw = function(socket, data, callback) { + if(!data || !data.hasOwnProperty('mid')) { + return callback(new Error('[[error:invalid-data]]')); + } + + Messaging.getMessageField(data.mid, 'content', callback); +}; + SocketModules.chats.send = function(socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); @@ -62,6 +70,32 @@ SocketModules.chats.send = function(socket, data, callback) { }); }; +SocketModules.chats.edit = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + Messaging.canEdit(data.mid, socket.uid, function(err, allowed) { + if (allowed) { + Messaging.editMessage(data.mid, data.message, callback); + } else { + return callback(new Error('[[error:cant-edit-chat-message]]')); + } + }); +}; + +SocketModules.chats.delete = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) { + if (allowed) { + Messaging.deleteMessage(data.messageId, callback); + } + }); +} + SocketModules.chats.canMessage = function(socket, toUid, callback) { Messaging.canMessage(socket.uid, toUid, function(err, allowed) { callback(!allowed ? new Error('[[error:chat-restricted]]') : undefined); From 3b6b36cfa6417fc0bc2aeb284a8b7e2d368b1102 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Dec 2015 19:50:21 -0500 Subject: [PATCH 12/59] fixing possible crash --- src/messaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messaging.js b/src/messaging.js index 7d3a89316e..81758f43b7 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -390,7 +390,7 @@ var db = require('./database'), count: 1, markRead: false }, function(err, teaser) { - teaser = teaser[0]; + var teaser = teaser[0]; teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s; next(err, teaser); }); From bcbc8608a4664d1c83181ed03e513306919e6f0f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 11 Dec 2015 20:04:27 -0500 Subject: [PATCH 13/59] newSet calculations bugfix --- src/messaging.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/messaging.js b/src/messaging.js index 81758f43b7..bff12313ba 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -284,7 +284,13 @@ var db = require('./database'), async.waterfall([ async.apply(db.sortedSetRank, key, messages[0].messageId), function(index, next) { - db.getSortedSetRange(key, index-1, index-1, next); + // Continue only if this isn't the first message in sorted set + if (index > 0) { + db.getSortedSetRange(key, index-1, index-1, next); + } else { + messages[0].newSet = true; + return next(undefined, messages); + } }, function(mid, next) { Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next); @@ -390,7 +396,7 @@ var db = require('./database'), count: 1, markRead: false }, function(err, teaser) { - var teaser = teaser[0]; + teaser = teaser[0]; teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s; next(err, teaser); }); From b4c8301596ea7a97992c168d58a078ffdea732d2 Mon Sep 17 00:00:00 2001 From: Aziz Khoury Date: Sat, 12 Dec 2015 13:07:51 -0500 Subject: [PATCH 14/59] [minor] meta tag, content-type shouldnt be escaped so `text/html; charset=UTF-8` and not `text/html; charset=UTF-8` --- src/meta/tags.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/meta/tags.js b/src/meta/tags.js index 55e5bd32d2..8495b827a6 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -17,7 +17,8 @@ module.exports = function(Meta) { content: 'width=device-width, initial-scale=1.0, user-scalable=no' }, { name: 'content-type', - content: 'text/html; charset=UTF-8' + content: 'text/html; charset=UTF-8', + noEscape: true }, { name: 'apple-mobile-web-app-capable', content: 'yes' @@ -124,4 +125,4 @@ module.exports = function(Meta) { }); } } -}; \ No newline at end of file +}; From 518552de648bc131e53c295dcb9fc970f874c616 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sun, 13 Dec 2015 17:07:36 +0200 Subject: [PATCH 15/59] dont calculate pid index on new replies --- src/topics/create.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/topics/create.js b/src/topics/create.js index 828214df28..af576b9d03 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -251,11 +251,6 @@ module.exports = function(Topics) { Topics.follow(postData.tid, uid); } - posts.getPidIndex(postData.pid, postData.tid, settings.topicPostSort, next); - }, - function(postIndex, next) { - postData.index = postIndex; - if (parseInt(uid, 10)) { Topics.notifyFollowers(postData, uid); user.setUserField(uid, 'lastonline', Date.now()); @@ -297,6 +292,7 @@ module.exports = function(Topics) { function (results, next) { postData.user = results.userInfo[0]; postData.topic = results.topicInfo; + postData.index = parseInt(results.topicInfo.postcount, 10) - 1; // Username override for guests, if enabled if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postData.uid, 10) === 0 && data.handle) { From 24a302ea1acdec84101e3694aa358b82e67e4d9b Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sun, 13 Dec 2015 18:06:14 +0200 Subject: [PATCH 16/59] closes #3947 --- src/socket.io/topics/move.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index 42c82f6b8f..80fc2e3772 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -55,7 +55,7 @@ module.exports = function(SocketTopics) { async.waterfall([ function (next) { - privileges.categories.canMoveAllTopics(data.currentCid, data.cid, data.uid, next); + privileges.categories.canMoveAllTopics(data.currentCid, data.cid, socket.uid, next); }, function (canMove, next) { if (!canMove) { From 1cf2ad339a7a56268d37d765034502c0c791f38d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 09:28:15 -0500 Subject: [PATCH 17/59] latest translations and fallbacks --- public/language/ar/error.json | 1 + public/language/ar/modules.json | 1 + public/language/bg/error.json | 1 + public/language/bg/modules.json | 3 +- public/language/bg/topic.json | 12 ++++---- public/language/bg/user.json | 4 +-- public/language/bn/error.json | 1 + public/language/bn/modules.json | 1 + public/language/cs/error.json | 1 + public/language/cs/modules.json | 1 + public/language/da/error.json | 1 + public/language/da/modules.json | 1 + public/language/de/error.json | 1 + public/language/de/modules.json | 1 + public/language/de/pages.json | 2 +- public/language/el/error.json | 1 + public/language/el/modules.json | 1 + public/language/en@pirate/error.json | 7 ++--- public/language/en@pirate/modules.json | 1 + public/language/en_US/error.json | 7 ++--- public/language/en_US/modules.json | 1 + public/language/es/error.json | 1 + public/language/es/modules.json | 1 + public/language/et/error.json | 1 + public/language/et/modules.json | 1 + public/language/fa_IR/category.json | 2 +- public/language/fa_IR/error.json | 39 ++++++++++++------------ public/language/fa_IR/global.json | 8 ++--- public/language/fa_IR/groups.json | 6 ++-- public/language/fa_IR/login.json | 2 +- public/language/fa_IR/modules.json | 13 ++++---- public/language/fa_IR/notifications.json | 6 ++-- public/language/fa_IR/pages.json | 22 ++++++------- public/language/fa_IR/topic.json | 18 +++++------ public/language/fa_IR/user.json | 16 +++++----- public/language/fi/error.json | 1 + public/language/fi/modules.json | 1 + public/language/fr/error.json | 1 + public/language/fr/modules.json | 1 + public/language/gl/error.json | 1 + public/language/gl/modules.json | 1 + public/language/he/error.json | 1 + public/language/he/modules.json | 1 + public/language/hu/error.json | 1 + public/language/hu/modules.json | 1 + public/language/id/error.json | 1 + public/language/id/modules.json | 1 + public/language/it/error.json | 1 + public/language/it/modules.json | 1 + public/language/ja/error.json | 1 + public/language/ja/modules.json | 1 + public/language/ko/error.json | 1 + public/language/ko/modules.json | 1 + public/language/lt/error.json | 1 + public/language/lt/modules.json | 1 + public/language/ms/error.json | 1 + public/language/ms/modules.json | 1 + public/language/nb/error.json | 1 + public/language/nb/modules.json | 1 + public/language/nl/error.json | 1 + public/language/nl/modules.json | 1 + public/language/pl/error.json | 1 + public/language/pl/modules.json | 1 + public/language/pt_BR/error.json | 1 + public/language/pt_BR/modules.json | 1 + public/language/ro/error.json | 1 + public/language/ro/modules.json | 1 + public/language/ru/error.json | 1 + public/language/ru/modules.json | 1 + public/language/rw/error.json | 1 + public/language/rw/modules.json | 1 + public/language/sc/error.json | 1 + public/language/sc/modules.json | 1 + public/language/sk/error.json | 1 + public/language/sk/modules.json | 1 + public/language/sl/error.json | 1 + public/language/sl/modules.json | 1 + public/language/sr/error.json | 1 + public/language/sr/modules.json | 1 + public/language/sv/error.json | 1 + public/language/sv/modules.json | 1 + public/language/th/error.json | 1 + public/language/th/modules.json | 1 + public/language/tr/error.json | 1 + public/language/tr/modules.json | 1 + public/language/vi/error.json | 1 + public/language/vi/modules.json | 1 + public/language/zh_CN/error.json | 1 + public/language/zh_CN/modules.json | 1 + public/language/zh_TW/error.json | 1 + public/language/zh_TW/modules.json | 1 + 91 files changed, 159 insertions(+), 83 deletions(-) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 7bb7290cec..32e348c4f9 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -78,6 +78,7 @@ "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", "reputation-system-disabled": "نظام السمعة معطل", "downvoting-disabled": "التصويتات السلبية معطلة", "not-enough-reputation-to-downvote": "ليس لديك سمعة تكفي لإضافة صوت سلبي لهذا الموضوع", diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index 17998d13f9..34c2c43e43 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -15,6 +15,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?", "composer.compose": "اكتب", "composer.show_preview": "عرض المعاينة", "composer.hide_preview": "إخفاء المعاينة", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index cbc97af490..92f321a474 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -78,6 +78,7 @@ "too-many-messages": "Изпратили сте твърде много съобщения. Моля, изчакайте малко.", "invalid-chat-message": "Невалидно съобщение", "chat-message-too-long": "Съобщението е твърде дълго", + "cant-edit-chat-message": "Нямате право да редактирате това съобщение", "reputation-system-disabled": "Системата за репутация е изключена.", "downvoting-disabled": "Отрицателното гласуване е изключено", "not-enough-reputation-to-downvote": "Нямате достатъчно репутация, за да гласувате отрицателно за тази публикация", diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index cb4509c571..884db0222a 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -15,12 +15,13 @@ "chat.seven_days": "7 дни", "chat.thirty_days": "30 дни", "chat.three_months": "3 месеца", + "chat.delete_message_confirm": "Сигурен/а ли сте, че искате да изтриете това съобщение?", "composer.compose": "Писане", "composer.show_preview": "Показване на прегледа", "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", diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index 217b68c211..f698f949dd 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -48,15 +48,15 @@ "thread_tools.move_all": "Преместване на всички", "thread_tools.fork": "Разделяне на темата", "thread_tools.delete": "Изтриване на темата", - "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": "Преместване", diff --git a/public/language/bg/user.json b/public/language/bg/user.json index c6f82b78d8..6b2a9a87b9 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -10,8 +10,8 @@ "ban_account_confirm": "Наистина ли искате да блокирате този потребител?", "unban_account": "Отблокиране на акаунта", "delete_account": "Изтриване на акаунта", - "delete_account_confirm": "Сигурни ли сте, че искате да изтриете акаунта си?
Това действие е необратимо и няма да можете да възстановите нищо от данните си

Въведете потребителското си име, за да потвърдите, че искате да унищожите този акаунт.", - "delete_this_account_confirm": "Сигурни ли сте, че искате да изтриете този акаунт?
Това действие е необратимо и няма да можете да възстановите нищо от данните

", + "delete_account_confirm": "Сигурен/а ли сте, че искате да изтриете акаунта си?
Това действие е необратимо и няма да можете да възстановите нищо от данните си

Въведете потребителското си име, за да потвърдите, че искате да унищожите този акаунт.", + "delete_this_account_confirm": "Сигурен/а ли сте, че искате да изтриете този акаунт?
Това действие е необратимо и няма да можете да възстановите нищо от данните

", "account-deleted": "Акаунтът е изтрит", "fullname": "Цяло име", "website": "Уеб сайт", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index a8273c1320..7905e5e766 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -78,6 +78,7 @@ "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", "reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে", "downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।", "not-enough-reputation-to-downvote": "আপনার এই পোস্ট downvote করার জন্য পর্যাপ্ত সম্মাননা নেই", diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json index dc26f2fd17..27052c6ef9 100644 --- a/public/language/bn/modules.json +++ b/public/language/bn/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "৭ দিন", "chat.thirty_days": "৩০ দিন", "chat.three_months": "৩ মাস", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 3efb6790f4..79ff4e89f2 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -78,6 +78,7 @@ "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", "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", diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index cd4fe64059..d3687e90bb 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -15,6 +15,7 @@ "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?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", diff --git a/public/language/da/error.json b/public/language/da/error.json index fb02400438..86deb28910 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -78,6 +78,7 @@ "too-many-messages": "Du har sendt for mange beskeder, vent venligt lidt.", "invalid-chat-message": "Ugyldig chat besked", "chat-message-too-long": "Chat beskeden er for lang", + "cant-edit-chat-message": "You are not allowed to edit this message", "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", diff --git a/public/language/da/modules.json b/public/language/da/modules.json index a15c48fffd..6f4af1099c 100644 --- a/public/language/da/modules.json +++ b/public/language/da/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 dage", "chat.thirty_days": "30 dage", "chat.three_months": "3 måneder", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Skriv", "composer.show_preview": "Vis forhåndsvisning", "composer.hide_preview": "Fjern forhåndsvisning", diff --git a/public/language/de/error.json b/public/language/de/error.json index e4918e8800..ae24d940a3 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -78,6 +78,7 @@ "too-many-messages": "Du hast zu viele Nachrichten versandt, bitte warte eine Weile.", "invalid-chat-message": "Ungültige Nachricht", "chat-message-too-long": "Die Nachricht ist zu lang", + "cant-edit-chat-message": "Du darfst diese Nachricht nicht ändern", "reputation-system-disabled": "Das Reputationssystem ist deaktiviert.", "downvoting-disabled": "Downvotes sind deaktiviert.", "not-enough-reputation-to-downvote": "Deine Reputation ist zu niedrig, um diesen Beitrag negativ zu bewerten.", diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 4c0a2058d1..3f4a175a72 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 Tage", "chat.thirty_days": "30 Tage", "chat.three_months": "3 Monate", + "chat.delete_message_confirm": "Bist du sicher, dass du diese Nachricht löschen möchtest?", "composer.compose": "Verfassen", "composer.show_preview": "Vorschau zeigen", "composer.hide_preview": "Vorschau ausblenden", diff --git a/public/language/de/pages.json b/public/language/de/pages.json index 6eda35c198..a914d22356 100644 --- a/public/language/de/pages.json +++ b/public/language/de/pages.json @@ -35,7 +35,7 @@ "account/favourites": "Von %1 favorisierte Beiträge", "account/settings": "Benutzer-Einstellungen", "account/watched": "Themen angeschaut von %1", - "maintenance.text": "%1 befindet sich derzeit in der Wartung. Bitte komm später wieder.", + "maintenance.text": "%1 befindet sich derzeit in der Wartung. Bitte komme später wieder.", "maintenance.messageIntro": "Zusätzlich hat der Administrator diese Nachricht hinterlassen:", "throttled.text": "%1 ist momentan aufgrund von Überlastung nicht verfügbar. Bitte komm später wieder." } \ No newline at end of file diff --git a/public/language/el/error.json b/public/language/el/error.json index 2b18e271de..2b88dc7741 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -78,6 +78,7 @@ "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", "reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.", "downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί", "not-enough-reputation-to-downvote": "Δεν έχεις αρκετή φήμη για να καταψηφίσεις αυτή την δημοσίευση", diff --git a/public/language/el/modules.json b/public/language/el/modules.json index a47af0e58a..f5ebd8bf9d 100644 --- a/public/language/el/modules.json +++ b/public/language/el/modules.json @@ -15,6 +15,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?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", diff --git a/public/language/en@pirate/error.json b/public/language/en@pirate/error.json index 0933126bc8..f942fa3536 100644 --- a/public/language/en@pirate/error.json +++ b/public/language/en@pirate/error.json @@ -78,6 +78,7 @@ "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", "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", @@ -87,7 +88,5 @@ "registration-error": "Registration Error", "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-username": "Please use your username to login" +} \ No newline at end of file diff --git a/public/language/en@pirate/modules.json b/public/language/en@pirate/modules.json index 7d2111c556..6a44ff18d9 100644 --- a/public/language/en@pirate/modules.json +++ b/public/language/en@pirate/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 Days", "chat.thirty_days": "30 Days", "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", diff --git a/public/language/en_US/error.json b/public/language/en_US/error.json index 00f7ed9236..64f954a4cd 100644 --- a/public/language/en_US/error.json +++ b/public/language/en_US/error.json @@ -78,6 +78,7 @@ "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", "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", @@ -87,7 +88,5 @@ "registration-error": "Registration Error", "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-username": "Please use your username to login" +} \ No newline at end of file diff --git a/public/language/en_US/modules.json b/public/language/en_US/modules.json index b089365c68..f38f3f7ed5 100644 --- a/public/language/en_US/modules.json +++ b/public/language/en_US/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 Days", "chat.thirty_days": "30 Days", "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Compose", "composer.show_preview": "Show Preview", "composer.hide_preview": "Hide Preview", diff --git a/public/language/es/error.json b/public/language/es/error.json index 6b3cf3948b..35705567f6 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -78,6 +78,7 @@ "too-many-messages": "Has enviado demasiados mensajes, por favor espera un poco.", "invalid-chat-message": "Mensaje de Chat inválido", "chat-message-too-long": "Mensaje de Chat es demasiado largo", + "cant-edit-chat-message": "You are not allowed to edit this message", "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", diff --git a/public/language/es/modules.json b/public/language/es/modules.json index 6badc0c562..f16b1e3e1b 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 días", "chat.thirty_days": "30 días", "chat.three_months": "3 meses", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Crear", "composer.show_preview": "Ver Previsualización", "composer.hide_preview": "Ocultar Previsualización", diff --git a/public/language/et/error.json b/public/language/et/error.json index 666098ff89..5f4fc05e44 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -78,6 +78,7 @@ "too-many-messages": "Oled saatnud liiga palju sõnumeid, oota natukene.", "invalid-chat-message": "Vigane vestluse sõnum", "chat-message-too-long": "Vestluse sõnum on liiga pikk", + "cant-edit-chat-message": "You are not allowed to edit this message", "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.", diff --git a/public/language/et/modules.json b/public/language/et/modules.json index 99ab787405..691063085f 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -15,6 +15,7 @@ "chat.seven_days": "7 Päeva", "chat.thirty_days": "30 Päeva", "chat.three_months": "3 Kuud", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "composer.compose": "Koosta", "composer.show_preview": "Kuva eelvaadet", "composer.hide_preview": "Peida eelvaade", diff --git a/public/language/fa_IR/category.json b/public/language/fa_IR/category.json index a17a584fd5..a75615859f 100644 --- a/public/language/fa_IR/category.json +++ b/public/language/fa_IR/category.json @@ -12,5 +12,5 @@ "ignore": "نادیده گرفتن", "watch.message": "در حال حاظر شما به روز رسانی های این شاخه را دنبال می کنید", "ignore.message": "در حال حاظر شما به روز رسانی های این شاخه را نادیده میگیرد", - "watched-categories": "Watched categories" + "watched-categories": "دسته های دیده شده" } \ No newline at end of file diff --git a/public/language/fa_IR/error.json b/public/language/fa_IR/error.json index be76d173ad..dc30bcb82d 100644 --- a/public/language/fa_IR/error.json +++ b/public/language/fa_IR/error.json @@ -39,30 +39,30 @@ "still-uploading": "خواهشمندیم تا پایان بارگذاری‌ها شکیبا باشید.", "content-too-short": "خواهشمندیم دیدگاه بلندتری بنویسید. دیدگاه‌ها دست‌کم باید 1% نویسه داشته باشند.", "content-too-long": "لطفا طول مطلب را کوتاه تر کنید. طول پست نمیتواند بیشتر از %1 کاراکتر باشد.", - "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)", - "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "title-too-short": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل 1% کاراکتر داشته باشد.", + "title-too-long": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل 1% کاراکتر داشته باشد.", + "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% برچسب داشته باشند", + "file-too-big": "حداکثر مجاز حجم فایل 1% کیلوبایت می باشد - لطفا فایلی با حجم کمتر بارگزاری کنید", "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", + "cant-remove-last-admin": "شما تنها مدیر می باشید . شما باید قبل از عزل خود از مدیریت یک کاربر دیگر را مدیر کنید", "invalid-image-type": "نوع تصویر نامعتبر است. نوعهای قابل قبول اینها هستند: %1", "invalid-image-extension": "پسوند عکس نامعتبر است", "invalid-file-type": "نوع پرونده نامعتبر است. نوعهای قابل قبول اینها هستند: %1", "group-name-too-short": "نام گروه خیلی کوتاه است.", "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": "جستار پیش‌تر حذف شده است", @@ -71,18 +71,19 @@ "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": "این کاربر پیام های گفتگوی خود را محدود کرده است . آنها بایدشما را دنبال کنند تا اینکه شما بتوانید به آنها پیامی بفرستید", "too-many-messages": "شما پیامهای خیلی زیادی فرستاده اید، لطفا مدتی صبر نمایید", - "invalid-chat-message": "Invalid chat message", - "chat-message-too-long": "Chat message is too long", + "invalid-chat-message": "پیغام نامعتبر", + "chat-message-too-long": "پیغام طولانی تر از حد مجاز است", + "cant-edit-chat-message": "شما اجازه ی ویرایش این پیغام را ندارید", "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": "NodeBB در هنگام بارگذاری مجدد با یک مشکل مواجه شده است: \"%1\". NodeBB سرویس رسانی به کلاینت های سرویس گیرنده را ادامه خواهد داد، اگرچه شما کاری را قبل از بارگیری مجدد انجام دادید بازگردانی کنید", "registration-error": "خطای ثبت نام", "parse-error": "هنگام تجزیه پاسخ سرور اشتباهی پیش امد", diff --git a/public/language/fa_IR/global.json b/public/language/fa_IR/global.json index 76959934cd..6289311c41 100644 --- a/public/language/fa_IR/global.json +++ b/public/language/fa_IR/global.json @@ -56,8 +56,8 @@ "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", "posted_in_ago_by_guest": "ارسال شده در %1 %2 توسط مهمان", @@ -70,7 +70,7 @@ "recentposts": "دیدگاه‌های تازه", "recentips": "آخرین IPها", "away": "دور از دسترس", - "dnd": "Do not disturb", + "dnd": "مزاحم نشوید", "invisible": "مخفی", "offline": "آفلاین", "email": "رایانامه", @@ -83,5 +83,5 @@ "follow": "دنبال کن", "unfollow": "دنبال نکن", "delete_all": "حذف همه", - "map": "Map" + "map": "نقشه" } \ No newline at end of file diff --git a/public/language/fa_IR/groups.json b/public/language/fa_IR/groups.json index a997c32629..b378005c8d 100644 --- a/public/language/fa_IR/groups.json +++ b/public/language/fa_IR/groups.json @@ -12,9 +12,9 @@ "invited.none": "در حال حاضر هیچ کسی دعوت نشده است", "invited.uninvite": "لغو دعوت", "invited.search": "جستجو به دنبال کاربرانی به جهت دعوت به این گروه", - "invited.notification_title": "You have been invited to join %1", - "request.notification_title": "Group Membership Request from %1", - "request.notification_text": "%1 has requested to become a member of %2", + "invited.notification_title": "شما برای پیوستن به %1 دعوت شده اید", + "request.notification_title": "درخواست عضویت در گروه از طرف %1", + "request.notification_text": "%1 درخواست عضویت در %2 را دارد", "cover-save": "ذخیره", "cover-saving": "در حال ذخیره کردن", "details.title": "جزئیات گروه", diff --git a/public/language/fa_IR/login.json b/public/language/fa_IR/login.json index 7a7c99fd6b..578071109d 100644 --- a/public/language/fa_IR/login.json +++ b/public/language/fa_IR/login.json @@ -1,7 +1,7 @@ { "username-email": "نام کاربری / رایانامه", "username": "نام کاربری", - "email": "رایانامه", + "email": "ایمیل", "remember_me": "مرا به یاد بسپار؟", "forgot_password": "گذرواژه را فراموش کرده‌اید؟", "alternative_logins": "روش‌های درون آمدن جایگزین", diff --git a/public/language/fa_IR/modules.json b/public/language/fa_IR/modules.json index 6548730c12..f49a077dd6 100644 --- a/public/language/fa_IR/modules.json +++ b/public/language/fa_IR/modules.json @@ -5,7 +5,7 @@ "chat.no_active": "شما هیچ گفتگوی فعالی ندارید.", "chat.user_typing": "%1 در حال نوشتن است...", "chat.user_has_messaged_you": "%1 به شما پیام داده است.", - "chat.see_all": "See all chats", + "chat.see_all": "دیدن همه ی چت ها", "chat.no-messages": "مشخص کنید تاریخچه گفتگوهایتان با چه کاربری را می‌خواهید ببینید", "chat.recent-chats": "گفتگوهای اخیر", "chat.contacts": "تماس‌ها", @@ -15,6 +15,7 @@ "chat.seven_days": "7 روز", "chat.thirty_days": "30 روز", "chat.three_months": "3 ماه", + "chat.delete_message_confirm": "آیا مطمئن هستید که می خواهید این پیغام را حذف کنید؟", "composer.compose": "ارسال", "composer.show_preview": "نمایش پیش‌نمایش", "composer.hide_preview": "مخفی کردن پیش‌نمایش", @@ -23,11 +24,11 @@ "composer.discard": "آیا از دور انداختن این دیدگاه اطمینان دارید؟", "composer.submit_and_lock": "ارسال و قفل", "composer.toggle_dropdown": "باز و بسته کردن کرکره", - "composer.uploading": "Uploading %1", + "composer.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" + "bootbox.confirm": "تایید", + "cover.dragging_title": "تنظیم مکان عکس کاور", + "cover.dragging_message": "عکس کاور با کلیک موس گرفته و در مکان دلخواه رها کنید و بر روی \"ذخیره\" کلیک کنید", + "cover.saved": "عکس کاور و مکان آن ذخیره شد" } \ No newline at end of file diff --git a/public/language/fa_IR/notifications.json b/public/language/fa_IR/notifications.json index 5d4098ccbe..276db73036 100644 --- a/public/language/fa_IR/notifications.json +++ b/public/language/fa_IR/notifications.json @@ -1,7 +1,7 @@ { "title": "آگاه‌سازی‌ها", "no_notifs": "هیچ آگاه‌سازی تازه‌ای ندارید", - "see_all": "See all notifications", + "see_all": "دیدن همه ی اطلاعیه ها", "mark_all_read": "همه اطلاعیه ها را خوانده شده علامت بزن", "back_to_home": "بازگشت به %1", "outgoing_link": "پیوند برون‌رو", @@ -12,8 +12,8 @@ "you_have_unread_notifications": "شما آگاه‌سازی‌های نخوانده دارید.", "new_message_from": "پیام تازه از %1", "upvoted_your_post_in": "%1 امتیاز مثبت به دیدگاه شما در %2 داده", - "moved_your_post": "%1 has moved your post to %2", - "moved_your_topic": "%1 has moved %2", + "moved_your_post": "%1 پست شما را به %2 انتقال داده است", + "moved_your_topic": "%2 %1 را منتقل کرده است", "favourited_your_post_in": "%1 دیدگاه شما را در %2 برگزیده کرده.", "user_flagged_post_in": "%1 دیدگاه شما را در %2 علامتدار کرده", "user_posted_to": "پاسخ دادن به %2 از سوی %1", diff --git a/public/language/fa_IR/pages.json b/public/language/fa_IR/pages.json index 6a25342754..d48dcf06c2 100644 --- a/public/language/fa_IR/pages.json +++ b/public/language/fa_IR/pages.json @@ -1,16 +1,16 @@ { "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": "جستارهای تازه", "users/online": "کاربران آنلاین", "users/latest": "آخرین کاربران", "users/sort-posts": "کاربران با بیش‌ترین دیدگاه", - "users/sort-reputation": "Users with the most reputation", - "users/map": "User Map", + "users/sort-reputation": "کاربران دارای بیشترین اعتبار", + "users/map": "نقشه ی کاربر", "users/search": "جستجوی کاربر", "notifications": "آگاه‌سازی‌ها", "tags": "برچسب‌ها", @@ -24,9 +24,9 @@ "chats": "گفتگوها", "chat": "گفتگو با %1", "account/edit": "ویرایش \"%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": "ویرایش کلمه ی عبورِ \"1%\"", + "account/edit/username": "ویرایش نام کاربریِ \"1%\"", + "account/edit/email": "ویرایش ایمیلِ \"1%\"", "account/following": "کاربرانی که %1 دنبال می‌کند", "account/followers": "کاربرانی که %1 را دنبال می‌کنند", "account/posts": "دیدگاه‌های %1", @@ -34,8 +34,8 @@ "account/groups": "گروه‌های %1", "account/favourites": "دیدگاه‌های مورد پسند %1", "account/settings": "تنظیمات کاربر", - "account/watched": "Topics watched by %1", + "account/watched": "موضوع های دیده شده توسط \"1%\"", "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/fa_IR/topic.json b/public/language/fa_IR/topic.json index aea16ab1ab..211a360e84 100644 --- a/public/language/fa_IR/topic.json +++ b/public/language/fa_IR/topic.json @@ -25,7 +25,7 @@ "tools": "ابزارها", "flag": "پرچم", "locked": "قفل شده است", - "bookmark_instructions": "Click here to return to the last unread post in this thread.", + "bookmark_instructions": "برای بازگشت به آخرین پست خوانده نشده در این دسته اینجا کلیک کنید", "flag_title": "پرچم‌گذاری این جستار برای بررسی ناظران", "flag_success": "این جستار برای بررسی ناظران پرچم گذاشته شد.", "deleted_message": "این جستار پاک شده است. تنها کاربرانِ با حق مدیریت جستار می‌توانند آن را ببینند.", @@ -96,12 +96,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.create": "Create a new topic", - "stale.reply_anyway": "Reply to this topic anyway", - "stale.link_back": "Re: [%1](%2)", - "spam": "Spam", - "offensive": "Offensive", - "custom-flag-reason": "Enter a flagging reason" + "stale.title": "آیا مایلید به جای آن یک موضوع جدید ایجاد کنید؟", + "stale.warning": "موضوعی که شما در حال پاسخگویی به آن هستید قدیمی می باشد. آیا میلید به جای آن یک موضوع جدید ایجاد کنید و در آن به این موضوع ارجاع دهید؟", + "stale.create": "ایجاد یک موضوع جدید", + "stale.reply_anyway": "در هر صورت می خواهم به این موضوع پاسخ دهم", + "stale.link_back": "پاسخ : [1%](2%)", + "spam": "اسپم", + "offensive": "توهین آمیز", + "custom-flag-reason": "وارد کردن دلیل پرچمگذاری" } \ No newline at end of file diff --git a/public/language/fa_IR/user.json b/public/language/fa_IR/user.json index e0249abb3c..f808164656 100644 --- a/public/language/fa_IR/user.json +++ b/public/language/fa_IR/user.json @@ -12,7 +12,7 @@ "delete_account": "حذف حساب کاربری", "delete_account_confirm": "آیا مطمئنید که میخواهید حساب کاربری خود را حذف کنید؟
این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات خود را بازیابی کنید./strong>

برای تایید حذف این حساب کاربری، نام کاربری خود را وارد کنید", "delete_this_account_confirm": " آیا مطمئنید که میخواهید این حساب کاربری را حذف کنید؟
این عمل غیر قابل بازگشت است و شما قادر نخواهید بود هیچ کدام از اطلاعات را بازیابی کنید.

", - "account-deleted": "Account deleted", + "account-deleted": "حساب کاربری پاک شد", "fullname": "نام و نام خانوادگی", "website": "تارنما", "location": "محل سکونت", @@ -30,16 +30,16 @@ "signature": "امضا", "birthday": "روز تولد", "chat": "گفتگو", - "chat_with": "Chat with %1", + "chat_with": "چت کردن با 1%", "follow": "دنبال کن", "unfollow": "دنبال نکن", "more": "بیشتر", "profile_update_success": "نمایه باموفقیت به روز شده است!", "change_picture": "تغییر تصویر", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "تغییر نام کاربری", + "change_email": "تغییر ایمیل", "edit": "ویرایش", - "default_picture": "Default Icon", + "default_picture": "آیکون پیش فرض", "uploaded_picture": "تصویر بارشده", "upload_new_picture": "بارگذاری تصویر تازه", "upload_new_picture_from_url": "بارگذاری تصویر جدید از نشانی وب", @@ -54,11 +54,11 @@ "confirm_password": "تکرار گذرواژه", "password": "گذرواژه", "username_taken_workaround": "نام کاربری درخواستی شما در حال حاضر گرفته شده است، بنابراین ما آن را کمی تغییر داده‌ایم. شما هم‌اکنون با نام %1 Date: Mon, 14 Dec 2015 11:31:49 -0500 Subject: [PATCH 18/59] updated modal to not show parentheses when max file size is not defined... Also fixing hideAlerts error in ACP when opening the upload modal. --- public/src/admin/settings.js | 2 -- public/src/modules/uploader.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index d6c9ec3407..b80df5450d 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -113,8 +113,6 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { uploader.open(uploadBtn.attr('data-route'), {}, 0, function(image) { $('#' + uploadBtn.attr('data-target')).val(image); }); - - uploader.hideAlerts(); }); }); } diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index e951f0aabe..05bf9379b6 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -21,7 +21,7 @@ define('uploader', ['csrf'], function(csrf) { if (fileSize) { uploadForm.find('#file-size-block') - .translateText('[[uploads:maximum-file-size, ' + fileSize + ']]') + .translateText('([[uploads:maximum-file-size, ' + fileSize + ']])') .removeClass('hide'); } else { uploadForm.find('#file-size-block').addClass('hide'); @@ -102,7 +102,7 @@ define('uploader', ['csrf'], function(csrf) { } module.hideAlerts = function(modal) { - modal.find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide'); + $(modal).find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide'); }; return module; From c1460d36f0067b737c31948f973914ee13d72e51 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 12:00:24 -0500 Subject: [PATCH 19/59] Deprecated uploader.open, use uploader.show instead ... as it now takes a single Object argument instead of multiple parameters. Also, closes #3942. Help text is just omitted now. --- public/src/admin/settings.js | 7 +++++- public/src/modules/uploader.js | 32 ++++++++++++++++------------ src/views/admin/settings/general.tpl | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index b80df5450d..117857d5ec 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -110,7 +110,12 @@ define('admin/settings', ['uploader', 'sounds'], function(uploader, sounds) { $('#content input[data-action="upload"]').each(function() { var uploadBtn = $(this); uploadBtn.on('click', function() { - uploader.open(uploadBtn.attr('data-route'), {}, 0, function(image) { + uploader.show({ + route: uploadBtn.attr('data-route'), + params: {}, + fileSize: 0, + showHelp: uploadBtn.attr('data-help') ? uploadBtn.attr('data-help') === 1 : undefined + }, function(image) { $('#' + uploadBtn.attr('data-target')).val(image); }); }); diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index 05bf9379b6..d15d5ddda0 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -2,12 +2,24 @@ /* globals define, templates, translator */ -define('uploader', ['csrf'], function(csrf) { +define('uploader', ['csrf', 'translator'], function(csrf, translator) { var module = {}; module.open = function(route, params, fileSize, callback) { - parseModal(function(uploadModal) { + 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, + params: params, + fileSize: fileSize + }, callback); + }; + + module.show = function(data, callback) { + parseModal({ + showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true, + fileSize: data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false + }, function(uploadModal) { uploadModal = $(uploadModal); uploadModal.modal('show'); @@ -16,16 +28,8 @@ define('uploader', ['csrf'], function(csrf) { }); var uploadForm = uploadModal.find('#uploadForm'); - uploadForm.attr('action', route); - uploadForm.find('#params').val(JSON.stringify(params)); - - if (fileSize) { - uploadForm.find('#file-size-block') - .translateText('([[uploads:maximum-file-size, ' + fileSize + ']])') - .removeClass('hide'); - } else { - uploadForm.find('#file-size-block').addClass('hide'); - } + uploadForm.attr('action', data.route); + uploadForm.find('#params').val(JSON.stringify(data.params)); uploadModal.find('#pictureUploadSubmitBtn').off('click').on('click', function() { uploadForm.submit(); @@ -84,8 +88,8 @@ define('uploader', ['csrf'], function(csrf) { }); }; - function parseModal(callback) { - templates.parse('partials/modals/upload_picture_modal', {}, function(html) { + function parseModal(tplVals, callback) { + templates.parse('partials/modals/upload_picture_modal', tplVals, function(html) { translator.translate(html, callback); }); } diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index e45f1093eb..147657195e 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -73,7 +73,7 @@
- +
From 46571ccd77cf40e4faee44f26ac2647c6f97d1a7 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 13:39:53 -0500 Subject: [PATCH 20/59] closes #3930 --- public/src/client/chats.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 0531c40de9..9f0103333e 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -27,6 +27,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.scrollToBottom($('.expanded-chat ul')); Chats.initialised = true; + + if (ajaxify.data.hasOwnProperty('meta') && ajaxify.data.meta.hasOwnProperty('uid')) { + // This is an active chat, focus on the input box + components.get('chat/input').focus(); + } }; Chats.getRecipientUid = function() { @@ -93,8 +98,6 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', if (prev.length) { Chats.switchChat(parseInt(prev.attr('data-uid'), 10), prev.attr('data-username')); } - - $('[component="chat/input"]').focus(); }); Mousetrap.bind('ctrl+down', function() { var activeContact = $('.chats-list .bg-primary'), @@ -103,8 +106,6 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', if (next.length) { Chats.switchChat(parseInt(next.attr('data-uid'), 10), next.attr('data-username')); } - - $('[component="chat/input"]').focus(); }); Mousetrap.bind('up', function(e) { if (e.target === components.get('chat/input').get(0)) { @@ -195,11 +196,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', var contactEl = $('.chats-list [data-uid="' + uid + '"]'); Chats.loadChatSince(uid, $('.chat-content'), 'recent'); - Chats.addSendHandlers(uid, $('[component="chat/input"]'), $('[data-action="send"]')); + Chats.addSendHandlers(uid, components.get('chat/input'), $('[data-action="send"]')); contactEl.addClass('bg-primary').siblings().removeClass('bg-primary'); - $('[component="chat/title"]').text(username); - $('[component="chat/messages"]').attr('data-uid', uid).attr('data-username', username); - $('[component="breadcrumb/current"]').text(username); + components.get('chat/title').text(username); + components.get('chat/messages').attr('data-uid', uid).attr('data-username', username); + components.get('breadcrumb/current').text(username); + components.get('chat/input').focus(); if (window.history && window.history.pushState) { var url = 'chats/' + utils.slugify(username); From 0b735fad17fb2eabec7d74764ec5e02d03ec9cc0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:07:18 -0500 Subject: [PATCH 21/59] fixed #3936 --- package.json | 4 ++-- public/src/client/chats.js | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index da7883cacc..35cadccc19 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "nodebb-plugin-spam-be-gone": "0.4.5", "nodebb-rewards-essentials": "0.0.6", "nodebb-theme-lavender": "3.0.2", - "nodebb-theme-persona": "4.0.41", - "nodebb-theme-vanilla": "5.0.16", + "nodebb-theme-persona": "4.0.42", + "nodebb-theme-vanilla": "5.0.17", "nodebb-widget-essentials": "2.0.5", "nodemailer": "0.7.1", "npm": "^2.1.4", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 9f0103333e..32f66f69d1 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -263,6 +263,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.addSocketListeners = function() { socket.on('event:chats.receive', function(data) { + console.log(data); if (Chats.isCurrentChat(data.withUid)) { newMessage = data.self === 0; data.message.self = data.self; @@ -270,18 +271,22 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.appendChatMessage($('.expanded-chat .chat-content'), data.message); } else { var contactEl = $('[component="chat/recent"] li[data-uid="' + data.withUid + '"]'), - userKey = data.withUid === data.message.fromuid ? 'fromUser' : 'toUser'; + userKey = parseInt(data.withUid, 10) === parseInt(data.message.fromuid, 10) ? 'fromUser' : 'toUser'; // Spawn a new contact if required - templates.parse('partials/chat_contact', { - uid: data.withUid, - username: data.message[userKey].username, - status: data.message[userKey].status, - picture: data.message[userKey].picture, - teaser: { - content: data.message.cleanedContent, - timestampISO: new Date(Date.now()).toISOString() - } + templates.parse('partials/chat_contacts', { + contacts: [{ + uid: data.message[userKey].uid, + username: data.message[userKey].username, + status: data.message[userKey].status, + picture: data.message[userKey].picture, + 'icon:text': data.message[userKey]['icon:text'], + 'icon:bgColor': data.message[userKey]['icon:bgColor'], + teaser: { + content: data.message.cleanedContent, + timestampISO: new Date(Date.now()).toISOString() + } + }] }, function(html) { translator.translate(html, function(translatedHTML) { if (contactEl.length) { From c04622506d82878c2b5df662d966ecf90da12c1b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:08:58 -0500 Subject: [PATCH 22/59] console.log removal --- public/src/client/chats.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 32f66f69d1..1a4cdb1056 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -263,7 +263,6 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.addSocketListeners = function() { socket.on('event:chats.receive', function(data) { - console.log(data); if (Chats.isCurrentChat(data.withUid)) { newMessage = data.self === 0; data.message.self = data.self; From a4d8ab9d120a187a4a9afb208994e924d7803655 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:13:18 -0500 Subject: [PATCH 23/59] fixes #3950 --- public/src/client/chats.js | 7 ++++++- src/socket.io/modules.js | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 1a4cdb1056..9dc1c9d8e0 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -197,7 +197,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.loadChatSince(uid, $('.chat-content'), 'recent'); Chats.addSendHandlers(uid, components.get('chat/input'), $('[data-action="send"]')); - contactEl.addClass('bg-primary').siblings().removeClass('bg-primary'); + + contactEl + .removeClass('unread') + .addClass('bg-primary') + .siblings().removeClass('bg-primary'); + components.get('chat/title').text(username); components.get('chat/messages').attr('data-uid', uid).attr('data-username', username); components.get('breadcrumb/current').text(username); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 7b6ed82778..42ac4092d2 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -27,6 +27,9 @@ SocketModules.chats.get = function(socket, data, callback) { since: data.since, isNew: false }, callback); + + // Mark chat as read + Messaging.markRead(socket.uid, data.touid); }; SocketModules.chats.getRaw = function(socket, data, callback) { From 7b49dc90615b5e8bba2e6d6fd97b8dab655b5c53 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:20:52 -0500 Subject: [PATCH 24/59] fixes #3935 --- public/src/client/chats.js | 2 +- public/src/modules/chat.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 9dc1c9d8e0..e026a0ac2c 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -189,7 +189,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', }; Chats.switchChat = function(uid, username) { - if (!$('[component="chat/messages"]').length) { + if (!$('#content [component="chat/messages"]').length) { return ajaxify.go('chats/' + utils.slugify(username)); } diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 27d7b9548e..14bf5982c5 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -19,10 +19,6 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra }); socket.on('event:chats.receive', function(data) { - if (ajaxify.currentPage.match(/^chats/)) { - return; - } - var username = data.message.fromUser.username; var isSelf = parseInt(data.message.fromUser.uid, 10) === parseInt(app.user.uid, 10); data.message.self = data.self; From cc4997aecaa8ab5bad18c4fa88563985f6c764c6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:28:27 -0500 Subject: [PATCH 25/59] fixes #3940 --- public/logo.png | Bin 3151 -> 7189 bytes src/middleware/middleware.js | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/logo.png b/public/logo.png index 5284e5983897789c0b2f74993da100abfa3dadc2..6a6c184126221ac0b44a53d08ef3fe012ed73b45 100644 GIT binary patch literal 7189 zcmeHM_fr!<(~gKrQ+g5Uy(3k6Cp0lMLApq4nZxnHWhMpP8O$#G&io2PU`s!K+003g#|B+iE<*Gq9iwuFJRMd5HPU>j>57I*OBpJEhmZ5fv4#AHIj|!-zRo!wr2qq|`{{Lj)8!+0-zk`+$UJ zarcY@HIyP@w9B{X3aBWH@lhcFZT_FP|7-Gp@{3W+;={dLf@@bpL;y9CC7WQn1KrQD zuwklfkyv#uPAxHZ!lgr>Bel}4Ey{#;P=b^E(*s?>fO?)oJvK@MyY#TiP-H<_pOev% zU>zw;Jh6)}W_#`6=jZAs`V-m#t;buipwpfZbCRQH`M1i5kMJtaL7#d19lbXPy(~on zxSCZEKAHn1cv=`*q&zBv#&}9=_icrQm$>7&UFY9bOQz#mH#;|Yw4+rHqqDomc;iCX zd2Kkg(6At1oeWgj`ybFIQS~0)rLfq5XequPo|BCGuRu_WE_F) z(cC3Zu|C1D=xdZKAfBQ_3Tmqww)t1wSD1$0uyZEB%$Sq7iW%s(eTP$#3^5hv5=zK~ z1;=3rMz!sE_L91whsVi9fW9)&XYYe_CAv1EMts|8AGo;Th-KAActK#{MW0uDLJ~uK zxez4!YSi)j=u@;qGhYf16@DB$=TTS@w<6|_H1_ud|rau`t7M5LX zuwWZP7R(NzF6~)m?A_r%TNXF?7wgB z%6p+VDUl(mZA_PK=rTL|!B6*Lq1CSf`+o+3*(y;238M!rjfy0(qzOA})zYQKZ<8xJ z;)X^8_Vr(Pb3GL8DzyRT{hXlH?}R!Np?Ss{X+J`fH1%;g*>x93?6_xk8S@Xcn?9!7 zNY|cs4m*xpEyN~$z&{9Yc^5iM`is}am|TN5VN%=jIwg1S{aCgDq`i&?rB~T8>@3H_54L zDqmcogr`^Fh3o8aC4u*Xg?9-83M0EpPr+UP69~JjZDAz*tGGF8evNUNe!6J7KZHH^ zklAFh#v}3XN3zAxlLn^f`{fO(JVt@ZRk2sUmiy#rAMW}EETjQZMFvjQbHICxoCkW1 z448;5dEjeuI%VMYk9FkmE8j2~ySE<352j+1PQS||q`vH>mxZUS7o9V~YSlYs1W+0# z5(`e^7(W$RwSE-dJ(1wb5S zR45BKDm-EW04ejk@u{v2l7o?*rP(7P7qegfvIy#6Zms5RhnM&J#7Qn`@+CKQ#1Lkl zP$!{!-ZPovz~lXtuaRrod7$1QMaq|Y@UqKSVA>_D46*vhEN|wL)$OK8&&3~=&5;YH z5pfvq;ivgA&!i)mOR+@nu7}#k-gKy89BFb0+YLK?FL*K)l(&knx;xaX8&sfYTol!5 z7Sx*TF?nLKEq7AAKZ?I+8y}+u8e6igF%o@Q{koN&C7i?1<{h}HiQ&?53*&(wXcHtl#{f0>mzc=82d3;Z5Bc+GB>iun?- zvQ^Lh0H6)hv;Rl;t=?|uT>`j;l{bb}dZNS9a^{PQ-WZX~!sz9T33dB>PU}y(m7exj zTvaX_a6t|$5U6XokY=aURG>xNJ8TG7%~8)GmUr-c2BNC_H76}I zLEmzfnxCk;A3RA>;!3}k(JlAEzD^!dktsZu!F}ZNTSV)gKogm^1yyMcSIopF-7uFe z;witzc#pAD*5HfVqSizBnkp$RWceD+!i2sYN0K6K?0K8H4n@L;Dof=_+XiV%=l1&9 zpK-WS!WT%k`_)fV8I-3x-RCu$>qzc5UB3HUU?<3z;!?SXg>5MP0Z>L9cyn*zRNW8poW3A}~A#<|(~oe zhcdojf%kaEonGAP(I}(0rERs+8p?i-8ucYb=$zVEH3(K@&=iafJAD5b!Tv+5i&CMo z?SdQ7(6Q-fR)QzfAQ^{jR=%E`$JA*AKwXcAF^ZuotVM-pe=XzeL*{Lf@(7>LSz~E2 z24|KD1z}H&=^Re$K(NENjN74l^=4dQA%_Xa(?mtlxIVd0pbXZO}iIVhkz-{41=G)($BqK@}S{%0Na#sZrW@Ng$mJZ`qm~J_v#!OwbrJ&)L z^9tLnGVU{>n-{sGx_2a(CSHRrq6vJ;&(Gq}@3?|PSl$yor_i3H()c?y^k_;kl={li z?@)r;j=o31NxG3&79Mf%@Cc;x{$*`6VcwJEqKW!}tnnt8_a6L{Fz-S1ejMVA-CFwz zHj|+v%WDT*UV_*U9G8pkjDJq7q4rZVJbibVqsc8FU)x4x>G>fdG-zongyP+rpgfYI zaeXeH>BpS)qQ=PlyEyjypI03PQ17Uw!NHE~^4udTJ> ziEx__GUjkuDI+M-eJon}C|!xSVhvgAXk4%pJkolQLIPAC@O}&PtjsKHJG_epUt3Si z-p@$%G;?uQZ*8wk7ytFV(;>$^3c>R|_vth#k-nwZIx6bPsHD#3AXZEAS{l*%Zf5?` z5bi2G7AA~mbylbPd$#?r>vGV76N$HZKKHae%v;ohp3Z#ls|o5EMW491g5~Y@%^V-F%gXoD{8@tZcz z!G&`h{-QMV`l92u*IBREp&rm;Hrf+>4OvuwO)URL$rFW%v;zZ3A?xDX!Wz5}?D#4| z&HEI%b6@7(LC79;EKJczSf7g6&D*|bZb!yTyd$Y|KfrPNZB;#gc8RQCdfytp0Kvoj zu~6D%8u@x+NP*)YjUKD=uJifhf#o4MDy1##!IE&X!e$fiXmX$G%hTV=Sumds#;BGZ8W zUUXjBzc&pX?+9*-rgXV9@v^&w&V}}64(p1d1t}F(S#n#pNWne{l17QG*>E<*IAs z5YpP|BC+SS1w4n~b;xnG7oHK(*Q4cgBwbZrA2c8&+LI%(`R%7aTx%Q7!fGT@#J^>5 z)tlSsF=^AKLv#Qad`#9op;3z5lg(AN3x57=Kw57h01ZDLL0gd*@T(>clJ=~@ zwgB40B?j$&8!5Gl5uI$!WItI`behP)^rLZB1MheYU60TpMxTm5p{2aW##lXYKYb$+ zA9ed*tx@Ge!L91{PbwS%FH`Qa8w;R-eCHK)kW0-R@N?n%`u44b7UB88X11^7&%|Q( ze5<6$s@M$r-wQ`0E- z)NXylq^%>B*vpHbhbwIBHYs()WlY0-${6RiEx@}@29Np8%HtaDY0HcPJJH~TG@c7e zC{Oq=6EX1(pa=7QP;)(p^}TYI=8H=`E|2Dn-%+zSP`X*c?h}+9Hgh|4Mz%X0M}-8X zEdHulTf~=u(Cas+U;RaYf?Od|m5R-dG1I3~bgbuYP{?%?b-YWfO@}Sdm_8plaXQVX z*|vm}9?!~N#OIk^zz3d)?sI;sKa3wIpter4qOt-QiENkF_{N%qJ!Apg&F~b}+JDrRQD z&leuKdD-;r67W*Aa!UQy!V(bha7#-znK?W;c#&n&5Ox6u@ z4-YpH#mGa|buet0Yk3`!Ho(kiNnRS_6>7XZZEr!VD%|1^aH`I5N(!y_>Tp)NK7mis ztAWo~hSb$g{9Q209%8Si?0{_nQp%}DfkXgD%os^!OqW+I7g3ehxJ!0`Ar^Z7-emcmgxVBA)*_-qT)7fwqWd zGv=V{4+|B>uQDOmg!Y;GGJYjXAISN#H2YQ4N;+uspB8&inh3Zw?olv;<0BTtH_Vj_XVo(qe9thC&OLNl-nf>8aEW(fR%!~#DL3LJ zIEgoc7c^>HVvHf>1g1Am)v1+j%WZ#2lkgJn^YLmbVQ};}!^br*lUq(0*|#hJj;cCG z1Fx&}oHgC2hfRL(zd5B1Ew6qbzDyXx?zFCZX*a{dXVkvzSESRnGAJ*Wjm>^B%7Q0u z51zjquLJJAN3lK48+ntZpLdmw+QjxQ$tY7Vb@j$ z+ww^{`0r^k5^s$1?B|7WsqBTfL1$B3qA$(Po|4JTd9L%g>q;e8RoE-;+%hPbECdy zOHJ5_n{(FmD3@M4F%C*7)b!31LJLXbbOH!6*PTYvK?}?|EAHGxe0jRtw^yCTX|CBG zdsWlP4i6g8dp{zIgjGmGI*hPo?%H|*?TbpmyIj%4Lds;;=I7;W4y<`fsJ%vX~3trmB$y@o&)&srPLd(VmxW{teXU_G_Iw0sJDe>FOf27Ky*-_{U1^mm;9|vFib4m zK;a&f#GhRsI_VK>Ik4yN@wNrXs^_q zC-cUi=N{Gs1}=FY;Q2q?R*Ts0U3lN&E(kA2!f<-Ii8NsuEX8=i7JEzUW z+mupQblhpf=#@G>F~=8k$vf6F3|X=s>H#WWMPE6tlQWg1UM~b6827?kgo3AaE0isaG;ls=`+$r0tq8v5r=?d@=PbO8NNB&+0(wCr{pX|iBHc6 ze|th&AqoqUoL|%j(&;4&8PC0Aj-6Xu4zxvlF3EGR?8_X#|LNtaNZvZ{HItf-c6O^g zmKpy*Yewh|(GX{_h+wfmkBNwxj$#R2cjhdi)RJy%4Y)U4!zWar)-a!ouao|8qrm8s zg;PJ=nNEYg54~L;$n!;`mznj?o>83fZhQR6&gsZjTl5URai2<;-s~+*K(aJ&n%1>Z zV(XlnDcybaPL}~GHw}nJ{tsc89> zLQ1Q)li#Ff6jdc97$!T{Rm<>CS5qj=I55_{yWX2H`1~jjzV3ot@{TH_NWHr0xS7-O@8BjLrWlSzE8>ycS^;0_fZpH5_9A8jd#YdrRRvkM(O4?-xen5B_D?zkv8GtUb$7Jhpf&i`qa2VK>6NN61oYGMJY zmgUC&1Ua@js#?qDp9}UUqHJf9o*X*Nr5R*fgt-B23)$@F`|6b=XHK5~o^O*q)NxIb zq_b5gd24jy`wFL;QJ%}{o-D=SkyzH)=Vub>tr&n5-1*+62q`$Ont1>g89yv7lotub zfRt!X!th-c)4{6Jj*9AX(UZ@pdKb<;cA^;~J5rV2!fpd;!O&C1N}LZ;UaK(yK4?L& z(lkY5{71iVR*RY576jW`2o7zBliPD}#y|f%#S-$* znChB_^pMLx#Pr^jtja{h>XAKrd~9=Pk{)f39(a1u zw|D7v#+BW1Z-IT0R5T7~fBa91%5D&}vnti8DYbsE%tmP57LHk^n~Q>ZPT6VCF|{Yg zK50)nDflN=+r#zutqjfp*1`?5B6}3!buNy!P%rd4?tZ32mjWm}w^A0{1yeQ#r2ZK^btC%=yVy>sGOVLWY zCjH~@D?xe{Ca;qAVEW`1i;>ogl@Ru&OIt<#ofug5N8Os>abEOod9~_`4!2-vXxai_3?d<7S@rt>0bxcK>0L_=t?~N^tucki~Y}&`= z9T4<3p=;FxvUFhz0tAXrpAY7T$>4S?C@a3$=OV5x-m{^b7qOf>=U-i%L&)@g;I^T27`+Pp1^EsdM`JC7LmWwk|YOlgx5C|mY z=wRyx^ldv$VmI(77Lf2jCwA4w(L)0Gk|Zu>0rNdk4!&1GAjyL}O=R$6$`1g9#M=49 zo{0$wj19v@s(FNALqK{)uv4ZmeS@nvhaLbDN;?wnxX{?Rz?cw_^+jAzh?;#^Oh_Cq zCgQ5v8F%A3dV3CFvd+=g+9SSV;bVY9(Ws*MG7cfWOOsRKl6^&dUw6jHoihi;oPy-6 zAF4TwWt(4~F0U*j8JpSQ_+AB8%v zzkri^uDfz(t>N_x-ddJS4Ja~9F?3@F3QD2-+%4C_LV*);i-N}Hn46z_M0r5i>iYo(D!J~-Hf`y}i zJqf%FVp1^m0l>Hxn*~@9V6Ne&OM3_Xu+scy@p~luZc8uM*Ytnpq-^xWO}zTT zM`pQk;~Xa$`xw*RK7fi~3W=_ELp}xxVMzq=j@k;6ZNEeqvIk>zpp%`Ks7P3@AHhQ2 z(=a!mO0h5Y?yj#1%TZt$*3d022iJ5oNo9{0!dLW6Kchl4zRbO^7g1m)X>{vnJ12#S?VlfRzAMBVjvzcaC=1OA3|JX{KU) z(PO}{Q;D(4W=1RnvPlLBju?#P;Csl1tAQ^;SH;s%YIN>s|80%{*g7v4qwP;1cnril zAz{Tu6mo0}b*A1od5_v>>#hxp^NL46bi(evu8M)7 z9<$^IRkGMkYePfBJB^L}4~_idfdSWXIzd(rhI#RY(`J#34JS>%E?nvGv0<@Zp6$&N!ZE1pBQK7^)?d;;A znQV6Nr=HCFm)@#~iEby|@CgVYyVp6Z!(e21HY|!zD6L|ig)TRC?fZ`>fpD6nZ1z$| zk46Wjb#%a6z7_F!ypgAre`T`Qc>YhXwUpS^x3zKjsiudUp*Tm!J!2m}BtPw|WLKZb z{4@N$Hedc?Gju6m6PZ_4bxv6`;<^=RVPWAFn|+UD(>*4LCoR%!F_4gGM@VwRI%5N~tCN-ZBB?(4hbMXCijG47TRBcWVIB2w9U&rcQM z$~2NM;pF3vDfi-DHgp1TXxsJMh*amF62jGa_oVF~rSLxv_Vn}&A1L~>mR@C<+znJg zL$HGh93IY=dju$hXjeZ=@Fkc&p4|KQ_05yk z^Wc9NQmIt0E4gmX+Az@S+8Pj-+NV#?3t<8M>3H74;^NCD@3#8jcxb-EB>#OAa1-sV z^nlK!E+Fe0g5wuA*S>gFYKTHgUR_;v=~D|yCWw3=@u^d!0gzuu z>@E22(`J_cUY34y+k)88aN-{>;{!)kvbT3@e;YkaBR{L{`*XkXrD#IkUvWz+3JMC# zQ@xUWt%-R)e`?CPhqra>vSD_VX%65hUkfyj4DuUSjV4@L{?sFGXkrq~Y4w*^QaZvO z92&ZLuF5h$bXgO+bW+7UJ`K>>B`%$Zrj8ox8hdXuTE3NTZ>NlR;3XAJB5m<_BPNTb zpSqni&jD)W(JZ7I2S2ld<6vWmzm%PY3>si>vGPwJ)4YKR!kl78b_;Sl3sv zNJy_Vi`!nkbnMu%Kk;9@7S0VQ^_3o<#{WfEdDY)<4Lm2m9>F#uribb}LcWZr;j@v737*5e#&&VhvKR>^D z`_;x-RV3Wh^s;Sq$dsaS5UWI2_^A!;>^uboKrC#vn|IX9-+$|4o9r#IvhwncV_0b# zJv}BS#>LlnV}WzaRy0#uxto#reaZnc)SyUI)kz?F)7x9Z7Y(ajIR;<$5#mAOja{7_wss`gt5#ru9ujmDMH_TPYz(GtJ6 zdR9eE8(H4HFxU^jJ8AKYcc88j`i?%c{E7%u&hzKWq#5^fE^NQw{bZM$10xW1z$P6O zD>|aF74&-l_HXZIOr1Su?TU&_>?B_P*OR{UKDxrWSneocEH9~09d!Ie>sxdDK`RG*v`9^{iGK`2NyNokAw474rYMmZjE4ao(JE|DPiLRHMW-qFA z>0%N1yt85FxkWQ6G+BD_D}V|O+SlK_h!O^_&OG}e6Tf;&78^y5)f~1?$^52+gIk8* z25;oIQgXC$d&!t>tNHpGZTk|F3?wYf9_%@g@>Zx*LdF#2YBvV0GRxo8P(uS$=M(zN z1hw+!3G?NycAm{TxNK{HMksoE^o|Kc?Q<}jF&?hqCRTYt(!;;^CV%VM6;-9N>MmaW zf`_Jd&E11gvm2ubb&iURE7fmKp%o1edwOKB%yy~fV!}-t>-SRPAn8=f7)iUan^&os z0Sw7IyRZC78MKFms2J6eVl@x81@6NT9;gMb4%P!c(CE*)ZVsR{kzmhHf&?|<0E+r+ zc;qHEw22(c0Ep-a%pTVAb1NOkvALrcMuC&=klU0u{}<`q;lx|2 Date: Mon, 14 Dec 2015 21:51:47 +0200 Subject: [PATCH 26/59] closes #3948 --- public/src/client/topic/fork.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js index 8e95ee6d9a..c29dc90f8f 100644 --- a/public/src/client/topic/fork.js +++ b/public/src/client/topic/fork.js @@ -118,7 +118,7 @@ define('forum/topic/fork', ['components'], function(components) { if (pids.length) { pids.sort(function(a,b) { return a - b; }); - forkModal.find('#fork-pids').html(pids.toString()); + forkModal.find('#fork-pids').html(pids.join(', ')); } else { showNoPostsSelected(); } From 37b13b48791f1c6b20159990a6aa43280cc111cd Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 14:56:28 -0500 Subject: [PATCH 27/59] Re-added file extension restriction ACP option Closes #3918 --- src/controllers/uploads.js | 8 ++++++++ src/views/admin/settings/post.tpl | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 511cfe2f8e..fec5607fe4 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -125,6 +125,14 @@ function uploadFile(uid, uploadedFile, callback) { return callback(new Error('[[error:file-too-big, ' + meta.config.maximumFileSize + ']]')); } + if (meta.config.hasOwnProperty('allowedFileExtensions')) { + var allowed = meta.config.allowedFileExtensions.split(',').filter(Boolean); + var extension = path.extname(uploadedFile.name).slice(1); + if (allowed.length > 0 && allowed.indexOf(extension) === -1) { + return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]')); + } + } + var filename = uploadedFile.name || 'upload'; filename = Date.now() + '-' + validator.escape(filename).substr(0, 255); diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 96ceb01ff6..dbb13f2714 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -132,13 +132,18 @@ Allow users to upload regular files +
- Maximum File Size

+ +
+ + +
- Topic Thumb Size

+ +
+ + +
+ +
+ + +

+ Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). + An empty list means all extensions are allowed. +

+
From 8c87ff44d1b23fd26c72847ae89fb785ad9c8e67 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 15:00:57 -0500 Subject: [PATCH 28/59] #3829 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35cadccc19..5c835f0428 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "nodebb-plugin-composer-default": "1.0.24", "nodebb-plugin-dbsearch": "0.3.0", "nodebb-plugin-emoji-extended": "0.4.17", - "nodebb-plugin-markdown": "4.0.8", + "nodebb-plugin-markdown": "4.0.9", "nodebb-plugin-mentions": "1.0.12", "nodebb-plugin-soundpack-default": "0.1.5", "nodebb-plugin-spam-be-gone": "0.4.5", From 530b0dc19e7431c4a2fac43c83af7dbfb5e61cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 14 Dec 2015 22:03:06 +0200 Subject: [PATCH 29/59] use .text() --- public/src/client/topic/fork.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js index c29dc90f8f..4f6fa3ae9f 100644 --- a/public/src/client/topic/fork.js +++ b/public/src/client/topic/fork.js @@ -118,7 +118,7 @@ define('forum/topic/fork', ['components'], function(components) { if (pids.length) { pids.sort(function(a,b) { return a - b; }); - forkModal.find('#fork-pids').html(pids.join(', ')); + forkModal.find('#fork-pids').text(pids.join(', ')); } else { showNoPostsSelected(); } From ea1b640435e8fc07e6879f9c168c81f12f2a8e24 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 14 Dec 2015 16:20:08 -0500 Subject: [PATCH 30/59] calling translator in parseMessage --- public/src/client/chats.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index e026a0ac2c..90b5e912ed 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -423,7 +423,9 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.parseMessage = function(data, callback) { templates.parse('partials/chat_message' + (Array.isArray(data) ? 's' : ''), { messages: data - }, callback); + }, function(html) { + translator.translate(html, callback); + }); }; function loadMoreRecentChats() { From 55b75ba5021968deb258ffae16958db09021265f Mon Sep 17 00:00:00 2001 From: psychobunny Date: Mon, 14 Dec 2015 18:06:45 -0500 Subject: [PATCH 31/59] up'd persona --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c835f0428..4d885bbc9d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "nodebb-plugin-spam-be-gone": "0.4.5", "nodebb-rewards-essentials": "0.0.6", "nodebb-theme-lavender": "3.0.2", - "nodebb-theme-persona": "4.0.42", + "nodebb-theme-persona": "4.0.44", "nodebb-theme-vanilla": "5.0.17", "nodebb-widget-essentials": "2.0.5", "nodemailer": "0.7.1", From e602d2ad4d9baadf8cf72006a7a05daf465f94ae Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 09:41:46 -0500 Subject: [PATCH 32/59] closes #3932 --- public/less/admin/bootstrap/alerts.less | 5 + public/less/admin/bootstrap/badges.less | 9 +- public/less/admin/bootstrap/bootstrap.less | 6 + .../less/admin/bootstrap/button-groups.less | 9 +- public/less/admin/bootstrap/buttons.less | 18 +- public/less/admin/bootstrap/carousel.less | 29 +-- public/less/admin/bootstrap/close.less | 1 + .../admin/bootstrap/component-animations.less | 3 +- public/less/admin/bootstrap/dropdowns.less | 9 +- public/less/admin/bootstrap/forms.less | 129 +++++++++--- public/less/admin/bootstrap/glyphicons.less | 75 ++++++- public/less/admin/bootstrap/input-groups.less | 11 +- public/less/admin/bootstrap/jumbotron.less | 11 +- public/less/admin/bootstrap/list-group.less | 12 +- public/less/admin/bootstrap/media.less | 19 ++ public/less/admin/bootstrap/mixins.less | 1 + .../bootstrap/mixins/background-variant.less | 3 +- .../less/admin/bootstrap/mixins/buttons.less | 25 ++- .../bootstrap/mixins/grid-framework.less | 4 +- public/less/admin/bootstrap/mixins/grid.less | 8 +- .../admin/bootstrap/mixins/hide-text.less | 4 +- .../admin/bootstrap/mixins/list-group.less | 3 +- .../admin/bootstrap/mixins/pagination.less | 3 +- .../admin/bootstrap/mixins/reset-text.less | 18 ++ .../mixins/responsive-visibility.less | 2 +- .../admin/bootstrap/mixins/text-emphasis.less | 3 +- .../bootstrap/mixins/vendor-prefixes.less | 6 +- public/less/admin/bootstrap/modals.less | 6 +- public/less/admin/bootstrap/navbar.less | 2 +- public/less/admin/bootstrap/navs.less | 2 - public/less/admin/bootstrap/normalize.less | 19 +- public/less/admin/bootstrap/pagination.less | 7 +- public/less/admin/bootstrap/panels.less | 12 +- public/less/admin/bootstrap/popovers.less | 12 +- public/less/admin/bootstrap/print.less | 6 - .../admin/bootstrap/responsive-embed.less | 20 +- public/less/admin/bootstrap/scaffolding.less | 11 ++ public/less/admin/bootstrap/tables.less | 6 +- public/less/admin/bootstrap/theme.less | 33 +++- public/less/admin/bootstrap/tooltip.less | 10 +- public/less/admin/bootstrap/type.less | 4 +- public/less/admin/bootstrap/utilities.less | 1 - public/less/admin/bootstrap/variables.less | 184 +++++++++--------- 43 files changed, 513 insertions(+), 248 deletions(-) create mode 100644 public/less/admin/bootstrap/mixins/reset-text.less diff --git a/public/less/admin/bootstrap/alerts.less b/public/less/admin/bootstrap/alerts.less index df070b8ab2..c4199db927 100644 --- a/public/less/admin/bootstrap/alerts.less +++ b/public/less/admin/bootstrap/alerts.less @@ -18,6 +18,7 @@ // Specified for the h4 to prevent conflicts of changing @headings-color color: inherit; } + // Provide class for links that match alerts .alert-link { font-weight: @alert-link-font-weight; @@ -28,6 +29,7 @@ > ul { margin-bottom: 0; } + > p + p { margin-top: 5px; } @@ -57,12 +59,15 @@ .alert-success { .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); } + .alert-info { .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); } + .alert-warning { .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); } + .alert-danger { .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); } diff --git a/public/less/admin/bootstrap/badges.less b/public/less/admin/bootstrap/badges.less index b27c405a30..6ee16dca41 100644 --- a/public/less/admin/bootstrap/badges.less +++ b/public/less/admin/bootstrap/badges.less @@ -12,7 +12,7 @@ font-weight: @badge-font-weight; color: @badge-color; line-height: @badge-line-height; - vertical-align: baseline; + vertical-align: middle; white-space: nowrap; text-align: center; background-color: @badge-bg; @@ -28,7 +28,9 @@ position: relative; top: -1px; } - .btn-xs & { + + .btn-xs &, + .btn-group-xs > .btn & { top: 0; padding: 1px 5px; } @@ -49,12 +51,15 @@ color: @badge-active-color; background-color: @badge-active-bg; } + .list-group-item > & { float: right; } + .list-group-item > & + & { margin-right: 5px; } + .nav-pills > li > a > & { margin-left: 3px; } diff --git a/public/less/admin/bootstrap/bootstrap.less b/public/less/admin/bootstrap/bootstrap.less index 61b77474f9..1c0477805f 100644 --- a/public/less/admin/bootstrap/bootstrap.less +++ b/public/less/admin/bootstrap/bootstrap.less @@ -1,3 +1,9 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + // Core variables and mixins @import "variables.less"; @import "mixins.less"; diff --git a/public/less/admin/bootstrap/button-groups.less b/public/less/admin/bootstrap/button-groups.less index f84febbd56..293245a650 100644 --- a/public/less/admin/bootstrap/button-groups.less +++ b/public/less/admin/bootstrap/button-groups.less @@ -36,6 +36,7 @@ margin-left: -5px; // Offset the first child's margin &:extend(.clearfix all); + .btn, .btn-group, .input-group { float: left; @@ -71,13 +72,13 @@ .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } -.btn-group > .btn-group:first-child { +.btn-group > .btn-group:first-child:not(:last-child) { > .btn:last-child, > .dropdown-toggle { .border-right-radius(0); } } -.btn-group > .btn-group:last-child > .btn:first-child { +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { .border-left-radius(0); } @@ -172,12 +173,12 @@ border-radius: 0; } &:first-child:not(:last-child) { - border-top-right-radius: @border-radius-base; + .border-top-radius(@btn-border-radius-base); .border-bottom-radius(0); } &:last-child:not(:first-child) { - border-bottom-left-radius: @border-radius-base; .border-top-radius(0); + .border-bottom-radius(@btn-border-radius-base); } } .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { diff --git a/public/less/admin/bootstrap/buttons.less b/public/less/admin/bootstrap/buttons.less index 40553c6386..9cbb8f416f 100644 --- a/public/less/admin/bootstrap/buttons.less +++ b/public/less/admin/bootstrap/buttons.less @@ -17,7 +17,7 @@ background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 border: 1px solid transparent; white-space: nowrap; - .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); + .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base); .user-select(none); &, @@ -47,10 +47,16 @@ &[disabled], fieldset[disabled] & { cursor: @cursor-disabled; - pointer-events: none; // Future-proof disabling of clicks .opacity(.65); .box-shadow(none); } + + a& { + &.disabled, + fieldset[disabled] & { + pointer-events: none; // Future-proof disabling of clicks on `` elements + } + } } @@ -107,7 +113,7 @@ &:hover, &:focus { color: @link-hover-color; - text-decoration: underline; + text-decoration: @link-hover-decoration; background-color: transparent; } &[disabled], @@ -126,14 +132,14 @@ .btn-lg { // line-height: ensure even-numbered height of button next to large input - .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); + .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large); } .btn-sm { // line-height: ensure proper height of button next to small input - .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); + .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); } .btn-xs { - .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); } diff --git a/public/less/admin/bootstrap/carousel.less b/public/less/admin/bootstrap/carousel.less index 5724d8a56e..252011e9e2 100644 --- a/public/less/admin/bootstrap/carousel.less +++ b/public/less/admin/bootstrap/carousel.less @@ -27,24 +27,24 @@ // WebKit CSS3 transforms for supported devices @media all and (transform-3d), (-webkit-transform-3d) { - transition: transform .6s ease-in-out; - backface-visibility: hidden; - perspective: 1000; + .transition-transform(~'0.6s ease-in-out'); + .backface-visibility(~'hidden'); + .perspective(1000px); &.next, &.active.right { - transform: translate3d(100%, 0, 0); + .translate3d(100%, 0, 0); left: 0; } &.prev, &.active.left { - transform: translate3d(-100%, 0, 0); + .translate3d(-100%, 0, 0); left: 0; } &.next.left, &.prev.right, &.active { - transform: translate3d(0, 0, 0); + .translate3d(0, 0, 0); left: 0; } } @@ -101,6 +101,7 @@ color: @carousel-control-color; text-align: center; text-shadow: @carousel-text-shadow; + background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug // We can't have this transition here because WebKit cancels the carousel // animation if you trip this while in the middle of another animation. @@ -130,6 +131,7 @@ .glyphicon-chevron-right { position: absolute; top: 50%; + margin-top: -10px; z-index: 5; display: inline-block; } @@ -147,7 +149,7 @@ .icon-next { width: 20px; height: 20px; - margin-top: -10px; + line-height: 1; font-family: serif; } @@ -195,6 +197,7 @@ // Internet Explorer 8-9 does not support clicks on elements without a set // `background-color`. We cannot use `filter` since that's not viewed as a // background color by the browser. Thus, a hack is needed. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer // // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we // set alpha transparency for the best results possible. @@ -238,18 +241,18 @@ .glyphicon-chevron-right, .icon-prev, .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; + width: (@carousel-control-font-size * 1.5); + height: (@carousel-control-font-size * 1.5); + margin-top: (@carousel-control-font-size / -2); + font-size: (@carousel-control-font-size * 1.5); } .glyphicon-chevron-left, .icon-prev { - margin-left: -15px; + margin-left: (@carousel-control-font-size / -2); } .glyphicon-chevron-right, .icon-next { - margin-right: -15px; + margin-right: (@carousel-control-font-size / -2); } } diff --git a/public/less/admin/bootstrap/close.less b/public/less/admin/bootstrap/close.less index 9b4e74f2b8..6d5bfe087a 100644 --- a/public/less/admin/bootstrap/close.less +++ b/public/less/admin/bootstrap/close.less @@ -23,6 +23,7 @@ // Additional properties for button version // iOS requires the button element instead of an anchor tag. // If you want the anchor version, it requires `href="#"`. + // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile button& { padding: 0; cursor: pointer; diff --git a/public/less/admin/bootstrap/component-animations.less b/public/less/admin/bootstrap/component-animations.less index 967715d98b..0bcee910ac 100644 --- a/public/less/admin/bootstrap/component-animations.less +++ b/public/less/admin/bootstrap/component-animations.less @@ -17,9 +17,8 @@ .collapse { display: none; - visibility: hidden; - &.in { display: block; visibility: visible; } + &.in { display: block; } tr&.in { display: table-row; } tbody&.in { display: table-row-group; } } diff --git a/public/less/admin/bootstrap/dropdowns.less b/public/less/admin/bootstrap/dropdowns.less index 84a48c1413..f6876c1a9b 100644 --- a/public/less/admin/bootstrap/dropdowns.less +++ b/public/less/admin/bootstrap/dropdowns.less @@ -10,12 +10,14 @@ height: 0; margin-left: 2px; vertical-align: middle; - border-top: @caret-width-base solid; + border-top: @caret-width-base dashed; + border-top: @caret-width-base solid ~"\9"; // IE8 border-right: @caret-width-base solid transparent; border-left: @caret-width-base solid transparent; } // The dropdown wrapper (div) +.dropup, .dropdown { position: relative; } @@ -183,14 +185,15 @@ // Reverse the caret .caret { border-top: 0; - border-bottom: @caret-width-base solid; + border-bottom: @caret-width-base dashed; + border-bottom: @caret-width-base solid ~"\9"; // IE8 content: ""; } // Different positioning for bottom up menu .dropdown-menu { top: auto; bottom: 100%; - margin-bottom: 1px; + margin-bottom: 2px; } } diff --git a/public/less/admin/bootstrap/forms.less b/public/less/admin/bootstrap/forms.less index 1bcc2b6b97..e8b071a138 100644 --- a/public/less/admin/bootstrap/forms.less +++ b/public/less/admin/bootstrap/forms.less @@ -56,7 +56,6 @@ input[type="checkbox"] { line-height: normal; } -// Set the height of file controls to match text inputs input[type="file"] { display: block; } @@ -123,7 +122,7 @@ output { background-color: @input-bg; background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 border: 1px solid @input-border; - border-radius: @input-border-radius; + border-radius: @input-border-radius; // Note: This has no effect on s in CSS. .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); @@ -133,6 +132,12 @@ output { // Placeholder .placeholder(); + // Unstyle the caret on `` background color -@input-bg: transparent; +@input-bg: #fff; //** `` background color -@input-bg-disabled: transparent; +@input-bg-disabled: @gray-lighter; //** Text color for ``s @input-color: @gray; //** `` border color -@input-border: transparent; +@input-border: #ccc; // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4 //** Default `.form-control` border radius @@ -203,7 +203,7 @@ @input-border-focus: #66afe9; //** Placeholder text color -@input-color-placeholder: @gray-light; +@input-color-placeholder: #999; //** Default `.form-control` height @input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); @@ -219,7 +219,7 @@ @legend-border-color: #e5e5e5; //** Background color for textual input addons -@input-group-addon-bg: transparent; +@input-group-addon-bg: @gray-lighter; //** Border color for textual input addons @input-group-addon-border-color: @input-border; @@ -241,11 +241,11 @@ @dropdown-divider-bg: #e5e5e5; //** Dropdown link text color. -@dropdown-link-color: @text-color; +@dropdown-link-color: @gray-dark; //** Hover color for dropdown links. @dropdown-link-hover-color: darken(@gray-dark, 5%); //** Hover background for dropdown links. -@dropdown-link-hover-bg: @gray-lighter; +@dropdown-link-hover-bg: #f5f5f5; //** Active dropdown menu item text color. @dropdown-link-active-color: @component-active-color; @@ -259,7 +259,7 @@ @dropdown-header-color: @gray-light; //** Deprecated `@dropdown-caret-color` as of v3.1.0 -@dropdown-caret-color: @gray-light; +@dropdown-caret-color: #000; //-- Z-index master list @@ -357,45 +357,45 @@ //## // Basics of a navbar -@navbar-height: 64px; +@navbar-height: 50px; @navbar-margin-bottom: @line-height-computed; @navbar-border-radius: @border-radius-base; @navbar-padding-horizontal: floor((@grid-gutter-width / 2)); @navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); @navbar-collapse-max-height: 340px; -@navbar-default-color: @gray-light; -@navbar-default-bg: #fff; -@navbar-default-border: transparent; +@navbar-default-color: #777; +@navbar-default-bg: #f8f8f8; +@navbar-default-border: darken(@navbar-default-bg, 6.5%); // Navbar links -@navbar-default-link-color: @gray; -@navbar-default-link-hover-color: @gray-dark; +@navbar-default-link-color: #777; +@navbar-default-link-hover-color: #333; @navbar-default-link-hover-bg: transparent; -@navbar-default-link-active-color: @gray-dark; +@navbar-default-link-active-color: #555; @navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%); @navbar-default-link-disabled-color: #ccc; @navbar-default-link-disabled-bg: transparent; // Navbar brand label @navbar-default-brand-color: @navbar-default-link-color; -@navbar-default-brand-hover-color: @navbar-default-link-hover-color; +@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); @navbar-default-brand-hover-bg: transparent; // Navbar toggle -@navbar-default-toggle-hover-bg: transparent; -@navbar-default-toggle-icon-bar-bg: rgba(0,0,0,0.5); -@navbar-default-toggle-border-color: transparent; +@navbar-default-toggle-hover-bg: #ddd; +@navbar-default-toggle-icon-bar-bg: #888; +@navbar-default-toggle-border-color: #ddd; //=== Inverted navbar // Reset inverted navbar basics -@navbar-inverse-color: @gray-light; -@navbar-inverse-bg: @brand-primary; -@navbar-inverse-border: transparent; +@navbar-inverse-color: lighten(@gray-light, 15%); +@navbar-inverse-bg: #222; +@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); // Inverted navbar links -@navbar-inverse-link-color: lighten(@brand-primary, 30%); +@navbar-inverse-link-color: lighten(@gray-light, 15%); @navbar-inverse-link-hover-color: #fff; @navbar-inverse-link-hover-bg: transparent; @navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; @@ -408,10 +408,10 @@ @navbar-inverse-brand-hover-color: #fff; @navbar-inverse-brand-hover-bg: transparent; -// Inverted navbar toggle\ -@navbar-inverse-toggle-hover-bg: transparent; -@navbar-inverse-toggle-icon-bar-bg: rgba(0,0,0,0.5); -@navbar-inverse-toggle-border-color: transparent; +// Inverted navbar toggle +@navbar-inverse-toggle-hover-bg: #333; +@navbar-inverse-toggle-icon-bar-bg: #fff; +@navbar-inverse-toggle-border-color: #333; //== Navs @@ -426,15 +426,15 @@ @nav-disabled-link-hover-color: @gray-light; //== Tabs -@nav-tabs-border-color: transparent; +@nav-tabs-border-color: #ddd; @nav-tabs-link-hover-border-color: @gray-lighter; -@nav-tabs-active-link-hover-bg: transparent; +@nav-tabs-active-link-hover-bg: @body-bg; @nav-tabs-active-link-hover-color: @gray; -@nav-tabs-active-link-hover-border-color: transparent; +@nav-tabs-active-link-hover-border-color: #ddd; -@nav-tabs-justified-link-border-color: @nav-tabs-border-color; +@nav-tabs-justified-link-border-color: #ddd; @nav-tabs-justified-active-link-border-color: @body-bg; //== Pills @@ -486,8 +486,8 @@ @jumbotron-padding: 30px; @jumbotron-color: inherit; -@jumbotron-bg: #f9f9f9; -@jumbotron-heading-color: @headings-color; +@jumbotron-bg: @gray-lighter; +@jumbotron-heading-color: inherit; @jumbotron-font-size: ceil((@font-size-base * 1.5)); @jumbotron-heading-font-size: ceil((@font-size-base * 4.5)); @@ -496,20 +496,20 @@ // //## Define colors for form feedback states and, by default, alerts. -@state-success-text: @brand-success; +@state-success-text: #3c763d; @state-success-bg: #dff0d8; @state-success-border: darken(spin(@state-success-bg, -10), 5%); -@state-info-text: @brand-info; -@state-info-bg: #e1bee7; +@state-info-text: #31708f; +@state-info-bg: #d9edf7; @state-info-border: darken(spin(@state-info-bg, -10), 7%); -@state-warning-text: @brand-warning; -@state-warning-bg: #ffe0b2; +@state-warning-text: #8a6d3b; +@state-warning-bg: #fcf8e3; @state-warning-border: darken(spin(@state-warning-bg, -10), 5%); -@state-danger-text: @brand-danger; -@state-danger-bg: #f9bdbb; +@state-danger-text: #a94442; +@state-danger-bg: #f2dede; @state-danger-border: darken(spin(@state-danger-bg, -10), 5%); @@ -522,7 +522,7 @@ //** Tooltip text color @tooltip-color: #fff; //** Tooltip background color -@tooltip-bg: #727272; +@tooltip-bg: #000; @tooltip-opacity: .9; //** Tooltip arrow width @@ -540,9 +540,9 @@ //** Popover maximum width @popover-max-width: 276px; //** Popover border color -@popover-border-color: transparent; +@popover-border-color: rgba(0,0,0,.2); //** Popover fallback border color -@popover-fallback-border-color: transparent; +@popover-fallback-border-color: #ccc; //** Popover title background color @popover-title-bg: darken(@popover-bg, 3%); @@ -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, 5%); //** Popover outer arrow fallback color @popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); @@ -598,7 +598,7 @@ //** Background color of modal content area @modal-content-bg: #fff; //** Modal content border color -@modal-content-border-color: transparent; +@modal-content-border-color: rgba(0,0,0,.2); //** Modal content border color **for IE8** @modal-content-fallback-border-color: #999; @@ -607,7 +607,7 @@ //** Modal backdrop opacity @modal-backdrop-opacity: .5; //** Modal header border color -@modal-header-border-color: transparent; +@modal-header-border-color: #e5e5e5; //** Modal footer border color @modal-footer-border-color: @modal-header-border-color; @@ -720,21 +720,21 @@ @panel-primary-border: @brand-primary; @panel-primary-heading-bg: @brand-primary; -@panel-success-text: #fff; +@panel-success-text: @state-success-text; @panel-success-border: @state-success-border; -@panel-success-heading-bg: @brand-success; +@panel-success-heading-bg: @state-success-bg; -@panel-info-text: #fff; +@panel-info-text: @state-info-text; @panel-info-border: @state-info-border; -@panel-info-heading-bg: @brand-info; +@panel-info-heading-bg: @state-info-bg; -@panel-warning-text: #fff; +@panel-warning-text: @state-warning-text; @panel-warning-border: @state-warning-border; -@panel-warning-heading-bg: @brand-warning; +@panel-warning-heading-bg: @state-warning-bg; -@panel-danger-text: #fff; +@panel-danger-text: @state-danger-text; @panel-danger-border: @state-danger-border; -@panel-danger-heading-bg: @brand-danger; +@panel-danger-heading-bg: @state-danger-bg; //== Thumbnails @@ -760,8 +760,8 @@ // //## -@well-bg: #f9f9f9; -@well-border: transparent; +@well-bg: #f5f5f5; +@well-border: darken(@well-bg, 7%); //== Badges @@ -778,7 +778,7 @@ //** Badge background color in active nav link @badge-active-bg: #fff; -@badge-font-weight: normal; +@badge-font-weight: bold; @badge-line-height: 1; @badge-border-radius: 10px; @@ -820,9 +820,9 @@ // //## -@close-font-weight: normal; +@close-font-weight: bold; @close-color: #000; -@close-text-shadow: none; +@close-text-shadow: 0 1px 0 #fff; //== Code @@ -863,5 +863,7 @@ @page-header-border-color: @gray-lighter; //** Width of horizontal description list titles @dl-horizontal-offset: @component-offset-horizontal; +//** Point at which .dl-horizontal becomes horizontal +@dl-horizontal-breakpoint: @grid-float-breakpoint; //** Horizontal line color. @hr-border: @gray-lighter; From 33a3a56fd7d5a0a415fe5ca7b453c56f735ee62f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 10:04:58 -0500 Subject: [PATCH 33/59] Fixed issue where -w and -a flags didn't work in NodeBB reset. --- src/reset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reset.js b/src/reset.js index ce1b585a01..eae981ba5f 100644 --- a/src/reset.js +++ b/src/reset.js @@ -123,7 +123,7 @@ function resetPlugins(callback) { } function resetWidgets(callback) { - require('./src/widgets').reset(function(err) { + require('./widgets').reset(function(err) { winston.info('[reset] All Widgets moved to Draft Zone'); if (typeof callback === 'function') { callback(err); From 9db0f59432b1724f1348763950c1b38f30aa2868 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 11:20:21 -0500 Subject: [PATCH 34/59] part of #3912 --- public/language/en_GB/notifications.json | 2 ++ src/notifications.js | 45 ++++++++++++++++++++++++ src/socket.io/helpers.js | 3 +- src/user/notifications.js | 33 +++++++---------- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index f9b569170d..3048f082b9 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -17,6 +17,8 @@ "moved_your_post": "%1 has moved your post to %2", "moved_your_topic": "%1 has moved %2", "favourited_your_post_in": "%1 has favourited your post in %2.", + "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.", + "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.", "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to" : "%1 has posted a reply to: %2", "user_posted_topic": "%1 has posted a new topic: %2", diff --git a/src/notifications.js b/src/notifications.js index 2cd372923e..ec77cb4a99 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -331,5 +331,50 @@ var async = require('async'), }); }; + 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'], + isolated, modifyIndex; + + notifications = mergeIds.reduce(function(notifications, mergeId) { + isolated = notifications.filter(function(notifObj) { + return notifObj.mergeId === mergeId; + }); + + if (isolated.length <= 1) { + return notifications; // Nothing to merge + } + + modifyIndex = notifications.indexOf(isolated[0]); + + switch(mergeId) { + case 'notifications:favourited_your_post_in': + var usernames = isolated.map(function(notifObj) { + return notifObj.user.username; + }); + var numUsers = usernames.length; + + // Update bodyShort + if (numUsers === 2) { + isolated[0].bodyShort = '[[notifications:favourited_your_post_in_dual, ' + usernames.join(', ') + ', Welcome to your NodeBB!]]' + } else { + isolated[0].bodyShort = '[[notifications:favourited_your_post_in_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', Welcome to your NodeBB!]]' + } + break; + } + + // Filter out duplicates + return notifications.filter(function(notifObj, idx) { + return notifObj.mergeId !== mergeId || idx === modifyIndex; + }); + }, notifications); + + plugins.fireHook('filter:notifications.merge', { + notifications: notifications + }, function(err, data) { + callback(err, data.notifications); + }); + }; + }(exports)); diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 5b6a2a2e67..19e46871b3 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -69,7 +69,8 @@ SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification) bodyLong: results.postObj.content, pid: pid, nid: 'post:' + pid + ':uid:' + fromuid, - from: fromuid + from: fromuid, + mergeId: notification }, function(err, notification) { if (!err && notification) { notifications.push(notification, [postData.uid]); diff --git a/src/user/notifications.js b/src/user/notifications.js index 477a9cf53f..5a30149eef 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -64,28 +64,21 @@ var async = require('async'), } function getNotificationsFromSet(set, read, uid, start, stop, callback) { - db.getSortedSetRevRange(set, start, stop, function(err, nids) { - if (err) { - return callback(err); - } - - if(!Array.isArray(nids) || !nids.length) { - return callback(null, []); - } - - UserNotifications.getNotifications(nids, uid, function(err, notifications) { - if (err) { - return callback(err); + async.waterfall([ + async.apply(db.getSortedSetRevRange, set, start, stop), + function(nids, next) { + if(!Array.isArray(nids) || !nids.length) { + return callback(null, []); } + UserNotifications.getNotifications(nids, uid, next); + }, + function(notifs, next) { var deletedNids = []; - notifications.forEach(function(notification, index) { + notifs.forEach(function(notification, index) { if (!notification) { - if (process.env.NODE_ENV === 'development') { - winston.info('[notifications.get] nid ' + nids[index] + ' not found. Removing.'); - } - + winston.verbose('[notifications.get] nid ' + nids[index] + ' not found. Removing.'); deletedNids.push(nids[index]); } else { notification.read = read; @@ -97,9 +90,9 @@ var async = require('async'), db.sortedSetRemove(set, deletedNids); } - callback(null, notifications); - }); - }); + notifications.merge(notifs, next); + } + ], callback); } UserNotifications.getNotifications = function(nids, uid, callback) { From 05df8900db4fab06ede2b15c4e133c2ebfa17f72 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 12:15:24 -0500 Subject: [PATCH 35/59] closes #3912 --- public/language/en_GB/notifications.json | 8 ++++++++ src/notifications.js | 20 +++++++++++++++----- src/socket.io/helpers.js | 3 ++- src/socket.io/posts/flag.js | 4 +++- src/socket.io/user.js | 3 ++- src/topics/follow.js | 4 +++- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 3048f082b9..0692155373 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -14,15 +14,23 @@ "new_message_from": "New message from %1", "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", "moved_your_post": "%1 has moved your post to %2", "moved_your_topic": "%1 has moved %2", "favourited_your_post_in": "%1 has favourited your post in %2.", "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.", "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.", "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", "user_posted_to" : "%1 has posted a reply to: %2", + "user_posted_to_dual" : "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple" : "%1 and %2 others have posted replies to: %3", "user_posted_topic": "%1 has posted a new topic: %2", "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", "new_register": "%1 sent a registration request.", "email-confirmed": "Email Confirmed", diff --git a/src/notifications.js b/src/notifications.js index ec77cb4a99..c09f438fc9 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -333,12 +333,18 @@ var async = require('async'), 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'], + var mergeIds = [ + 'notifications:favourited_your_post_in', + 'notifications:upvoted_your_post_in', + 'notifications:user_started_following_you', + 'notifications:user_posted_to', + 'notifications:user_flagged_post_in' + ], isolated, modifyIndex; notifications = mergeIds.reduce(function(notifications, mergeId) { isolated = notifications.filter(function(notifObj) { - return notifObj.mergeId === mergeId; + return notifObj.mergeId.split('|')[0] === mergeId; }); if (isolated.length <= 1) { @@ -348,7 +354,11 @@ var async = require('async'), modifyIndex = notifications.indexOf(isolated[0]); switch(mergeId) { - case 'notifications:favourited_your_post_in': + case 'notifications:favourited_your_post_in': // 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 = isolated.map(function(notifObj) { return notifObj.user.username; }); @@ -356,9 +366,9 @@ var async = require('async'), // Update bodyShort if (numUsers === 2) { - isolated[0].bodyShort = '[[notifications:favourited_your_post_in_dual, ' + usernames.join(', ') + ', Welcome to your NodeBB!]]' + isolated[0].bodyShort = '[[' + mergeId.split('|') + '_dual, ' + usernames.join(', ') + ', ' + isolated[0].topicTitle + ']]' } else { - isolated[0].bodyShort = '[[notifications:favourited_your_post_in_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', Welcome to your NodeBB!]]' + isolated[0].bodyShort = '[[' + mergeId.split('|') + '_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', ' + isolated[0].topicTitle + ']]' } break; } diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 19e46871b3..789ec1660a 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -70,7 +70,8 @@ SocketHelpers.sendNotificationToPostOwner = function(pid, fromuid, notification) pid: pid, nid: 'post:' + pid + ':uid:' + fromuid, from: fromuid, - mergeId: notification + mergeId: notification + '|' + postData.tid, + topicTitle: results.topicTitle }, function(err, notification) { if (!err && notification) { notifications.push(notification, [postData.uid]); diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js index aa82e84626..d3f4007e01 100644 --- a/src/socket.io/posts/flag.js +++ b/src/socket.io/posts/flag.js @@ -84,7 +84,9 @@ module.exports = function(SocketPosts) { bodyLong: post.content, pid: data.pid, nid: 'post_flag:' + data.pid + ':uid:' + socket.uid, - from: socket.uid + from: socket.uid, + mergeId: 'notifications:user_flagged_post_in|' + data.pid, + topicTitle: post.topic.title }, function(err, notification) { if (err || !notification) { return next(err); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 14b0c2c828..3a19d62d63 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -142,7 +142,8 @@ 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: '/user/' + userData.userslug, + mergeId: 'notifications:user_started_following_you' }, next); }, function(notification, next) { diff --git a/src/topics/follow.js b/src/topics/follow.js index e05a705c51..250f571dc4 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -137,7 +137,9 @@ module.exports = function(Topics) { pid: postData.pid, nid: 'new_post:tid:' + postData.topic.tid + ':pid:' + postData.pid + ':uid:' + exceptUid, tid: postData.topic.tid, - from: exceptUid + from: exceptUid, + mergeId: 'notifications:user_posted_to|' + postData.topic.tid, + topicTitle: title }, function(err, notification) { if (err) { return next(err); From 9f5815555a6f6a44ab2aeee0e19558ae2dc88690 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 13:56:12 -0500 Subject: [PATCH 36/59] fixed null error with notif grouping --- src/notifications.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/notifications.js b/src/notifications.js index c09f438fc9..cda7fae2fd 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -344,6 +344,10 @@ var async = require('async'), notifications = mergeIds.reduce(function(notifications, mergeId) { isolated = notifications.filter(function(notifObj) { + if (!notifObj.hasOwnProperty('mergeId')) { + return false; + } + return notifObj.mergeId.split('|')[0] === mergeId; }); From 40f0076b77ad7298c6dd0e88cb5e73fe08603e87 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 14:30:58 -0500 Subject: [PATCH 37/59] fixed bug with notif merging --- src/notifications.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index cda7fae2fd..2340f50dba 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -340,7 +340,7 @@ var async = require('async'), 'notifications:user_posted_to', 'notifications:user_flagged_post_in' ], - isolated, modifyIndex; + isolated, differentiator, modifyIndex; notifications = mergeIds.reduce(function(notifications, mergeId) { isolated = notifications.filter(function(notifObj) { @@ -355,6 +355,8 @@ var async = require('async'), return notifications; // Nothing to merge } + differentiator = isolated[0].mergeId.split('|')[1]; + modifyIndex = notifications.indexOf(isolated[0]); switch(mergeId) { @@ -370,16 +372,16 @@ var async = require('async'), // Update bodyShort if (numUsers === 2) { - isolated[0].bodyShort = '[[' + mergeId.split('|') + '_dual, ' + usernames.join(', ') + ', ' + isolated[0].topicTitle + ']]' + isolated[0].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + ', ' + isolated[0].topicTitle + ']]' } else { - isolated[0].bodyShort = '[[' + mergeId.split('|') + '_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', ' + isolated[0].topicTitle + ']]' + isolated[0].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers-1) + ', ' + isolated[0].topicTitle + ']]' } break; } // Filter out duplicates return notifications.filter(function(notifObj, idx) { - return notifObj.mergeId !== mergeId || idx === modifyIndex; + return notifObj.mergeId !== mergeId + '|' + differentiator || idx === modifyIndex; }); }, notifications); From f4187fc67154dd601f9ba29369fcc00b3a60f2f2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 16 Dec 2015 15:33:00 -0500 Subject: [PATCH 38/59] fixing crash with deleted notifications --- src/user/notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/user/notifications.js b/src/user/notifications.js index 5a30149eef..b085e7654d 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -78,8 +78,8 @@ var async = require('async'), notifs.forEach(function(notification, index) { if (!notification) { - winston.verbose('[notifications.get] nid ' + nids[index] + ' not found. Removing.'); - deletedNids.push(nids[index]); + winston.verbose('[notifications.get] nid ' + notification.nid + ' not found. Removing.'); + deletedNids.push(notification.nid); } else { notification.read = read; notification.readClass = !notification.read ? 'unread' : ''; From 8b04b4fe504f5d32e17fd37ec7f3a539b73a36b5 Mon Sep 17 00:00:00 2001 From: psychobunny Date: Wed, 16 Dec 2015 16:55:50 -0500 Subject: [PATCH 39/59] up persona --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d885bbc9d..295ff26a0c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "nodebb-plugin-spam-be-gone": "0.4.5", "nodebb-rewards-essentials": "0.0.6", "nodebb-theme-lavender": "3.0.2", - "nodebb-theme-persona": "4.0.44", + "nodebb-theme-persona": "4.0.45", "nodebb-theme-vanilla": "5.0.17", "nodebb-widget-essentials": "2.0.5", "nodemailer": "0.7.1", From 23c2fb2b52cf8d5dadf6fdde59b73b6905a8e12f Mon Sep 17 00:00:00 2001 From: psychobunny Date: Wed, 16 Dec 2015 17:24:07 -0500 Subject: [PATCH 40/59] un-nuking @julianlam :rage1: also fixed for latest BS --- public/less/admin/bootstrap/variables.less | 182 ++++++++++----------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/public/less/admin/bootstrap/variables.less b/public/less/admin/bootstrap/variables.less index b057ef5bf9..d0cf54f043 100644 --- a/public/less/admin/bootstrap/variables.less +++ b/public/less/admin/bootstrap/variables.less @@ -1,4 +1,4 @@ -// +// Paper 3.3.5 // Variables // -------------------------------------------------- @@ -9,16 +9,16 @@ @gray-base: #000; @gray-darker: lighten(@gray-base, 13.5%); // #222 -@gray-dark: lighten(@gray-base, 20%); // #333 -@gray: lighten(@gray-base, 33.5%); // #555 -@gray-light: lighten(@gray-base, 46.7%); // #777 +@gray-dark: #212121; +@gray: #666; +@gray-light: #bbb; @gray-lighter: lighten(@gray-base, 93.5%); // #eee -@brand-primary: darken(#428bca, 6.5%); // #337ab7 -@brand-success: #5cb85c; -@brand-info: #5bc0de; -@brand-warning: #f0ad4e; -@brand-danger: #d9534f; +@brand-primary: #2196F3; +@brand-success: #4CAF50; +@brand-info: #9C27B0; +@brand-warning: #ff9800; +@brand-danger: #e51c23; //== Scaffolding @@ -28,7 +28,7 @@ //** Background color for ``. @body-bg: #fff; //** Global text color on ``. -@text-color: @gray-dark; +@text-color: @gray; //** Global textual link color. @link-color: @brand-primary; @@ -42,33 +42,33 @@ // //## Font, line-height, and color for body text, headings, and more. -@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-sans-serif: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; @font-family-serif: Georgia, "Times New Roman", Times, serif; //** Default monospace fonts for ``, ``, and `
`.
 @font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
 @font-family-base:        @font-family-sans-serif;
 
-@font-size-base:          14px;
+@font-size-base:          13px;
 @font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
 @font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
 
-@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
-@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
-@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
-@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
-@font-size-h5:            @font-size-base;
-@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+@font-size-h1:            56px;
+@font-size-h2:            45px;
+@font-size-h3:            34px;
+@font-size-h4:            24px;
+@font-size-h5:            20px;
+@font-size-h6:            14px;
 
 //** Unit-less `line-height` for use in components like buttons.
-@line-height-base:        1.428571429; // 20/14
+@line-height-base:        1.846; // 20/14
 //** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
 @line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
 
 //** By default, this inherits from the ``.
 @headings-font-family:    inherit;
-@headings-font-weight:    500;
+@headings-font-weight:    400;
 @headings-line-height:    1.1;
-@headings-color:          inherit;
+@headings-color:          #444;
 
 
 //== Iconography
@@ -88,7 +88,7 @@
 //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
 
 @padding-base-vertical:     6px;
-@padding-base-horizontal:   12px;
+@padding-base-horizontal:   16px;
 
 @padding-large-vertical:    10px;
 @padding-large-horizontal:  16px;
@@ -102,9 +102,9 @@
 @line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
 @line-height-small:         1.5;
 
-@border-radius-base:        4px;
-@border-radius-large:       6px;
-@border-radius-small:       3px;
+@border-radius-base:        0px;
+@border-radius-large:       0px;
+@border-radius-small:       0px;
 
 //** Global color for active items (e.g., navs or dropdowns).
 @component-active-color:    #fff;
@@ -144,29 +144,29 @@
 
 @btn-font-weight:                normal;
 
-@btn-default-color:              #333;
+@btn-default-color:              #444;
 @btn-default-bg:                 #fff;
-@btn-default-border:             #ccc;
+@btn-default-border:             transparent;
 
 @btn-primary-color:              #fff;
 @btn-primary-bg:                 @brand-primary;
-@btn-primary-border:             darken(@btn-primary-bg, 5%);
+@btn-primary-border:             transparent;
 
 @btn-success-color:              #fff;
 @btn-success-bg:                 @brand-success;
-@btn-success-border:             darken(@btn-success-bg, 5%);
+@btn-success-border:             transparent;
 
 @btn-info-color:                 #fff;
 @btn-info-bg:                    @brand-info;
-@btn-info-border:                darken(@btn-info-bg, 5%);
+@btn-info-border:                transparent;
 
 @btn-warning-color:              #fff;
 @btn-warning-bg:                 @brand-warning;
-@btn-warning-border:             darken(@btn-warning-bg, 5%);
+@btn-warning-border:             transparent;
 
 @btn-danger-color:               #fff;
 @btn-danger-bg:                  @brand-danger;
-@btn-danger-border:              darken(@btn-danger-bg, 5%);
+@btn-danger-border:              transparent;
 
 @btn-link-disabled-color:        @gray-light;
 
@@ -181,14 +181,14 @@
 //##
 
 //** `` background color
-@input-bg:                       #fff;
+@input-bg:                       transparent;
 //** `` background color
-@input-bg-disabled:              @gray-lighter;
+@input-bg-disabled:              transparent;
 
 //** Text color for ``s
 @input-color:                    @gray;
 //** `` border color
-@input-border:                   #ccc;
+@input-border:                   transparent;
 
 // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
 //** Default `.form-control` border radius
@@ -203,7 +203,7 @@
 @input-border-focus:             #66afe9;
 
 //** Placeholder text color
-@input-color-placeholder:        #999;
+@input-color-placeholder:        @gray-light;
 
 //** Default `.form-control` height
 @input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
@@ -219,7 +219,7 @@
 @legend-border-color:            #e5e5e5;
 
 //** Background color for textual input addons
-@input-group-addon-bg:           @gray-lighter;
+@input-group-addon-bg:           transparent;
 //** Border color for textual input addons
 @input-group-addon-border-color: @input-border;
 
@@ -241,11 +241,11 @@
 @dropdown-divider-bg:            #e5e5e5;
 
 //** Dropdown link text color.
-@dropdown-link-color:            @gray-dark;
+@dropdown-link-color:            @text-color;
 //** Hover color for dropdown links.
 @dropdown-link-hover-color:      darken(@gray-dark, 5%);
 //** Hover background for dropdown links.
-@dropdown-link-hover-bg:         #f5f5f5;
+@dropdown-link-hover-bg:         @gray-lighter;
 
 //** Active dropdown menu item text color.
 @dropdown-link-active-color:     @component-active-color;
@@ -259,7 +259,7 @@
 @dropdown-header-color:          @gray-light;
 
 //** Deprecated `@dropdown-caret-color` as of v3.1.0
-@dropdown-caret-color:           #000;
+@dropdown-caret-color:           @gray-light;
 
 
 //-- Z-index master list
@@ -357,45 +357,45 @@
 //##
 
 // Basics of a navbar
-@navbar-height:                    50px;
+@navbar-height:                    64px;
 @navbar-margin-bottom:             @line-height-computed;
 @navbar-border-radius:             @border-radius-base;
 @navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
 @navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
 @navbar-collapse-max-height:       340px;
 
-@navbar-default-color:             #777;
-@navbar-default-bg:                #f8f8f8;
-@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-color:             @gray-light;
+@navbar-default-bg:                #fff;
+@navbar-default-border:            transparent;
 
 // Navbar links
-@navbar-default-link-color:                #777;
-@navbar-default-link-hover-color:          #333;
+@navbar-default-link-color:                @gray;
+@navbar-default-link-hover-color:          @gray-dark;
 @navbar-default-link-hover-bg:             transparent;
-@navbar-default-link-active-color:         #555;
+@navbar-default-link-active-color:         @gray-dark;
 @navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
 @navbar-default-link-disabled-color:       #ccc;
 @navbar-default-link-disabled-bg:          transparent;
 
 // Navbar brand label
 @navbar-default-brand-color:               @navbar-default-link-color;
-@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-color:         @navbar-default-link-hover-color;
 @navbar-default-brand-hover-bg:            transparent;
 
 // Navbar toggle
-@navbar-default-toggle-hover-bg:           #ddd;
-@navbar-default-toggle-icon-bar-bg:        #888;
-@navbar-default-toggle-border-color:       #ddd;
+@navbar-default-toggle-hover-bg:           transparent;
+@navbar-default-toggle-icon-bar-bg:        rgba(0,0,0,0.5);
+@navbar-default-toggle-border-color:       transparent;
 
 
 //=== Inverted navbar
 // Reset inverted navbar basics
-@navbar-inverse-color:                      lighten(@gray-light, 15%);
-@navbar-inverse-bg:                         #222;
-@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-color:                      @gray-light;
+@navbar-inverse-bg:                         @brand-primary;
+@navbar-inverse-border:                     transparent;
 
 // Inverted navbar links
-@navbar-inverse-link-color:                 lighten(@gray-light, 15%);
+@navbar-inverse-link-color:                 lighten(@brand-primary, 30%);
 @navbar-inverse-link-hover-color:           #fff;
 @navbar-inverse-link-hover-bg:              transparent;
 @navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
@@ -408,10 +408,10 @@
 @navbar-inverse-brand-hover-color:          #fff;
 @navbar-inverse-brand-hover-bg:             transparent;
 
-// Inverted navbar toggle
-@navbar-inverse-toggle-hover-bg:            #333;
-@navbar-inverse-toggle-icon-bar-bg:         #fff;
-@navbar-inverse-toggle-border-color:        #333;
+// Inverted navbar toggle\
+@navbar-inverse-toggle-hover-bg:            transparent;
+@navbar-inverse-toggle-icon-bar-bg:         rgba(0,0,0,0.5);
+@navbar-inverse-toggle-border-color:        transparent;
 
 
 //== Navs
@@ -426,15 +426,15 @@
 @nav-disabled-link-hover-color:             @gray-light;
 
 //== Tabs
-@nav-tabs-border-color:                     #ddd;
+@nav-tabs-border-color:                     transparent;
 
 @nav-tabs-link-hover-border-color:          @gray-lighter;
 
-@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-bg:             transparent;
 @nav-tabs-active-link-hover-color:          @gray;
-@nav-tabs-active-link-hover-border-color:   #ddd;
+@nav-tabs-active-link-hover-border-color:   transparent;
 
-@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-link-border-color:            @nav-tabs-border-color;
 @nav-tabs-justified-active-link-border-color:     @body-bg;
 
 //== Pills
@@ -486,8 +486,8 @@
 
 @jumbotron-padding:              30px;
 @jumbotron-color:                inherit;
-@jumbotron-bg:                   @gray-lighter;
-@jumbotron-heading-color:        inherit;
+@jumbotron-bg:                   #f9f9f9;
+@jumbotron-heading-color:        @headings-color;
 @jumbotron-font-size:            ceil((@font-size-base * 1.5));
 @jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
 
@@ -496,20 +496,20 @@
 //
 //## Define colors for form feedback states and, by default, alerts.
 
-@state-success-text:             #3c763d;
+@state-success-text:             @brand-success;
 @state-success-bg:               #dff0d8;
 @state-success-border:           darken(spin(@state-success-bg, -10), 5%);
 
-@state-info-text:                #31708f;
-@state-info-bg:                  #d9edf7;
+@state-info-text:                @brand-info;
+@state-info-bg:                  #e1bee7;
 @state-info-border:              darken(spin(@state-info-bg, -10), 7%);
 
-@state-warning-text:             #8a6d3b;
-@state-warning-bg:               #fcf8e3;
+@state-warning-text:             @brand-warning;
+@state-warning-bg:               #ffe0b2;
 @state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
 
-@state-danger-text:              #a94442;
-@state-danger-bg:                #f2dede;
+@state-danger-text:              @brand-danger;
+@state-danger-bg:                #f9bdbb;
 @state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
 
 
@@ -522,7 +522,7 @@
 //** Tooltip text color
 @tooltip-color:               #fff;
 //** Tooltip background color
-@tooltip-bg:                  #000;
+@tooltip-bg:                  #727272;
 @tooltip-opacity:             .9;
 
 //** Tooltip arrow width
@@ -540,9 +540,9 @@
 //** Popover maximum width
 @popover-max-width:                   276px;
 //** Popover border color
-@popover-border-color:                rgba(0,0,0,.2);
+@popover-border-color:                transparent;
 //** Popover fallback border color
-@popover-fallback-border-color:       #ccc;
+@popover-fallback-border-color:       transparent;
 
 //** Popover title background color
 @popover-title-bg:                    darken(@popover-bg, 3%);
@@ -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, 5%);
+@popover-arrow-outer-color:           fadein(@popover-border-color, 7.5%);
 //** Popover outer arrow fallback color
 @popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
 
@@ -598,7 +598,7 @@
 //** Background color of modal content area
 @modal-content-bg:                             #fff;
 //** Modal content border color
-@modal-content-border-color:                   rgba(0,0,0,.2);
+@modal-content-border-color:                   transparent;
 //** Modal content border color **for IE8**
 @modal-content-fallback-border-color:          #999;
 
@@ -607,7 +607,7 @@
 //** Modal backdrop opacity
 @modal-backdrop-opacity:      .5;
 //** Modal header border color
-@modal-header-border-color:   #e5e5e5;
+@modal-header-border-color:   transparent;
 //** Modal footer border color
 @modal-footer-border-color:   @modal-header-border-color;
 
@@ -720,21 +720,21 @@
 @panel-primary-border:        @brand-primary;
 @panel-primary-heading-bg:    @brand-primary;
 
-@panel-success-text:          @state-success-text;
+@panel-success-text:          #fff;
 @panel-success-border:        @state-success-border;
-@panel-success-heading-bg:    @state-success-bg;
+@panel-success-heading-bg:    @brand-success;
 
-@panel-info-text:             @state-info-text;
+@panel-info-text:             #fff;
 @panel-info-border:           @state-info-border;
-@panel-info-heading-bg:       @state-info-bg;
+@panel-info-heading-bg:       @brand-info;
 
-@panel-warning-text:          @state-warning-text;
+@panel-warning-text:          #fff;
 @panel-warning-border:        @state-warning-border;
-@panel-warning-heading-bg:    @state-warning-bg;
+@panel-warning-heading-bg:    @brand-warning;
 
-@panel-danger-text:           @state-danger-text;
+@panel-danger-text:           #fff;
 @panel-danger-border:         @state-danger-border;
-@panel-danger-heading-bg:     @state-danger-bg;
+@panel-danger-heading-bg:     @brand-danger;
 
 
 //== Thumbnails
@@ -760,8 +760,8 @@
 //
 //##
 
-@well-bg:                     #f5f5f5;
-@well-border:                 darken(@well-bg, 7%);
+@well-bg:                     #f9f9f9;
+@well-border:                 transparent;
 
 
 //== Badges
@@ -778,7 +778,7 @@
 //** Badge background color in active nav link
 @badge-active-bg:             #fff;
 
-@badge-font-weight:           bold;
+@badge-font-weight:           normal;
 @badge-line-height:           1;
 @badge-border-radius:         10px;
 
@@ -820,9 +820,9 @@
 //
 //##
 
-@close-font-weight:           bold;
+@close-font-weight:           normal;
 @close-color:                 #000;
-@close-text-shadow:           0 1px 0 #fff;
+@close-text-shadow:           none;
 
 
 //== Code

From 3ce9d0ac2fce36cf38a7d56c1198da0bea2ab6a2 Mon Sep 17 00:00:00 2001
From: psychobunny 
Date: Wed, 16 Dec 2015 17:39:21 -0500
Subject: [PATCH 41/59] fixed ordered lists in post content

---
 public/less/mixins.less | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/public/less/mixins.less b/public/less/mixins.less
index c7d49d31c0..a96acc6c84 100644
--- a/public/less/mixins.less
+++ b/public/less/mixins.less
@@ -69,6 +69,10 @@
 
 		margin-bottom: 10px;
 	}
+
+	ol {
+		margin-bottom: 10px;
+	}
 }
 
 .user-icon-style(@size: 32px, @font-size: 1.5rem, @border-radius: inherit) {

From 065bb8ebc4dd8a67f8be6840a1e1e8993132b7a5 Mon Sep 17 00:00:00 2001
From: Julian Lam 
Date: Thu, 17 Dec 2015 09:35:41 -0500
Subject: [PATCH 42/59] #3962

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 295ff26a0c..1d14bde1de 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
     "mkdirp": "~0.5.0",
     "morgan": "^1.3.2",
     "nconf": "~0.8.2",
-    "nodebb-plugin-composer-default": "1.0.24",
+    "nodebb-plugin-composer-default": "1.0.25",
     "nodebb-plugin-dbsearch": "0.3.0",
     "nodebb-plugin-emoji-extended": "0.4.17",
     "nodebb-plugin-markdown": "4.0.9",

From bdeb97e225e0b87b89fe529a18495694f0bd30af Mon Sep 17 00:00:00 2001
From: Julian Lam 
Date: Thu, 17 Dec 2015 09:40:53 -0500
Subject: [PATCH 43/59] updating composer minver again

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1d14bde1de..44b1847559 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
     "mkdirp": "~0.5.0",
     "morgan": "^1.3.2",
     "nconf": "~0.8.2",
-    "nodebb-plugin-composer-default": "1.0.25",
+    "nodebb-plugin-composer-default": "1.0.26",
     "nodebb-plugin-dbsearch": "0.3.0",
     "nodebb-plugin-emoji-extended": "0.4.17",
     "nodebb-plugin-markdown": "4.0.9",

From 0af11c5c0c08e226ea155e84d3a8e09866ee7ca4 Mon Sep 17 00:00:00 2001
From: Julian Lam 
Date: Thu, 17 Dec 2015 14:01:08 -0500
Subject: [PATCH 44/59] fallbacks for new notification groupings

---
 public/language/ar/notifications.json        | 10 ++++++++++
 public/language/bg/notifications.json        | 10 ++++++++++
 public/language/bn/notifications.json        | 10 ++++++++++
 public/language/cs/notifications.json        | 10 ++++++++++
 public/language/da/notifications.json        | 10 ++++++++++
 public/language/de/notifications.json        | 10 ++++++++++
 public/language/el/notifications.json        | 10 ++++++++++
 public/language/en@pirate/notifications.json | 10 ++++++++++
 public/language/en_US/notifications.json     | 10 ++++++++++
 public/language/es/notifications.json        | 10 ++++++++++
 public/language/et/notifications.json        | 10 ++++++++++
 public/language/fa_IR/notifications.json     | 10 ++++++++++
 public/language/fi/notifications.json        | 10 ++++++++++
 public/language/fr/notifications.json        | 10 ++++++++++
 public/language/gl/notifications.json        | 10 ++++++++++
 public/language/he/notifications.json        | 10 ++++++++++
 public/language/hu/notifications.json        | 10 ++++++++++
 public/language/id/notifications.json        | 10 ++++++++++
 public/language/it/notifications.json        | 10 ++++++++++
 public/language/ja/notifications.json        | 10 ++++++++++
 public/language/ko/notifications.json        | 10 ++++++++++
 public/language/lt/notifications.json        | 10 ++++++++++
 public/language/ms/notifications.json        | 10 ++++++++++
 public/language/nb/notifications.json        | 10 ++++++++++
 public/language/nl/notifications.json        | 14 ++++++++++++--
 public/language/pl/notifications.json        | 10 ++++++++++
 public/language/pt_BR/notifications.json     | 10 ++++++++++
 public/language/ro/notifications.json        | 10 ++++++++++
 public/language/ru/notifications.json        | 10 ++++++++++
 public/language/rw/notifications.json        | 10 ++++++++++
 public/language/sc/notifications.json        | 10 ++++++++++
 public/language/sk/notifications.json        | 10 ++++++++++
 public/language/sl/notifications.json        | 10 ++++++++++
 public/language/sr/notifications.json        | 10 ++++++++++
 public/language/sv/notifications.json        | 10 ++++++++++
 public/language/th/notifications.json        | 10 ++++++++++
 public/language/tr/notifications.json        | 10 ++++++++++
 public/language/vi/notifications.json        | 10 ++++++++++
 public/language/zh_CN/notifications.json     | 10 ++++++++++
 public/language/zh_TW/notifications.json     | 10 ++++++++++
 40 files changed, 402 insertions(+), 2 deletions(-)

diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json
index 04fb977999..76b34aa3b1 100644
--- a/public/language/ar/notifications.json
+++ b/public/language/ar/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "لديك تنبيهات غير مقروءة.",
     "new_message_from": "رسالة جديدة من %1",
     "upvoted_your_post_in": "%1 أضاف صوتًا إيجابيا إلى مشاركتك في %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 أضاف مشاركتك في %2 إلى مفضلته.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 أشعَرَ بمشاركة مخلة في %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 أضاف ردا إلى: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 أنشأ موضوعًا جديدًا: %2",
     "user_started_following_you": "%1 صار يتابعك.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "تم التحقق من عنوان البريد الإلكتروني",
     "email-confirmed-message": "شكرًا على إثبات صحة عنوان بريدك الإلكتروني. صار حسابك مفعلًا بالكامل.",
diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json
index 960c14058d..900cc77153 100644
--- a/public/language/bg/notifications.json
+++ b/public/language/bg/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Имате непрочетени известия",
     "new_message_from": "Ново съобщение от %1",
     "upvoted_your_post_in": "%1 гласува положително за Ваша публикация в %2.",
+    "upvoted_your_post_in_dual": "%1 и %2 гласуваха положително за Ваша публикация в %3.",
+    "upvoted_your_post_in_multiple": "%1 и %2 други гласуваха положително за Ваша публикация в %3.",
     "moved_your_post": "%1 премести публикацията Ви в %2",
     "moved_your_topic": "%1 премести %2",
     "favourited_your_post_in": "%1 отбеляза Ваша публикация в %2 като любима.",
+    "favourited_your_post_in_dual": "%1 и %2 отбелязаха Ваша публикация в %3 като любима.",
+    "favourited_your_post_in_multiple": "%1 и %2 други отбелязаха Ваша публикация в %3 като любима.",
     "user_flagged_post_in": "%1 докладва Ваша публикация в %2",
+    "user_flagged_post_in_dual": "%1 и %2 докладваха Ваша публикация в %3",
+    "user_flagged_post_in_multiple": "%1 и %2 други докладваха Ваша публикация в %3",
     "user_posted_to": "%1 публикува отговор на: %2",
+    "user_posted_to_dual": "%1 и %2 публикуваха отговори на: %3",
+    "user_posted_to_multiple": "%1 и %2 други публикуваха отговори на: %3",
     "user_posted_topic": "%1 публикува нова тема: %2",
     "user_started_following_you": "%1 започна да Ви следва.",
+    "user_started_following_you_dual": "%1 и %2 започнаха да Ви следват.",
+    "user_started_following_you_multiple": "%1 и %2 започнаха да Ви следват.",
     "new_register": "%1 изпрати заявка за регистрация.",
     "email-confirmed": "Е-пощата беше потвърдена",
     "email-confirmed-message": "Благодарим Ви, че потвърдихте е-пощата си. Акаунтът Ви е вече напълно активиран.",
diff --git a/public/language/bn/notifications.json b/public/language/bn/notifications.json
index 887c120059..67900e08bf 100644
--- a/public/language/bn/notifications.json
+++ b/public/language/bn/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "আপনার অপঠিত বিজ্ঞপ্তি আছে।",
     "new_message_from": "%1 থেকে নতুন বার্তা",
     "upvoted_your_post_in": "%1 , %2 এ আপানার পোষ্টকে আপভোট করেছেন। ",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 একটি উত্তর দিয়েছেন: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 আপনাকে অনুসরন করা শুরু করেছেন।",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "ইমেইল নিশ্চিত করা হয়েছে",
     "email-confirmed-message": "আপনার ইমেইল যাচাই করার জন্য আপনাকে ধন্যবাদ। আপনার অ্যাকাউন্টটি এখন সম্পূর্ণরূপে সক্রিয়।",
diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json
index 1d9b507b33..d8583da969 100644
--- a/public/language/cs/notifications.json
+++ b/public/language/cs/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "You have unread notifications.",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/da/notifications.json b/public/language/da/notifications.json
index 0952b1eab7..af2a4476f2 100644
--- a/public/language/da/notifications.json
+++ b/public/language/da/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Du har ulæste notifikationer.",
     "new_message_from": "Ny besked fra %1",
     "upvoted_your_post_in": "%1 har upvotet dit indlæg i %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 har flyttet dit indlæg til %2",
     "moved_your_topic": "%1 har flyttet %2",
     "favourited_your_post_in": "%1 har favoriseret dit indlæg i %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 har anmeldt et indlæg i %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 har skrevet et svar til: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 har oprettet en ny tråd: %2",
     "user_started_following_you": "%1 har valgt at følge dig.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 har sendt en registrerings anmodning.",
     "email-confirmed": "Email bekræftet",
     "email-confirmed-message": "Tak fordi du validerede din email. Din konto er nu fuldt ud aktiveret.",
diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json
index 175a55e39f..073425aad6 100644
--- a/public/language/de/notifications.json
+++ b/public/language/de/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Du hast ungelesene Benachrichtigungen.",
     "new_message_from": "Neue Nachricht von %1",
     "upvoted_your_post_in": "%1 hat deinen Beitrag in %2 positiv bewertet.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 hat deinen Beitrag nach %2 verschoben.",
     "moved_your_topic": "%1 hat %2 verschoben.",
     "favourited_your_post_in": "%1 hat deinen Beitrag in %2 favorisiert.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 hat einen Beitrag in %2 gemeldet",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 hat auf %2 geantwortet.",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 hat ein neues Thema erstellt: %2",
     "user_started_following_you": "%1 folgt dir jetzt.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 hat eine Registrationsanfrage geschickt.",
     "email-confirmed": "E-Mail bestätigt",
     "email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.",
diff --git a/public/language/el/notifications.json b/public/language/el/notifications.json
index 5d0447dea8..0ef1347183 100644
--- a/public/language/el/notifications.json
+++ b/public/language/el/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Έχεις μη αναγνωσμένες ειδοποιήσεις.",
     "new_message_from": "Νέο μήνυμα από τον/την %1",
     "upvoted_your_post_in": "Ο/Η %1 υπερψήφισε την δημοσίευσή σου στο %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "Η δημοσίευσή σου στο %2 αρέσει στον/ην %1.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "Ο/Η %1 επεσήμανε μια δημοσίευσή σου στο %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "Ο/Η %1 έγραψε μια απάντηση στο: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "Ο/Η %1 σε ακολουθεί.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Το Εmail Επιβεβαιώθηκε",
     "email-confirmed-message": "Ευχαριστούμε που επιβεβαίωσες το email σου. Ο λογαριασμός σου είναι πλέον πλήρως ενεργοποιημένος.",
diff --git a/public/language/en@pirate/notifications.json b/public/language/en@pirate/notifications.json
index e6986f0684..3efe38a794 100644
--- a/public/language/en@pirate/notifications.json
+++ b/public/language/en@pirate/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "You have unread notifications.",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/en_US/notifications.json b/public/language/en_US/notifications.json
index 1d5a798c26..f7f6f404a8 100644
--- a/public/language/en_US/notifications.json
+++ b/public/language/en_US/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "You have unread notifications.",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favorited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json
index c9e5798685..506f9f83c4 100644
--- a/public/language/es/notifications.json
+++ b/public/language/es/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Tienes notificaciones sin leer.",
     "new_message_from": "Nuevo mensaje de %1",
     "upvoted_your_post_in": "%1 ha votado positivamente tu respuesta en %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 su tema ha sido movido a %2",
     "moved_your_topic": "%1 se ha movido %2",
     "favourited_your_post_in": "%1 ha marcado como favorito su publicación en %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 ha reportado una respuesta en %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 ha respondido a: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 ha publicado un nuevo tema: %2",
     "user_started_following_you": "%1 comenzó a seguirte.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 envió una solicitud de registro.",
     "email-confirmed": "Correo electrónico confirmado",
     "email-confirmed-message": "Gracias por validar tu correo electrónico. Tu cuenta ya está completamente activa.",
diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json
index beaf377781..9f5bc10fd8 100644
--- a/public/language/et/notifications.json
+++ b/public/language/et/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Sul ei ole lugemata teateid.",
     "new_message_from": "Uus sõnum kasutajalt %1",
     "upvoted_your_post_in": "%1 hääletas sinu postituse poolt  teemas %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 märgistas sinu postituse lemmikuks teemas %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 raporteeris postitust %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "Kasutaja %1 postitas vastuse teemasse %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 on postitanud uue teema: %2",
     "user_started_following_you": "%1 hakkas sind jälgima.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 saatis registreerimistaotluse.",
     "email-confirmed": "Emaili aadress kinnitatud",
     "email-confirmed-message": "Täname, et kinnitasite oma emaili aadressi. Teie kasutaja on nüüd täielikult aktiveeritud.",
diff --git a/public/language/fa_IR/notifications.json b/public/language/fa_IR/notifications.json
index 276db73036..7ca96bd4e6 100644
--- a/public/language/fa_IR/notifications.json
+++ b/public/language/fa_IR/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "شما آگاه‌سازی‌های نخوانده دارید.",
     "new_message_from": "پیام تازه از %1",
     "upvoted_your_post_in": "%1 امتیاز مثبت به دیدگاه شما در %2 داده",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 پست شما را به %2 انتقال داده است",
     "moved_your_topic": "%2 %1 را منتقل کرده است",
     "favourited_your_post_in": "%1 دیدگاه شما را در %2 برگزیده کرده.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 دیدگاه شما را در %2 علامتدار کرده",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "پاسخ دادن به %2 از سوی %1",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 یک جستار جدید ارسال کرده: %2",
     "user_started_following_you": "%1 شروع به دنبال کردن شما کرده",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 یک درخواست ثبت نام ارسال کرده است",
     "email-confirmed": "رایانامه تایید شد",
     "email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.",
diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json
index f3c309f82b..377ea60e91 100644
--- a/public/language/fi/notifications.json
+++ b/public/language/fi/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Sinulla on lukemattomia ilmoituksia.",
     "new_message_from": "Uusi viesti käyttäjältä %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 on vastannut viestiin: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 on kirjoittanut uuden aiheen: %2",
     "user_started_following_you": "%1 alkoi seurata sinua.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Sähköpostiosoite vahvistettu",
     "email-confirmed-message": "Kiitos sähköpostiosoitteesi vahvistamisesta. Käyttäjätilisi on nyt täysin aktivoitu.",
diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json
index a15584d512..d66e72dc24 100644
--- a/public/language/fr/notifications.json
+++ b/public/language/fr/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Vous avez des notifications non-lues",
     "new_message_from": "Nouveau message de %1",
     "upvoted_your_post_in": "%1 a voté pour votre message dans %2.",
+    "upvoted_your_post_in_dual": "%1 et %2 ont voté pour votre message dans %3.",
+    "upvoted_your_post_in_multiple": "%1 et %2 autres on voté pour votre message dans %3.",
     "moved_your_post": "%1 a déplacé votre message vers %2",
     "moved_your_topic": "%1 a déplacé %2.",
     "favourited_your_post_in": "%1 a mis votre message en favoris dans %2.",
+    "favourited_your_post_in_dual": "%1 et %2 ont mis votre message dans %3 en favori.",
+    "favourited_your_post_in_multiple": "%1 et %2 autres on mis votre message en favori %3.",
     "user_flagged_post_in": "%1 a signalé un message dans %2.",
+    "user_flagged_post_in_dual": "%1 et %2 ont signalé un message dans %3",
+    "user_flagged_post_in_multiple": "%1 et %2 autres on signalé un message dans %3",
     "user_posted_to": "%1 a répondu à : %2",
+    "user_posted_to_dual": "%1 et %2 ont posté une réponse à : %3",
+    "user_posted_to_multiple": "%1 et %2 autres ont posté une réponse à : %3",
     "user_posted_topic": "%1 a posté un nouveau sujet: %2.",
     "user_started_following_you": "%1 vous suit.",
+    "user_started_following_you_dual": "%1 et %2 vous suivent.",
+    "user_started_following_you_multiple": "%1 et %2 autres vous suivent.",
     "new_register": "%1 a envoyé une demande d'incription.",
     "email-confirmed": "Email vérifié",
     "email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.",
diff --git a/public/language/gl/notifications.json b/public/language/gl/notifications.json
index 17547336f7..5c91bd8677 100644
--- a/public/language/gl/notifications.json
+++ b/public/language/gl/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Tes notificacións non lidas",
     "new_message_from": "Nova mensaxe de %1",
     "upvoted_your_post_in": "%1 votoute positivo en %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 moveu a túa publicación a%2",
     "moved_your_topic": "%1 foi movido %2",
     "favourited_your_post_in": "%1 marcou favorita a túa publicación en %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 reportou unha mensaxe en %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 publicou unha resposta en: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 publicou un novo tema: %2",
     "user_started_following_you": "%1 comezou a seguirte.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 enviou unha petición de rexistro.",
     "email-confirmed": "Correo confirmado",
     "email-confirmed-message": "Grazas por validar o teu correo. A túa conta agora está activada.",
diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json
index 6d93c08512..49d1b54c31 100644
--- a/public/language/he/notifications.json
+++ b/public/language/he/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "יש לך התראות שלא נקראו.",
     "new_message_from": "הודעה חדשה מ %1",
     "upvoted_your_post_in": "%1 הצביע בעד הפוסט שלך ב %2",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 הוסיף את הפוסט שלך ב %2 למועדפים שלו.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 דיווח על פוסט ב %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 פרסם תגובה ל: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 העלה נושא חדש: %2",
     "user_started_following_you": "%1 התחיל לעקוב אחריך.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "כתובת המייל אושרה",
     "email-confirmed-message": "תודה שאישרת את כתובת המייל שלך. החשבון שלך פעיל כעת.",
diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json
index 643037bd28..f255352a1a 100644
--- a/public/language/hu/notifications.json
+++ b/public/language/hu/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "You have unread notifications.",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/id/notifications.json b/public/language/id/notifications.json
index 296bfd9c2a..7333364bd9 100644
--- a/public/language/id/notifications.json
+++ b/public/language/id/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Kamu memiliki pemberitahuan yang belum dibaca.",
     "new_message_from": "Pesan baru dari %1",
     "upvoted_your_post_in": "%1 telah melakukan upvote untuk posting kamu di %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 telah memfavoritkan posting mu di %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 menandai sebuah posting di %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 telah mengirim sebuah balasan kepada: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 telah membuat topik baru: %2",
     "user_started_following_you": "%1 mulai mengikutimu.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 mengirim permintaan registrasi.",
     "email-confirmed": "Email telah Dikonfirmasi",
     "email-confirmed-message": "Terimakasih telah melakukan validasi email. Akunmu saat ini telah aktif sepenuhnya.",
diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json
index 15ca49f2ec..e5d6625d0e 100644
--- a/public/language/it/notifications.json
+++ b/public/language/it/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Hai notifiche non lette.",
     "new_message_from": "Nuovo messaggio da %1",
     "upvoted_your_post_in": "%1 ha votato positivamente il tuo post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 ha messo nei favoriti il tuo post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 ha segnalato un post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 ha postato una risposta a: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 ha postato un nuovo Topic: %2",
     "user_started_following_you": "%1 ha iniziato a seguirti.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 ha inviato una richiesta di registrazione.",
     "email-confirmed": "Email Confermata",
     "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.",
diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json
index e5f9facc16..963dbc3e6e 100644
--- a/public/language/ja/notifications.json
+++ b/public/language/ja/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "未読の通知があります。",
     "new_message_from": "%1からの新しいメッセージ",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1%2 への返事を作成しました。",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json
index e7d56db47b..97dcfc3b64 100644
--- a/public/language/ko/notifications.json
+++ b/public/language/ko/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "읽지 않은 알림이 있습니다.",
     "new_message_from": "%1님이 메시지를 보냈습니다.",
     "upvoted_your_post_in": "%1님이 %2의 내 게시물을 추천했습니다.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1님이 %2의 내 게시물을 좋아합니다.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1님이 %2의 게시물을 신고했습니다.",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1님이 %2에 답글을 작성했습니다.",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1님이 새 주제를 작성했습니다: %2",
     "user_started_following_you": "%1님이 나를 팔로우합니다.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "확인된 이메일",
     "email-confirmed-message": "이메일을 확인해주셔서 감사합니다. 계정이 완전히 활성화되었습니다.",
diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json
index 596993bb92..4bbd1e6fae 100644
--- a/public/language/lt/notifications.json
+++ b/public/language/lt/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Jūs turite neperskaitytų pranešimų.",
     "new_message_from": "Nauja žinutė nuo %1",
     "upvoted_your_post_in": "%1  užbalsavo už jūsų pranešima čia  %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 patinka jūsų pranešimas čia %2",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1pagrįso nuomone čia %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 parašė atsaką %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 paskelbė naują temą: %2",
     "user_started_following_you": "%1 pradėjo sekti tave",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 atsiuntė registracijos prašymą",
     "email-confirmed": "El. paštas patvirtintas",
     "email-confirmed-message": "Dėkojame už el. pašto patvirtinimą. Jūsų paskyra pilnai aktyvuota.",
diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json
index 34c1f104a0..196e3971ca 100644
--- a/public/language/ms/notifications.json
+++ b/public/language/ms/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Ada pemberitahuan yang belum dibaca",
     "new_message_from": "Pesanan baru daripada  %1",
     "upvoted_your_post_in": "%1 telah mengundi naik kiriman and di %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 telah memindahkan kiriman anda ke %2",
     "moved_your_topic": "%1 telah memindahkan %2",
     "favourited_your_post_in": "%1 menggemari kiriman and di %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 menanda kiriman anda di %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 telah membalas kiriman kepada: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 membuka topik baru : %2",
     "user_started_following_you": "%1 mula mengikut anda.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 menghantar jemputan pendaftaran.",
     "email-confirmed": "Emel Disahkan",
     "email-confirmed-message": "Terima kasih kerana mengesahkan emel anda. Akaun anda telah diaktifkan sepenuhnya.",
diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json
index 425997b22f..568357137a 100644
--- a/public/language/nb/notifications.json
+++ b/public/language/nb/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Du har uleste varsler.",
     "new_message_from": "Ny melding fra %1",
     "upvoted_your_post_in": "%1 har stemt opp innlegget ditt i %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 har favorittmerket innlegget ditt i %2",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 har flagget et innlegg i %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 har skrevet et svar til: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 har skrevet et nytt emne: %2",
     "user_started_following_you": "%1 begynte å følge deg.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sendte en forespørsel om registrering",
     "email-confirmed": "E-post bekreftet",
     "email-confirmed-message": "Takk for at du har validert din e-post. Kontoen din er nå fullstendig aktivert.",
diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json
index f049f78b92..d2c8ec0058 100644
--- a/public/language/nl/notifications.json
+++ b/public/language/nl/notifications.json
@@ -9,19 +9,29 @@
     "continue_to": "Door naar %1",
     "return_to": "Terug naar %1",
     "new_notification": "Nieuwe melding",
-    "you_have_unread_notifications": "Ongelezen berichten",
+    "you_have_unread_notifications": "Je hebt nieuwe notificaties.",
     "new_message_from": "Nieuw bericht van %1",
     "upvoted_your_post_in": "%1 heeft voor een bericht gestemd in %2.",
+    "upvoted_your_post_in_dual": "%1 en %2 hebben voor een bericht in gestemd in %3.",
+    "upvoted_your_post_in_multiple": "%1 en %2 andere hebben in gestemd in %3.",
     "moved_your_post": "%1 heeft je bericht verplaatst naar %2",
     "moved_your_topic": "%1 heeft %2 verplaatst",
     "favourited_your_post_in": "%1 heeft een van je berichten in %2 aan zijn of haar favorieten toegevoegd.",
+    "favourited_your_post_in_dual": "%1 en %2 hebben een van je berichten in %3 aan zijn of haar favorieten toegevoegd.",
+    "favourited_your_post_in_multiple": "%1 en %2 hebben een van je berichten in 3 aan hun favorieten toegevoegd.",
     "user_flagged_post_in": "%1 rapporteerde een bericht in %2",
+    "user_flagged_post_in_dual": "%1 en %2 rapporteerde een bericht in %3",
+    "user_flagged_post_in_multiple": "%1 en %2 andere rapporteede een bericht in %3",
     "user_posted_to": "%1 heeft een reactie geplaatst in %2",
+    "user_posted_to_dual": "%1 en %2 hebben een reactie geplaatst in: %3",
+    "user_posted_to_multiple": "%1 en %2 hebben een reactie geplaatst in: %3",
     "user_posted_topic": "%1 heeft een nieuw onderwerp geplaatst: %2",
     "user_started_following_you": "%1 volgt jou nu.",
+    "user_started_following_you_dual": "%1 en %2 volgen jou nu.",
+    "user_started_following_you_multiple": "%1 en %2 andere volgen jou nu.",
     "new_register": "%1 heeft een registratie verzoek aangevraagd.",
     "email-confirmed": "E-mailadres bevestigd",
     "email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Dit account is nu volledig geactiveerd.",
     "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.",
-    "email-confirm-sent": "Bevestigingsmail verstuurd"
+    "email-confirm-sent": "Bevestigingsmail verstuurd."
 }
\ No newline at end of file
diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json
index e655a6253a..e34cb43dc6 100644
--- a/public/language/pl/notifications.json
+++ b/public/language/pl/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Masz nieprzeczytane powiadomienia.",
     "new_message_from": "Nowa wiadomość od %1",
     "upvoted_your_post_in": "%1 zagłosował na Twój post w %2",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 polubił Twój post w %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 oflagował Twój post w %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 dodał odpowiedź do %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 wysłał nowy temat: %2",
     "user_started_following_you": "%1 zaczął Cię śledzić.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 wysłał żądanie rejestracji.",
     "email-confirmed": "E-mail potwierdzony",
     "email-confirmed-message": "Dziękujemy za potwierdzenie maila. Twoje konto zostało aktywowane.",
diff --git a/public/language/pt_BR/notifications.json b/public/language/pt_BR/notifications.json
index 7a3b36d8d6..58d959e039 100644
--- a/public/language/pt_BR/notifications.json
+++ b/public/language/pt_BR/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Você possui notificações não lidas.",
     "new_message_from": "Nova mensagem de %1",
     "upvoted_your_post_in": "%1 deu voto positivo para seu post em %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 moveu seu post para %2",
     "moved_your_topic": "%1 movido %2",
     "favourited_your_post_in": "%1 adicionou seu tópico aos favoritos em %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 sinalizou um post em %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 postou uma resposta para: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 postou um novo tópico: %2",
     "user_started_following_you": "%1 começou a seguir você.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 lhe enviou um pedido de cadastro.",
     "email-confirmed": "Email Confirmado",
     "email-confirmed-message": "Obrigado por validar o seu email. Agora sua conta está plenamente ativada.",
diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json
index a7cc002529..a37dac936b 100644
--- a/public/language/ro/notifications.json
+++ b/public/language/ro/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Ai notificări necitite.",
     "new_message_from": "Un mesaj nou de la %1",
     "upvoted_your_post_in": "%1 a votat pozitiv mesajul tău în %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 a adăugat mesajul tău la favorite în %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 a semnalizat un mesaj în %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 a postat un răspuns la: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 a început să te urmărească.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email confirmat",
     "email-confirmed-message": "Îți mulțumim pentru validarea emailului. Contul tău este acuma activat.",
diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json
index 24be55d9ea..197bfa2c09 100644
--- a/public/language/ru/notifications.json
+++ b/public/language/ru/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "У вас есть непрочитанные уведомления.",
     "new_message_from": "Новое сообщение от %1",
     "upvoted_your_post_in": "%1 проголосовал за Ваше сообщение в %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 добавил в избранное Ваше сообщение в %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 пометил сообщение в %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 ответил на запись: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 открыл новую тему: %2",
     "user_started_following_you": "%1 подписался на Вас.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 отправлен запрос на регистрацию.",
     "email-confirmed": "Email подтвержден",
     "email-confirmed-message": "Спасибо за подтверждение Вашего Email-адреса. Ваш аккаунт активирован.",
diff --git a/public/language/rw/notifications.json b/public/language/rw/notifications.json
index a9d10be6e2..64f4c98753 100644
--- a/public/language/rw/notifications.json
+++ b/public/language/rw/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Ufite amatangazo utarasoma. ",
     "new_message_from": " %1 yakwandikiye",
     "upvoted_your_post_in": "%1 yagushimye aguha inota kuri %2 washyizeho.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 yatonesheje %2 washyizeho.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 yatambikanye ikintu muri %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 yanditse kuri: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 yatangije ikiganiro gishya: %2",
     "user_started_following_you": "%1 yatangiye kugukurikira.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 yasabye kwandikwa.",
     "email-confirmed": "Email Yemejwe",
     "email-confirmed-message": "Urakoze kugaragaza ko email yawe ikora. Ubu ngubu konte yawe irakora nta kabuza. ",
diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json
index 929f5a55c6..b9d09d0159 100644
--- a/public/language/sc/notifications.json
+++ b/public/language/sc/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "You have unread notifications.",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email Confirmed",
     "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json
index a8e6619d33..5b62d0317e 100644
--- a/public/language/sk/notifications.json
+++ b/public/language/sk/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Máte neprečítané notifikácie",
     "new_message_from": "Nova spáva od %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 odpovedal: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email bol potvrdený",
     "email-confirmed-message": "Ďakujeme za potvrdenie tvojho emailu. Účet je plne aktivovaný.",
diff --git a/public/language/sl/notifications.json b/public/language/sl/notifications.json
index b0164f086e..6554627f8f 100644
--- a/public/language/sl/notifications.json
+++ b/public/language/sl/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Imate neprebrana obvestila.",
     "new_message_from": "Novo obvestilo od %1",
     "upvoted_your_post_in": "%1 je glasoval za vašo objavo v %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 je dodal med priljubljene vašo objavo v %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1je prijavil vašo objavo v %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 je odgovoril na: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 je odprl novo temo: %2",
     "user_started_following_you": "%1 te sledi.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 je poslal prošnjo za registracijo.",
     "email-confirmed": "E-mail naslov potrjen",
     "email-confirmed-message": "Hvala ker ste potrdili svoj naslov. Račun je sedaj aktiviran.",
diff --git a/public/language/sr/notifications.json b/public/language/sr/notifications.json
index e65b13f46d..79d715bae4 100644
--- a/public/language/sr/notifications.json
+++ b/public/language/sr/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Имате непрочитаних обавештења.",
     "new_message_from": "Нова порука од %1",
     "upvoted_your_post_in": "%1 дода глас вашој поруци у %2",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 доду вашу поруку из %2 у омиљене.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 означи поруку у %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 посла нови одговор за: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 постави нову тему:",
     "user_started_following_you": "%1 поче да вас прати.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Е-пошта је је отврђена.",
     "email-confirmed-message": "Хвала на овери ваше е-поште. Ваш налог је сада у потпуности активан.",
diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json
index dbf4d290c6..dcb54b72ab 100644
--- a/public/language/sv/notifications.json
+++ b/public/language/sv/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Du har olästa notiser.",
     "new_message_from": "Nytt medelande från %1",
     "upvoted_your_post_in": "%1 har röstat upp ditt inlägg i %2",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 har favoriserat ditt inlägg i %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flaggade ett inlägg i %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 har skrivit ett svar på: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 har skapat ett nytt ämne: %2",
     "user_started_following_you": "%1 började följa dig.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 skickade en registreringsförfrågan.",
     "email-confirmed": "Epost bekräftad",
     "email-confirmed-message": "Tack för att du bekräftat din epostadress. Ditt konto är nu fullt ut aktiverat.",
diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json
index 34fc870629..14fd6afc25 100644
--- a/public/language/th/notifications.json
+++ b/public/language/th/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "คุณมีคำเตือนที่ยังไม่ได้อ่าน",
     "new_message_from": "New message from %1",
     "upvoted_your_post_in": "%1 has upvoted your post in %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 has favourited your post in %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 flagged a post in %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 has posted a reply to: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 has posted a new topic: %2",
     "user_started_following_you": "%1 started following you.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "Email ได้รับการยืนยันแล้ว",
     "email-confirmed-message": "ขอบคุณที่ยืนยัน Email ของคุณ บัญชีของคุณสามารถใช้งานได้แล้ว",
diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json
index b0eb25ec97..b69d4c980f 100644
--- a/public/language/tr/notifications.json
+++ b/public/language/tr/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Okunmamış bildirimleriniz var.",
     "new_message_from": "%1 size bir mesaj gönderdi",
     "upvoted_your_post_in": "%1 iletinizi beğendi. %2",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 senin iletin %2 taşındı",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 iletinizi favorilerine ekledi. %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 bir iletiyi bayrakladı. %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 %2 başlığına bir ileti gönderdi.",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 yeni bir konu yarattı: %2",
     "user_started_following_you": "%1 sizi takip etmeye başladı.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 kayıt olma isteği gönderdi.",
     "email-confirmed": "E-posta onaylandı",
     "email-confirmed-message": "E-postanızı onaylandığınız için teşekkürler. Hesabınız tamamen aktive edildi.",
diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json
index 533e5d2a53..3d3ca64228 100644
--- a/public/language/vi/notifications.json
+++ b/public/language/vi/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "Bạn có thông báo chưa đọc",
     "new_message_from": "Tin nhắn mới từ %1",
     "upvoted_your_post_in": "%1 đã bình chọn bài của bạn trong %2.",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 đã thích bài của bạn trong %2.",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 gắn cờ 1 bài trong %2",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 đã trả lời %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 đã gởi chủ đề mới ở %2",
     "user_started_following_you": "%1 đã theo dõi bạn.",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "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 đủ.",
diff --git a/public/language/zh_CN/notifications.json b/public/language/zh_CN/notifications.json
index 9589fe7507..5fba6a56e0 100644
--- a/public/language/zh_CN/notifications.json
+++ b/public/language/zh_CN/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "您有未读的通知。",
     "new_message_from": "来自 %1 的新消息",
     "upvoted_your_post_in": "%1%2 点赞了您的帖子。",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "您的帖子已被 %1 移动到了 %2",
     "moved_your_topic": "%1 移动到了 %2",
     "favourited_your_post_in": "%1%2 收藏了您的帖子。",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1%2 标记了一个帖子",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 回复了:%2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 发表了新主题:%2",
     "user_started_following_you": "%1关注了您。",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 发出了注册请求",
     "email-confirmed": "电子邮箱已确认",
     "email-confirmed-message": "感谢您验证您的电子邮箱。您的帐户现已全面激活。",
diff --git a/public/language/zh_TW/notifications.json b/public/language/zh_TW/notifications.json
index 993d2cd3d8..fb2f3baa39 100644
--- a/public/language/zh_TW/notifications.json
+++ b/public/language/zh_TW/notifications.json
@@ -12,13 +12,23 @@
     "you_have_unread_notifications": "您有未讀的訊息!",
     "new_message_from": "來自 %1 的新訊息",
     "upvoted_your_post_in": "%1 upvote了您在 %2的post。",
+    "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.",
+    "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.",
     "moved_your_post": "%1 has moved your post to %2",
     "moved_your_topic": "%1 has moved %2",
     "favourited_your_post_in": "%1 收藏了你在 %2的post。",
+    "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.",
+    "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.",
     "user_flagged_post_in": "%1 舉報了 %2裡的一個post。",
+    "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3",
+    "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3",
     "user_posted_to": "%1 發布一個回覆給: %2",
+    "user_posted_to_dual": "%1 and %2 have posted replies to: %3",
+    "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3",
     "user_posted_topic": "%1 發布了一個新的主題: %2",
     "user_started_following_you": "%1 開始關注你。",
+    "user_started_following_you_dual": "%1 and %2 started following you.",
+    "user_started_following_you_multiple": "%1 and %2 others started following you.",
     "new_register": "%1 sent a registration request.",
     "email-confirmed": "已確認電郵",
     "email-confirmed-message": "感謝您驗證您的電郵。您的帳戶現已全面啟用。",

From ba719148c1ca0b8aa50e28eca843551c67dd94e3 Mon Sep 17 00:00:00 2001
From: psychobunny 
Date: Thu, 17 Dec 2015 16:44:22 -0500
Subject: [PATCH 45/59] generic "related topics" functionality

---
 src/topics.js      |  7 +++++++
 src/topics/tags.js | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/src/topics.js b/src/topics.js
index 560e434fb5..423dafe197 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -192,12 +192,19 @@ var async = require('async'),
 				}, next);
 			},
 			function(results, next) {
+				if (plugins.hasListeners('filter:topic.getRelatedTopics')) {
+					plugins.fireHook('filter:topic.getRelatedTopics', results, next);
+				} else {
+					Topics.getRelatedTopics(results, 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.bookmark = results.bookmark;
+				topicData.related = results.related || [];
 
 				topicData.unreplied = parseInt(topicData.postcount, 10) === 1;
 				topicData.deleted = parseInt(topicData.deleted, 10) === 1;
diff --git a/src/topics/tags.js b/src/topics/tags.js
index bde9beff8f..8d29f947cf 100644
--- a/src/topics/tags.js
+++ b/src/topics/tags.js
@@ -5,6 +5,7 @@ var async = require('async'),
 
 	db = require('../database'),
 	meta = require('../meta'),
+	user = require('../user'),
 	_ = require('underscore'),
 	plugins = require('../plugins');
 
@@ -322,4 +323,41 @@ module.exports = function(Topics) {
 		});
 	};
 
+	Topics.getRelatedTopics = function(topicData, callback) {
+		if (!topicData.tags.length) {
+			return callback(null, topicData);
+		}
+
+		var related = [];
+
+		user.isAdministrator(topicData.threadTools.uid, function(err, isAdministrator) {
+			async.each(topicData.tags, function(tag, next) {
+				tag = tag.value;
+
+				Topics.getTagTids(tag, 0, 5, function(err, tids) {
+					Topics.getTopics(tids, topicData.threadTools.uid, function(err, topics) {
+						related = related.concat(topics.filter(function(topic) {
+							var doesntOwnTopic = parseInt(topic.uid, 10) !== parseInt(topicData.threadTools.uid, 10);
+							var isntSameTopic = parseInt(topic.tid, 10) !== parseInt(topicData.threadTools.topic.tid, 10);
+
+							return doesntOwnTopic && isntSameTopic;
+						}));
+
+						next(err);
+					});
+				});
+			}, function(err) {
+				if (!isAdministrator) {
+					related = related.filter(function(topic) {
+						return topic && !topic.deleted;
+					});
+				}
+
+				related = _.shuffle(related).slice(0, 5);
+
+				topicData.related = related;
+				callback(err, topicData);
+			});
+		});
+	};
 };
\ No newline at end of file

From 339b4c30fcc666843b7cca7d1fa3efa8b50e3379 Mon Sep 17 00:00:00 2001
From: psychobunny 
Date: Thu, 17 Dec 2015 16:52:02 -0500
Subject: [PATCH 46/59] ability to set maximum related topics rendered in ACP

---
 src/topics/tags.js                |  6 ++++--
 src/views/admin/settings/tags.tpl | 11 +++++++++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/topics/tags.js b/src/topics/tags.js
index 8d29f947cf..183dbe903c 100644
--- a/src/topics/tags.js
+++ b/src/topics/tags.js
@@ -324,7 +324,9 @@ module.exports = function(Topics) {
 	};
 
 	Topics.getRelatedTopics = function(topicData, callback) {
-		if (!topicData.tags.length) {
+		var maximumTopics = typeof meta.config.maximumRelatedTopics !== 'undefined' ? parseInt(meta.config.maximumRelatedTopics, 10) : 5;
+
+		if (!topicData.tags.length || maximumTopics === 0) {
 			return callback(null, topicData);
 		}
 
@@ -353,7 +355,7 @@ module.exports = function(Topics) {
 					});
 				}
 
-				related = _.shuffle(related).slice(0, 5);
+				related = _.shuffle(related).slice(0, maximumTopics);
 
 				topicData.related = related;
 				callback(err, topicData);
diff --git a/src/views/admin/settings/tags.tpl b/src/views/admin/settings/tags.tpl
index 6c09dab80e..90aa5ed0bc 100644
--- a/src/views/admin/settings/tags.tpl
+++ b/src/views/admin/settings/tags.tpl
@@ -31,4 +31,15 @@
 	
 
 
+
+
Related Topics
+
+
+
+ + +
+
+
+ \ No newline at end of file From 89901b2caa57efe303b928007b45d1d916f6c3f7 Mon Sep 17 00:00:00 2001 From: psychobunny Date: Thu, 17 Dec 2015 16:53:42 -0500 Subject: [PATCH 47/59] reorganized settings/tags acp --- src/views/admin/settings/tags.tpl | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/views/admin/settings/tags.tpl b/src/views/admin/settings/tags.tpl index 90aa5ed0bc..e1e8f01f1c 100644 --- a/src/views/admin/settings/tags.tpl +++ b/src/views/admin/settings/tags.tpl @@ -4,12 +4,6 @@
Tag Settings
-
- -
@@ -31,6 +25,21 @@
+ +
+
Privacy
+
+ +
+ +
+ +
+
+
Related Topics
@@ -39,6 +48,7 @@
+
From a0910671754b297484adb589e2ff1d8759e304b8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 17 Dec 2015 18:22:03 -0500 Subject: [PATCH 48/59] closes #3963 --- public/src/modules/translator.js | 36 ++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 35429af587..ca9cbb9f29 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -265,6 +265,7 @@ 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'; @@ -275,18 +276,35 @@ } fs.readFile(path.join(__dirname, '../../language', language, filename + '.json'), function(err, data) { - if (err) { - winston.error('Could not load `' + filename + '`: ' + err.message + '. Skipping...'); - return callback({}); + 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); } - try { - data = JSON.parse(data.toString()); - } catch (e) { - winston.error('Could not parse `' + filename + '.json`, syntax error? Skipping...'); - 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({}); + } } - callback(data); + + onData(data); }); } From e067d26ca36508cb724fde59c0a5e91c32dc0a4c Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Dec 2015 13:09:47 +0200 Subject: [PATCH 49/59] closes #3961 --- src/groups/update.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/groups/update.js b/src/groups/update.js index b239df39de..0ac688aca0 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -45,6 +45,7 @@ module.exports = function(Groups) { } async.series([ + async.apply(checkNameChange, groupName, values.name), async.apply(updatePrivacy, groupName, values.private), function(next) { if (values.hasOwnProperty('hidden')) { @@ -217,6 +218,23 @@ module.exports = function(Groups) { }); } + function checkNameChange(oldName, newName, callback) { + if (oldName === newName) { + return callback(); + } + var oldSlug = utils.slugify(oldName); + var newSlug = utils.slugify(newName); + if (oldSlug === newSlug) { + return callback(); + } + Groups.existsBySlug(newSlug, function(err, exists) { + if (err || exists) { + return callback(err || new Error('[[error:group-already-exists]]')); + } + callback(); + }); + } + function renameGroup(oldName, newName, callback) { if (oldName === newName || !newName || newName.length === 0) { return callback(); From 99815550423d0886498cc07c77d9a5e2e5e002fe Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Dec 2015 13:14:44 +0200 Subject: [PATCH 50/59] rename --- src/groups/update.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/groups/update.js b/src/groups/update.js index 0ac688aca0..26d2b258be 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -218,13 +218,13 @@ module.exports = function(Groups) { }); } - function checkNameChange(oldName, newName, callback) { - if (oldName === newName) { + function checkNameChange(currentName, newName, callback) { + if (currentName === newName) { return callback(); } - var oldSlug = utils.slugify(oldName); + var currentSlug = utils.slugify(currentName); var newSlug = utils.slugify(newName); - if (oldSlug === newSlug) { + if (currentSlug === newSlug) { return callback(); } Groups.existsBySlug(newSlug, function(err, exists) { From 270f8130daba7a012ec5310f573a9d2a277a773e Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 18 Dec 2015 13:32:53 +0200 Subject: [PATCH 51/59] closes #3952 --- src/socket.io/groups.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 3b12693be0..0361560f01 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -144,6 +144,9 @@ SocketGroups.update = 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.leave(data.groupName, data.uid, callback); }); From af55f55b453197f862820316bf2428928b880d28 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 18 Dec 2015 12:49:10 -0500 Subject: [PATCH 52/59] latest translations --- public/language/nl/error.json | 2 +- public/language/nl/modules.json | 2 +- public/language/ru/error.json | 2 +- public/language/ru/modules.json | 4 ++-- public/language/ru/notifications.json | 4 ++-- public/language/ru/topic.json | 6 +++--- public/language/ru/user.json | 14 +++++++------- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/public/language/nl/error.json b/public/language/nl/error.json index cf918a18db..bfe113c836 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -78,7 +78,7 @@ "too-many-messages": "Er zijn in korte tijd teveel berichten verzonden, een moment geduld.", "invalid-chat-message": "Ongeldig bericht", "chat-message-too-long": "Het chatbericht is te lang", - "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-edit-chat-message": "Het is niet toegestaan om dit bericht aan te passen", "reputation-system-disabled": "Reputatie systeem is uitgeschakeld.", "downvoting-disabled": "Negatief stemmen staat uitgeschakeld.", "not-enough-reputation-to-downvote": "Dit gebruikersaccount beschikt over onvoldoende reputatie om een negatieve stem uit te mogen brengen.", diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index 75792aec27..39a61ffe9f 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -15,7 +15,7 @@ "chat.seven_days": "7 dagen", "chat.thirty_days": "30 dagen", "chat.three_months": "3 maanden", - "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.delete_message_confirm": "Weet u het zeker dat u dit bericht wilt verwijderen?", "composer.compose": "Samenstellen", "composer.show_preview": "Voorbeeldweergave", "composer.hide_preview": "Verberg voorbeeld", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 717d307f63..4dd3c99934 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -78,7 +78,7 @@ "too-many-messages": "Вы отправили слишком много сообщений, подождите немного.", "invalid-chat-message": "Неверное сообщение чата", "chat-message-too-long": "Слишком длинное сообщение чата", - "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-edit-chat-message": "У вас нет доступа для редактирования этого сообщения", "reputation-system-disabled": "Система репутации отключена.", "downvoting-disabled": "Понижение оценки отключено", "not-enough-reputation-to-downvote": "У Вас недостаточно репутации для понижения оценки сообщения", diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index cada7e8fc9..b9ea06f477 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -15,7 +15,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": "Вы уверены, что хотите удалить это сообщение?", "composer.compose": "Редактор", "composer.show_preview": "Показать предпросмотр", "composer.hide_preview": "Скрыть предпросмотр", @@ -24,7 +24,7 @@ "composer.discard": "Вы уверены, что хотите отменить все изменения?", "composer.submit_and_lock": "Отправить и закрыть", "composer.toggle_dropdown": "Показать выпадающий список", - "composer.uploading": "Uploading %1", + "composer.uploading": "Загрузка %1", "bootbox.ok": "ОК", "bootbox.cancel": "Отмена", "bootbox.confirm": "Подтвердить", diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index 197bfa2c09..9c258affe0 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -15,9 +15,9 @@ "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", "moved_your_post": "%1 has moved your post to %2", - "moved_your_topic": "%1 has moved %2", + "moved_your_topic": "%1 переместил %2", "favourited_your_post_in": "%1 добавил в избранное Ваше сообщение в %2.", - "favourited_your_post_in_dual": "%1 and %2 have favourited your post in %3.", + "favourited_your_post_in_dual": "%1 и %2 добавили в избранное Ваше сообщение в %3.", "favourited_your_post_in_multiple": "%1 and %2 others have favourited your post in %3.", "user_flagged_post_in": "%1 пометил сообщение в %2", "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index b733f7305a..0ba0064291 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -98,10 +98,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", + "stale.create": "Создать новую тему", + "stale.reply_anyway": "Всё равно ответить в этой теме", "stale.link_back": "Re: [%1](%2)", - "spam": "Spam", + "spam": "Спам", "offensive": "Offensive", "custom-flag-reason": "Enter a flagging reason" } \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 83a614e156..d9b7cc6621 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -30,16 +30,16 @@ "signature": "Подпись", "birthday": "День рождения", "chat": "Чат", - "chat_with": "Chat with %1", + "chat_with": "Чат с %1", "follow": "Читать", "unfollow": "Не читать", "more": "Ещё", "profile_update_success": "Профиль обновлен!", "change_picture": "Изменить фотографию", - "change_username": "Change Username", - "change_email": "Change Email", + "change_username": "Изменить имя пользователя", + "change_email": "Изменить Email", "edit": "Редактировать", - "default_picture": "Default Icon", + "default_picture": "Иконка по умолчанию", "uploaded_picture": "Загруженные фотографии", "upload_new_picture": "Загрузить новую фотографию", "upload_new_picture_from_url": "Загрузить новое изображение с адреса URL", @@ -58,7 +58,7 @@ "upload_picture": "Загрузить фотографию", "upload_a_picture": "Загрузить фотографию", "remove_uploaded_picture": "Удалить загруженные фотографии", - "image_spec": "You may only upload PNG, JPG, or BMP files", + "image_spec": "Вы можете загружать только PNG, JPG или BMP файлы", "settings": "Настройки", "show_email": "Показывать мой Email", "show_fullname": "Показывать Полное Имя", @@ -92,8 +92,8 @@ "grouptitle": "Выберите бейдж группы для отображения", "no-group-title": "Не показывать бейдж", "select-skin": "Выбрать скин", - "select-homepage": "Select a Homepage", - "homepage": "Homepage", + "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\")", From 668adc5187dfe09050c12c44f147664bb8221170 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 18 Dec 2015 12:56:24 -0500 Subject: [PATCH 53/59] fixing bootswatch integration --- public/src/admin/appearance/skins.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js index 8f838ca8c2..895b97de52 100644 --- a/public/src/admin/appearance/skins.js +++ b/public/src/admin/appearance/skins.js @@ -5,9 +5,11 @@ define('admin/appearance/skins', function() { var Skins = {}; Skins.init = function() { - var scriptEl = $(' + \ No newline at end of file From b2049e7acf6c7d5cae6288fbdec43c66228d7f11 Mon Sep 17 00:00:00 2001 From: psychobunny Date: Fri, 18 Dec 2015 16:56:59 -0500 Subject: [PATCH 56/59] add color/bgColor to return --- src/controllers/unread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/unread.js b/src/controllers/unread.js index af7bf816ef..741e5c673a 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -34,7 +34,7 @@ unreadController.get = function(req, res, next) { privileges.categories.filterCids('read', results.watchedCategories, req.uid, next); }, function(cids, next) { - categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link'], next); + categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'link', 'color', 'bgColor'], next); }, function(categories, next) { categories = categories.filter(function(category) { From fe90dd77c186b72f4a8e02dd16dbb2b4a28ca7da Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sat, 19 Dec 2015 09:31:03 -0500 Subject: [PATCH 57/59] fixed #3967 --- src/user/notifications.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/user/notifications.js b/src/user/notifications.js index b085e7654d..2d08b83e33 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -64,6 +64,8 @@ var async = require('async'), } function getNotificationsFromSet(set, read, uid, start, stop, callback) { + var setNids; + async.waterfall([ async.apply(db.getSortedSetRevRange, set, start, stop), function(nids, next) { @@ -71,6 +73,7 @@ var async = require('async'), return callback(null, []); } + setNids = nids; UserNotifications.getNotifications(nids, uid, next); }, function(notifs, next) { @@ -78,8 +81,8 @@ var async = require('async'), notifs.forEach(function(notification, index) { if (!notification) { - winston.verbose('[notifications.get] nid ' + notification.nid + ' not found. Removing.'); - deletedNids.push(notification.nid); + winston.verbose('[notifications.get] nid ' + setNids[index] + ' not found. Removing.'); + deletedNids.push(setNids[index]); } else { notification.read = read; notification.readClass = !notification.read ? 'unread' : ''; From 80544119dcc76d19fbcd7f10d21ac2d15d80b6f9 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sat, 19 Dec 2015 17:21:09 +0200 Subject: [PATCH 58/59] some changes to related code --- src/topics.js | 11 ++++------ src/topics/tags.js | 55 +++++++++++++++++++--------------------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/topics.js b/src/topics.js index 423dafe197..d2c4c79359 100644 --- a/src/topics.js +++ b/src/topics.js @@ -192,25 +192,22 @@ var async = require('async'), }, next); }, function(results, next) { - if (plugins.hasListeners('filter:topic.getRelatedTopics')) { - plugins.fireHook('filter:topic.getRelatedTopics', results, next); - } else { - Topics.getRelatedTopics(results, 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.bookmark = results.bookmark; - 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 || []; plugins.fireHook('filter:topic.get', {topic: topicData, uid: uid}, next); }, function(data, next) { diff --git a/src/topics/tags.js b/src/topics/tags.js index 183dbe903c..d816d5c49c 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -5,7 +5,6 @@ var async = require('async'), db = require('../database'), meta = require('../meta'), - user = require('../user'), _ = require('underscore'), plugins = require('../plugins'); @@ -323,43 +322,33 @@ module.exports = function(Topics) { }); }; - Topics.getRelatedTopics = function(topicData, callback) { - var maximumTopics = typeof meta.config.maximumRelatedTopics !== 'undefined' ? parseInt(meta.config.maximumRelatedTopics, 10) : 5; + 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) || 5; if (!topicData.tags.length || maximumTopics === 0) { return callback(null, topicData); } - var related = []; - - user.isAdministrator(topicData.threadTools.uid, function(err, isAdministrator) { - async.each(topicData.tags, function(tag, next) { - tag = tag.value; - - Topics.getTagTids(tag, 0, 5, function(err, tids) { - Topics.getTopics(tids, topicData.threadTools.uid, function(err, topics) { - related = related.concat(topics.filter(function(topic) { - var doesntOwnTopic = parseInt(topic.uid, 10) !== parseInt(topicData.threadTools.uid, 10); - var isntSameTopic = parseInt(topic.tid, 10) !== parseInt(topicData.threadTools.topic.tid, 10); - - return doesntOwnTopic && isntSameTopic; - })); - - next(err); - }); + async.waterfall([ + function (next) { + async.map(topicData.tags, function (tag, next) { + Topics.getTagTids(tag.value, 0, 5, next); + }, next); + }, + function (tids, next) { + tids = _.shuffle(_.unique(_.flatten(tids))).slice(0, maximumTopics); + Topics.getTopics(tids, uid, next); + }, + function (topics, next) { + topics = topics.filter(function(topic) { + return topic && !topic.deleted && parseInt(topic.uid, 10) !== parseInt(uid, 10); }); - }, function(err) { - if (!isAdministrator) { - related = related.filter(function(topic) { - return topic && !topic.deleted; - }); - } - - related = _.shuffle(related).slice(0, maximumTopics); - - topicData.related = related; - callback(err, topicData); - }); - }); + next(null, topics); + } + ], callback); }; }; \ No newline at end of file From 5d43da0d1c653160e3eb50958269a0c0d384082d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sat, 19 Dec 2015 11:06:38 -0500 Subject: [PATCH 59/59] 0.9.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44b1847559..305eeb4416 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "0.9.2", + "version": "0.9.3", "homepage": "http://www.nodebb.org", "repository": { "type": "git",