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