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 @@ +
    - -

    Click on a user to remove them from the group

    -
    +
    +

    [[groups:details.members]]

    +
    -
      - -
    • - - {group.members.username} -
    • - -
    +
    + +