diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json
index 252e893d0d..167ab1aa66 100644
--- a/public/language/en_GB/error.json
+++ b/public/language/en_GB/error.json
@@ -77,7 +77,8 @@
"group-name-too-short": "Group name too short",
"group-already-exists": "Group already exists",
"group-name-change-not-allowed": "Group name change not allowed",
- "group-already-member": "You are already part of this group",
+ "group-already-member": "Already part of this group",
+ "group-not-member": "Not a member of this group",
"group-needs-owner": "This group requires at least one owner",
"group-already-invited": "This user has already been invited",
"group-already-requested": "Your membership request has already been submitted",
diff --git a/public/less/admin/manage/groups.less b/public/less/admin/manage/groups.less
index f6dac84921..ceeba06df7 100644
--- a/public/less/admin/manage/groups.less
+++ b/public/less/admin/manage/groups.less
@@ -1,5 +1,20 @@
.group {
- .current_members {
+ [component="groups/members"] {
padding: 0;
+ tbody {
+ max-height: 500px;
+ display: block;
+ overflow-y: auto;
+ padding-bottom: 100px;
+ .member-name {
+ width: 100%;
+ }
+ }
+
+
+ img {
+ width: 32px;
+ height: 32px;
+ }
}
}
\ No newline at end of file
diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js
index 37d2f64295..d8d6f4ec23 100644
--- a/public/src/admin/manage/group.js
+++ b/public/src/admin/manage/group.js
@@ -1,16 +1,16 @@
"use strict";
-/*global define, templates, socket, ajaxify, app, admin, bootbox, utils, config */
+/*global define, templates, socket, ajaxify, app, bootbox, translator */
define('admin/manage/group', [
+ 'forum/groups/memberlist',
'iconSelect',
'admin/modules/colorpicker'
-], function(iconSelect, colorpicker) {
+], function(memberList, iconSelect, colorpicker) {
var Groups = {};
Groups.init = function() {
var groupDetailsSearch = $('#group-details-search'),
groupDetailsSearchResults = $('#group-details-search-results'),
- groupMembersEl = $('ul.current_members'),
groupIcon = $('#group-icon'),
changeGroupUserTitle = $('#change-group-user-title'),
changeGroupLabelColor = $('#change-group-label-color'),
@@ -20,6 +20,8 @@ define('admin/manage/group', [
var groupName = ajaxify.data.group.name;
+ memberList.init(true);
+
changeGroupUserTitle.keyup(function() {
groupLabelPreview.text(changeGroupUserTitle.val());
});
@@ -46,10 +48,17 @@ define('admin/manage/group', [
}
groupDetailsSearchResults.empty();
+
for (x = 0; x < numResults; x++) {
foundUser = $('
');
foundUser
- .attr({title: results.users[x].username, 'data-uid': results.users[x].uid})
+ .attr({title:
+ results.users[x].username,
+ 'data-uid': results.users[x].uid,
+ 'data-username': results.users[x].username,
+ 'data-userslug': results.users[x].userslug,
+ 'data-picture': results.users[x].picture
+ })
.append($('
').attr('src', results.users[x].picture))
.append($('').html(results.users[x].username));
@@ -64,45 +73,74 @@ define('admin/manage/group', [
groupDetailsSearchResults.on('click', 'li[data-uid]', function() {
var userLabel = $(this),
- uid = parseInt(userLabel.attr('data-uid'), 10),
- members = [];
-
- groupMembersEl.find('li[data-uid]').each(function() {
- members.push(parseInt($(this).attr('data-uid'), 10));
- });
-
- if (members.indexOf(uid) === -1) {
- socket.emit('admin.groups.join', {
- groupName: groupName,
- uid: uid
- }, function(err, data) {
- if (!err) {
- groupMembersEl.append(userLabel.clone(true));
- }
- });
- }
- });
-
- groupMembersEl.on('click', 'li[data-uid]', function() {
- var uid = $(this).attr('data-uid');
+ uid = parseInt(userLabel.attr('data-uid'), 10);
- bootbox.confirm('Are you sure you want to remove this user?', function(confirm) {
- if (!confirm) {
- return;
+ socket.emit('admin.groups.join', {
+ groupName: groupName,
+ uid: uid
+ }, function(err) {
+ if (err) {
+ return app.alertError(err.message);
}
- socket.emit('admin.groups.leave', {
- groupName: groupName,
- uid: uid
- }, function(err, data) {
- if (err) {
- return app.alertError(err.message);
- }
- groupMembersEl.find('li[data-uid="' + uid + '"]').remove();
+ var member = {
+ uid: userLabel.attr('data-uid'),
+ username: userLabel.attr('data-username'),
+ userslug: userLabel.attr('data-userslug'),
+ picture: userLabel.attr('data-picture')
+ };
+
+ templates.parse('partials/groups/memberlist', 'members', {group: {isOwner: ajaxify.data.group.isOwner, members: [member]}}, function(html) {
+ translator.translate(html, function(html) {
+ $('[component="groups/members"] tr').first().before(html);
+ });
});
});
});
+ $('[component="groups/members"]').on('click', '[data-action]', function() {
+ var btnEl = $(this),
+ userRow = btnEl.parents('[data-uid]'),
+ ownerFlagEl = userRow.find('.member-name i'),
+ isOwner = !ownerFlagEl.hasClass('invisible') ? true : false,
+ uid = userRow.attr('data-uid'),
+ action = btnEl.attr('data-action');
+
+ switch(action) {
+ case 'toggleOwnership':
+ socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), {
+ toUid: uid,
+ groupName: groupName
+ }, function(err) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+ ownerFlagEl.toggleClass('invisible');
+ });
+ break;
+
+ case 'kick':
+ bootbox.confirm('Are you sure you want to remove this user?', function(confirm) {
+ if (!confirm) {
+ return;
+ }
+ socket.emit('admin.groups.leave', {
+ uid: uid,
+ groupName: groupName
+ }, function(err) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+ userRow.slideUp().remove();
+ });
+
+ });
+ break;
+ default:
+ break;
+ }
+ });
+
$('#group-icon').on('click', function() {
iconSelect.init(groupIcon);
});
diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js
index f5fb69e401..be624b2160 100644
--- a/public/src/client/groups/details.js
+++ b/public/src/client/groups/details.js
@@ -1,17 +1,22 @@
"use strict";
-/* globals define, socket, ajaxify, app, bootbox, RELATIVE_PATH, utils */
+/* globals define, socket, ajaxify, app, bootbox, utils */
+
+define('forum/groups/details', [
+ 'forum/groups/memberlist',
+ 'iconSelect',
+ 'components',
+ 'vendor/colorpicker/colorpicker',
+ 'vendor/jquery/draggable-background/backgroundDraggable'
+], function(memberList, iconSelect, components) {
-define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescroll', 'vendor/colorpicker/colorpicker', 'vendor/jquery/draggable-background/backgroundDraggable'], function(iconSelect, components, infinitescroll) {
var Details = {
cover: {}
};
- var searchInterval;
var groupName;
Details.init = function() {
- var detailsPage = components.get('groups/container'),
- settingsFormEl = detailsPage.find('form');
+ var detailsPage = components.get('groups/container');
groupName = ajaxify.data.group.name;
@@ -20,8 +25,8 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol
Details.initialiseCover();
}
- handleMemberSearch();
- handleMemberInfiniteScroll();
+ memberList.init();
+
handleMemberInvitations();
components.get('groups/activity').find('.content img:not(.not-responsive)').addClass('img-responsive');
@@ -291,44 +296,6 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol
});
};
- function handleMemberSearch() {
- $('[component="groups/members/search"]').on('keyup', function() {
- var query = $(this).val();
- if (searchInterval) {
- clearInterval(searchInterval);
- searchInterval = 0;
- }
-
- searchInterval = setTimeout(function() {
- socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function(err, results) {
- if (err) {
- return app.alertError(err.message);
- }
-
- infinitescroll.parseAndTranslate('groups/details', 'members', {
- group: {
- members: results.users,
- isOwner: ajaxify.data.group.isOwner
- }
- }, function(html) {
- $('[component="groups/members"] tbody').html(html);
- $('[component="groups/members"]').attr('data-nextstart', 20);
- });
- });
- }, 250);
- });
- }
-
- function handleMemberInfiniteScroll() {
- $('[component="groups/members"] tbody').on('scroll', function() {
- var $this = $(this);
- var bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
- if ($this.scrollTop() > bottom) {
- loadMoreMembers();
- }
- });
- }
-
function handleMemberInvitations() {
if (ajaxify.data.group.isOwner) {
var searchInput = $('[component="groups/members/invite"]');
@@ -349,48 +316,5 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol
}
}
- function loadMoreMembers() {
-
- var members = $('[component="groups/members"]');
- if (members.attr('loading')) {
- return;
- }
-
- members.attr('loading', 1);
- socket.emit('groups.loadMoreMembers', {
- groupName: groupName,
- after: members.attr('data-nextstart')
- }, function(err, data) {
- if (err) {
- return app.alertError(err.message);
- }
-
- if (data && data.users.length) {
- onMembersLoaded(data.users, function() {
- members.removeAttr('loading');
- members.attr('data-nextstart', data.nextStart);
- });
- } else {
- members.removeAttr('loading');
- }
- });
- }
-
- function onMembersLoaded(users, callback) {
- users = users.filter(function(user) {
- return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length;
- });
-
- infinitescroll.parseAndTranslate('groups/details', 'members', {
- group: {
- members: users,
- isOwner: ajaxify.data.group.isOwner
- }
- }, function(html) {
- $('[component="groups/members"] tbody').append(html);
- callback();
- });
- }
-
return Details;
});
\ No newline at end of file
diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js
new file mode 100644
index 0000000000..0cbc0e9116
--- /dev/null
+++ b/public/src/client/groups/memberlist.js
@@ -0,0 +1,97 @@
+"use strict";
+/* globals define, socket, ajaxify, app */
+
+define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], function(components, infinitescroll) {
+
+ var MemberList = {};
+ var searchInterval;
+ var groupName;
+
+ MemberList.init = function() {
+ groupName = ajaxify.data.group.name;
+
+ handleMemberSearch();
+ handleMemberInfiniteScroll();
+ };
+
+ function handleMemberSearch() {
+ $('[component="groups/members/search"]').on('keyup', function() {
+ var query = $(this).val();
+ if (searchInterval) {
+ clearInterval(searchInterval);
+ searchInterval = 0;
+ }
+
+ searchInterval = setTimeout(function() {
+ socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function(err, results) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+ parseAndTranslate(results.users, function(html) {
+ $('[component="groups/members"] tbody').html(html);
+ $('[component="groups/members"]').attr('data-nextstart', 20);
+ });
+ });
+ }, 250);
+ });
+ }
+
+ function handleMemberInfiniteScroll() {
+ $('[component="groups/members"] tbody').on('scroll', function() {
+ var $this = $(this);
+ var bottom = ($this[0].scrollHeight - $this.innerHeight()) * 0.9;
+
+ if ($this.scrollTop() > bottom) {
+ loadMoreMembers();
+ }
+ });
+ }
+
+ function loadMoreMembers() {
+ var members = $('[component="groups/members"]');
+ if (members.attr('loading')) {
+ return;
+ }
+
+ members.attr('loading', 1);
+ socket.emit('groups.loadMoreMembers', {
+ groupName: groupName,
+ after: members.attr('data-nextstart')
+ }, function(err, data) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+
+ if (data && data.users.length) {
+ onMembersLoaded(data.users, function() {
+ members.removeAttr('loading');
+ members.attr('data-nextstart', data.nextStart);
+ });
+ } else {
+ members.removeAttr('loading');
+ }
+ });
+ }
+
+ function onMembersLoaded(users, callback) {
+ users = users.filter(function(user) {
+ return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length;
+ });
+
+ parseAndTranslate(users, function(html) {
+ $('[component="groups/members"] tbody').append(html);
+ callback();
+ });
+ }
+
+ function parseAndTranslate(users, callback) {
+ infinitescroll.parseAndTranslate('groups/details', 'members', {
+ group: {
+ members: users,
+ isOwner: ajaxify.data.group.isOwner
+ }
+ }, callback);
+ }
+
+ return MemberList;
+});
\ No newline at end of file
diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js
index 259f93759d..dc8bf6ff82 100644
--- a/src/controllers/admin/groups.js
+++ b/src/controllers/admin/groups.js
@@ -59,12 +59,13 @@ groupsController.get = function(req, res, callback) {
if (!exists) {
return callback();
}
- groups.get(groupName, {uid: req.uid}, next);
+ groups.get(groupName, {uid: req.uid, truncateUserList: true, userListCount: 20}, next);
}
], function(err, group) {
if (err) {
return callback(err);
}
+ group.isOwner = true;
res.render('admin/manage/group', {group: group});
});
};
diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js
index 0cd47a778f..206604ec8f 100644
--- a/src/socket.io/admin/groups.js
+++ b/src/socket.io/admin/groups.js
@@ -1,5 +1,6 @@
"use strict";
+var async = require('async');
var groups = require('../../groups'),
Groups = {};
@@ -20,7 +21,17 @@ Groups.join = function(socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
- groups.join(data.groupName, data.uid, callback);
+ async.waterfall([
+ function (next) {
+ groups.isMember(data.uid, data.groupName, next);
+ },
+ function (isMember, next) {
+ if (isMember) {
+ return next(new Error('[[error:group-already-member]]'));
+ }
+ groups.join(data.groupName, data.uid, next);
+ }
+ ], callback);
};
Groups.leave = function(socket, data, callback) {
@@ -32,7 +43,17 @@ Groups.leave = function(socket, data, callback) {
return callback(new Error('[[error:cant-remove-self-as-admin]]'));
}
- groups.leave(data.groupName, data.uid, callback);
+ async.waterfall([
+ function (next) {
+ groups.isMember(data.uid, data.groupName, next);
+ },
+ function (isMember, next) {
+ if (!isMember) {
+ return next(new Error('[[error:group-not-member]]'));
+ }
+ groups.leave(data.groupName, data.uid, next);
+ }
+ ], callback);
};
Groups.update = function(socket, data, callback) {
diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js
index b75e6ba3df..1a07a90c26 100644
--- a/src/socket.io/groups.js
+++ b/src/socket.io/groups.js
@@ -62,8 +62,11 @@ SocketGroups.leave = function(socket, data, callback) {
function isOwner(next) {
return function (socket, data, callback) {
- groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) {
- if (err || !isOwner) {
+ async.parallel({
+ isAdmin: async.apply(user.isAdmin, socket.uid),
+ isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName)
+ }, function(err, results) {
+ if (err || (!isOwner && !results.isAdmin)) {
return callback(err || new Error('[[error:no-privileges]]'));
}
next(socket, data, callback);
diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl
index 3831f12a1c..c2828dfbd9 100644
--- a/src/views/admin/header.tpl
+++ b/src/views/admin/header.tpl
@@ -37,6 +37,7 @@
waitSeconds: 3,
urlArgs: "{cache-buster}",
paths: {
+ 'forum': '../client',
'admin': '../admin',
'vendor': '../../vendor',
'buzz': '../../vendor/buzz/buzz.min'
diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl
index e828d9da81..99acd57b13 100644
--- a/src/views/admin/manage/group.tpl
+++ b/src/views/admin/manage/group.tpl
@@ -54,24 +54,20 @@
+