diff --git a/public/src/client/users.js b/public/src/client/users.js index eaa9467a00..efd589d42d 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -118,25 +118,18 @@ define('forum/users', ['translator'], function(translator) { var notify = $('#user-notfound-notify'); page = page || 1; + if (!username) { + return loadPage(page); + } + notify.html(''); - var filters = []; - $('.user-filter').each(function() { - var $this = $(this); - if($this.is(':checked')) { - filters.push({ - field:$this.attr('data-filter-field'), - type: $this.attr('data-filter-type'), - value: $this.attr('data-filter-value') - }); - } - }); socket.emit('user.search', { query: username, page: page, - searchBy: ['username', 'fullname'], + searchBy: 'username', sortBy: $('.search select').val(), - filterBy: filters + onlineOnly: $('.search .online-only').is(':checked') }, function(err, data) { if (err) { reset(); @@ -147,33 +140,49 @@ define('forum/users', ['translator'], function(translator) { return reset(); } - templates.parse('partials/paginator', {pagination: data.pagination}, function(html) { - $('.pagination-container').replaceWith(html); - }); + renderSearchResults(data); + }); + } + + + function loadPage(page) { + socket.emit('user.loadPage', {page: page, sortBy: $('.search select').val(), onlineOnly: $('.search .online-only').is(':checked')}, function(err, data) { + if (err) { + return app.alertError(err.message); + } + + renderSearchResults(data); + }); + } - templates.parse('users', 'users', data, function(html) { - translator.translate(html, function(translated) { - $('#users-container').html(translated); - - if (!data.users.length) { - translator.translate('[[error:no-user]]', function(translated) { - notify.html(translated); - notify.parent().removeClass('btn-success label-success').addClass('btn-warning label-warning'); - }); - } else { - translator.translate('[[users:users-found-search-took, ' + data.matchCount + ', ' + data.timing + ']]', function(translated) { - notify.html(translated); - notify.parent().removeClass('btn-warning label-warning').addClass('btn-success label-success'); - }); - } - }); + function renderSearchResults(data) { + var notify = $('#user-notfound-notify'); + templates.parse('partials/paginator', {pagination: data.pagination}, function(html) { + $('.pagination-container').replaceWith(html); + }); + + templates.parse('users', 'users', data, function(html) { + translator.translate(html, function(translated) { + $('#users-container').html(translated); + + if (!data.users.length) { + translator.translate('[[error:no-user]]', function(translated) { + notify.html(translated); + notify.parent().removeClass('btn-success label-success').addClass('btn-warning label-warning'); + }); + } else { + translator.translate('[[users:users-found-search-took, ' + data.matchCount + ', ' + data.timing + ']]', function(translated) { + notify.html(translated); + notify.parent().removeClass('btn-warning label-warning').addClass('btn-success label-success'); + }); + } }); }); } function onUserStatusChange(data) { var section = getActiveSection(); - + if ((section.startsWith('online') || section.startsWith('users'))) { updateUser(data); } diff --git a/src/controllers/users.js b/src/controllers/users.js index 29dd552b87..2c837b2861 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -48,26 +48,26 @@ usersController.getOnlineUsers = function(req, res, next) { }; usersController.getUsersSortedByPosts = function(req, res, next) { - usersController.getUsers('users:postcount', 50, req, res, next); + usersController.getUsers('users:postcount', 0, 49, req, res, next); }; usersController.getUsersSortedByReputation = function(req, res, next) { - usersController.getUsers('users:reputation', 50, req, res, next); + usersController.getUsers('users:reputation', 0, 49, req, res, next); }; usersController.getUsersSortedByJoinDate = function(req, res, next) { - usersController.getUsers('users:joindate', 50, req, res, next); + usersController.getUsers('users:joindate', 0, 49, req, res, next); }; -usersController.getUsers = function(set, count, req, res, next) { - getUsersAndCount(set, req.uid, count, function(err, data) { +usersController.getUsers = function(set, start, stop, req, res, next) { + usersController.getUsersAndCount(set, req.uid, start, stop, function(err, data) { if (err) { return next(err); } var pageCount = Math.ceil(data.count / (parseInt(meta.config.userSearchResultsPerPage, 10) || 20)); var userData = { search_display: 'hidden', - loadmore_display: data.count > count ? 'block' : 'hide', + loadmore_display: data.count > (stop - start + 1) ? 'block' : 'hide', users: data.users, pagination: pagination.create(1, pageCount) }; @@ -76,13 +76,13 @@ usersController.getUsers = function(set, count, req, res, next) { }); }; -function getUsersAndCount(set, uid, count, callback) { +usersController.getUsersAndCount = function(set, uid, start, stop, callback) { async.parallel({ users: function(next) { - user.getUsersFromSet(set, uid, 0, count - 1, next); + user.getUsersFromSet(set, uid, start, stop, next); }, count: function(next) { - db.getObjectField('global', 'userCount', next); + db.sortedSetCard(set, next); } }, function(err, results) { if (err) { @@ -94,7 +94,7 @@ function getUsersAndCount(set, uid, count, callback) { callback(null, results); }); -} +}; usersController.getUsersForSearch = function(req, res, next) { if (!req.uid) { @@ -102,7 +102,7 @@ usersController.getUsersForSearch = function(req, res, next) { } var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; - getUsersAndCount('users:joindate', req.uid, resultsPerPage, function(err, data) { + usersController.getUsersAndCount('users:joindate', req.uid, 0, resultsPerPage - 1, function(err, data) { if (err) { return next(err); } diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 1d8f992ab9..fddda44388 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -509,4 +509,28 @@ module.exports = function(db, module) { callback(err, result && result.value ? result.value.score : null); }); }; + + module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) { + var query = {_key: key}; + if (min !== '-') { + query.value = {$gte: min}; + } + if (max !== '+') { + query.value = query.value || {}; + query.value.$lte = max; + } + db.collection('objects').find(query, {_id: 0, value: 1}) + .sort({value: 1}) + .skip(start) + .limit(count === -1 ? 0 : count) + .toArray(function(err, data) { + if (err) { + return callback(err); + } + data = data.map(function(item) { + return item && item.value; + }); + callback(err, data); + }); + }; }; \ No newline at end of file diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 195d9feec8..8d9b35896d 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -246,4 +246,14 @@ module.exports = function(redisClient, module) { module.sortedSetIncrBy = function(key, increment, value, callback) { redisClient.zincrby(key, increment, value, callback); }; + + module.getSortedSetRangeByLex = function(key, min, max, start, count, callback) { + if (min !== '-') { + min = '[' + min; + } + if (max !== '+') { + max = '(' + max; + } + redisClient.zrangebylex([key, min, max, 'LIMIT', start, count], callback); + }; }; \ No newline at end of file diff --git a/src/groups.js b/src/groups.js index b46b23aca5..c6db87f3c3 100644 --- a/src/groups.js +++ b/src/groups.js @@ -1144,7 +1144,7 @@ var async = require('async'), Groups.searchMembers = function(data, callback) { - function findUids(query, searchBy, startsWith, callback) { + function findUids(query, searchBy, callback) { if (!query) { return Groups.getMembers(data.groupName, 0, -1, callback); } @@ -1154,25 +1154,16 @@ var async = require('async'), Groups.getMembers(data.groupName, 0, -1, next); }, function(members, next) { - user.getMultipleUserFields(members, ['uid'].concat(searchBy), next); + user.getMultipleUserFields(members, ['uid'].concat([searchBy]), next); }, function(users, next) { var uids = []; - - for(var k=0; k 1) { - uids = uids.filter(function(uid, index, array) { - return array.indexOf(uid) === index; - }); - } - next(null, uids); } ], callback); diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 454a51741a..60494ca6e1 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -205,7 +205,7 @@ User.deleteUsers = function(socket, uids, callback) { }; User.search = function(socket, data, callback) { - user.search({query: data.query, searchBy: data.searchBy, startsWith: false, uid: socket.uid}, function(err, searchData) { + user.search({query: data.query, searchBy: data.searchBy, uid: socket.uid}, function(err, searchData) { if (err) { return callback(err); } diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 65af901b08..8a954faeaf 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -77,7 +77,7 @@ SocketUser.search = function(socket, data, callback) { page: data.page, searchBy: data.searchBy, sortBy: data.sortBy, - filterBy: data.filterBy, + onlineOnly: data.onlineOnly, uid: socket.uid }, callback); }; @@ -431,6 +431,60 @@ SocketUser.loadMore = function(socket, data, callback) { }); }; +SocketUser.loadPage = function(socket, data, callback) { + function done(err, result) { + if (err) { + return callback(err); + } + var pageCount = Math.ceil(result.count / resultsPerPage); + var userData = { + users: result.users, + pagination: pagination.create(data.page, pageCount) + }; + + callback(null, userData); + } + + var controllers = require('../controllers/users'); + var pagination = require('../pagination'); + var set = ''; + data.sortBy = data.sortBy || 'joindate'; + + var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; + var start = Math.max(0, data.page - 1) * resultsPerPage; + var stop = start + resultsPerPage - 1; + if (data.onlineOnly) { + async.parallel({ + users: function(next) { + user.getUsersFromSet('users:online', socket.uid, 0, 49, next); + }, + count: function(next) { + var now = Date.now(); + db.sortedSetCount('users:online', now - 300000, now, next); + } + }, done); + } else if (data.sortBy === 'username') { + async.parallel({ + count: function(next) { + db.sortedSetCard('username:sorted', next); + }, + users: function(next) { + db.getSortedSetRangeByLex('username:sorted', '-', '+', start, stop - start + 1, function(err, result) { + if (err) { + return next(err); + } + var uids = result.map(function(user) { + return user && user.split(':')[1]; + }); + user.getUsers(uids, socket.uid, next); + }); + } + }, done); + } else { + controllers.getUsersAndCount('users:joindate', socket.uid, start, stop, done); + } +}; + SocketUser.setStatus = function(socket, status, callback) { if (!socket.uid) { return callback(new Error('[[error:invalid-uid]]')); diff --git a/src/user/create.js b/src/user/create.js index 2580a738f1..9ce768b9ef 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -93,6 +93,9 @@ module.exports = function(User) { function(next) { db.sortedSetAdd('username:uid', userData.uid, userData.username, next); }, + function(next) { + db.sortedSetAdd('username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid, next); + }, function(next) { db.sortedSetAdd('userslug:uid', userData.uid, userData.userslug, next); }, @@ -107,7 +110,11 @@ module.exports = function(User) { }, function(next) { if (userData.email) { - db.sortedSetAdd('email:uid', userData.uid, userData.email.toLowerCase(), next); + async.parallel([ + async.apply(db.sortedSetAdd, 'email:uid', userData.uid, userData.email.toLowerCase()), + async.apply(db.sortedSetAdd, 'email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid) + ], next); + if (parseInt(userData.uid, 10) !== 1 && parseInt(meta.config.requireEmailConfirmation, 10) === 1) { User.email.sendValidationEmail(userData.uid, userData.email); } diff --git a/src/user/delete.js b/src/user/delete.js index 2d06caf745..2558399134 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -51,6 +51,9 @@ module.exports = function(User) { function(next) { db.sortedSetRemove('username:uid', userData.username, next); }, + function(next) { + db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid, next); + }, function(next) { db.sortedSetRemove('userslug:uid', userData.userslug, next); }, @@ -59,7 +62,10 @@ module.exports = function(User) { }, function(next) { if (userData.email) { - db.sortedSetRemove('email:uid', userData.email.toLowerCase(), next); + async.parallel([ + async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()), + async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid) + ], next); } else { next(); } diff --git a/src/user/profile.js b/src/user/profile.js index d10aba66db..db0ce9029b 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -164,8 +164,10 @@ module.exports = function(User) { if (userData.email === newEmail) { return callback(); } - - db.sortedSetRemove('email:uid', userData.email.toLowerCase(), function(err) { + async.series([ + async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()), + async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid) + ], function(err) { if (err) { return callback(err); } @@ -178,6 +180,9 @@ module.exports = function(User) { function(next) { db.sortedSetAdd('email:uid', uid, newEmail.toLowerCase(), next); }, + function(next) { + db.sortedSetAdd('email:sorted', 0, newEmail.toLowerCase() + ':' + uid, next); + }, function(next) { User.setUserField(uid, 'email', newEmail, next); }, @@ -216,7 +221,13 @@ module.exports = function(User) { function(next) { var newUserslug = utils.slugify(newUsername); updateUidMapping('userslug', uid, newUserslug, userData.userslug, next); - } + }, + function(next) { + async.series([ + async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid), + async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid) + ], next); + }, ], callback); }); } diff --git a/src/user/search.js b/src/user/search.js index ce1ad0c6b2..43697791be 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -11,8 +11,7 @@ module.exports = function(User) { User.search = function(data, callback) { var query = data.query || ''; - var searchBy = data.searchBy || ['username']; - var startsWith = data.hasOwnProperty('startsWith') ? data.startsWith : true; + var searchBy = data.searchBy || 'username'; var page = data.page || 1; var uid = data.uid || 0; var paginate = data.hasOwnProperty('paginate') ? data.paginate : true; @@ -27,14 +26,13 @@ module.exports = function(User) { async.waterfall([ function(next) { if (data.findUids) { - data.findUids(query, searchBy, startsWith, next); + data.findUids(query, searchBy, next); } else { - findUids(query, searchBy, startsWith, next); + findUids(query, searchBy, next); } }, function(uids, next) { - var filterBy = Array.isArray(data.filterBy) ? data.filterBy : []; - filterAndSortUids(uids, filterBy, data.sortBy, next); + filterAndSortUids(uids, data, next); }, function(uids, next) { plugins.fireHook('filter:users.search', {uids: uids, uid: uid}, next); @@ -75,70 +73,39 @@ module.exports = function(User) { }; }; - function findUids(query, searchBy, startsWith, callback) { + function findUids(query, searchBy, callback) { if (!query) { - return db.getSortedSetRevRange('users:joindate', 0, -1, callback); + return callback(null, []); } + var min = query; + var max = query.substr(0, query.length - 1) + String.fromCharCode(query.charCodeAt(query.length - 1) + 1); - var keys = searchBy.map(function(searchBy) { - return searchBy + ':uid'; - }); - - async.map(keys, function(key, next) { - db.getSortedSetRangeWithScores(key, 0, -1, next); - }, function(err, hashes) { - if (err || !hashes) { - return callback(err, []); - } - - hashes = hashes.filter(Boolean); - - query = query.toLowerCase(); - - var uids = []; - var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; - var hardCap = resultsPerPage * 10; - - for (var i=0; i= hardCap) { - break; - } - } - } - if (uids.length >= hardCap) { - break; - } - } + var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; + var hardCap = resultsPerPage * 10; - if (hashes.length > 1) { - uids = uids.filter(function(uid, index, array) { - return array.indexOf(uid) === index; - }); + db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function(err, data) { + if (err) { + return callback(err); } + var uids = data.map(function(data) { + return data.split(':')[1]; + }); callback(null, uids); }); } - function filterAndSortUids(uids, filterBy, sortBy, callback) { - sortBy = sortBy || 'joindate'; + function filterAndSortUids(uids, data, callback) { + var sortBy = data.sortBy || 'joindate'; - var fields = filterBy.map(function(filter) { - return filter.field; - }).concat(['uid', sortBy]).filter(function(field, index, array) { - return array.indexOf(field) === index; - }); + var fields = ['uid', 'status', sortBy]; async.parallel({ userData: function(next) { User.getMultipleUserFields(uids, fields, next); }, isOnline: function(next) { - if (fields.indexOf('status') !== -1) { + if (data.onlineOnly) { require('../socket.io').isUsersOnline(uids, next); } else { next(); @@ -148,50 +115,22 @@ module.exports = function(User) { if (err) { return callback(err); } + var userData = results.userData; - if (results.isOnline) { - userData.forEach(function(userData, index) { - userData.status = User.getStatus(userData.status, results.isOnline[index]); + if (data.onlineOnly) { + userData = userData.filter(function(user, index) { + return user && user.status !== 'offline' && results.isOnline[index]; }); } - userData = filterUsers(userData, filterBy); - sortUsers(userData, sortBy); uids = userData.map(function(user) { return user && user.uid; }); - callback(null, uids); - }); - } - function filterUsers(userData, filterBy) { - function passesFilter(user, filter) { - if (!user || !filter) { - return false; - } - var userValue = user[filter.field]; - if (filter.type === '=') { - return userValue === filter.value; - } else if (filter.type === '!=') { - return userValue !== filter.value; - } - return false; - } - - if (!filterBy.length) { - return userData; - } - - return userData.filter(function(user) { - for(var i=0; i