diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 6a40edde0a..4aedd27dcd 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -76,6 +76,8 @@ "group-name-change-not-allowed": "Group name change not allowed", "group-already-member": "You are already part 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", "post-already-deleted": "This post has already been deleted", "post-already-restored": "This post has already been restored", diff --git a/public/language/en_GB/groups.json b/public/language/en_GB/groups.json index d6a5ed6b6e..dd8b795bcb 100644 --- a/public/language/en_GB/groups.json +++ b/public/language/en_GB/groups.json @@ -9,6 +9,11 @@ "pending.reject": "Reject", "pending.accept_all": "Accept All", "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", "cover-instructions": "Drag and Drop a photo, drag to position, and hit Save", "cover-change": "Change", @@ -18,6 +23,7 @@ "details.title": "Group Details", "details.members": "Member List", "details.pending": "Pending Members", + "details.invited": "Invited Members", "details.has_no_posts": "This group's members have not made any posts.", "details.latest_posts": "Latest Posts", "details.private": "Private", diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index a855febacf..ae6c46e03c 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -22,6 +22,7 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol handleMemberSearch(); handleMemberInfiniteScroll(); + handleMemberInvitations(); components.get('groups/activity').find('.content img').addClass('img-responsive'); @@ -72,6 +73,8 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol case 'leave': case 'accept': case 'reject': + case 'issueInvite': + case 'rescindInvite': case 'acceptInvite': case 'rejectInvite': case 'acceptAll': @@ -326,6 +329,26 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol }); } + function handleMemberInvitations() { + if (ajaxify.data.group.isOwner) { + var searchInput = $('[component="groups/members/invite"]'); + require(['autocomplete'], function(autocomplete) { + autocomplete.user(searchInput, function(e, selected) { + socket.emit('groups.issueInvite', { + toUid: selected.item.user.uid, + groupName: ajaxify.data.group.name + }, function(err) { + if (!err) { + ajaxify.refresh(); + } else { + app.alertError(err.message); + } + }); + }); + }); + } + } + function loadMoreMembers() { var members = $('[component="groups/members"]'); diff --git a/src/groups.js b/src/groups.js index f8bacf77d0..4a0d2a563e 100644 --- a/src/groups.js +++ b/src/groups.js @@ -138,6 +138,16 @@ var async = require('async'), } ], next); }, + invited: function (next) { + async.waterfall([ + function(next) { + db.getSetMembers('group:' + groupName + ':invited', next); + }, + function(uids, next) { + user.getUsersData(uids, next); + } + ], next); + }, isMember: async.apply(Groups.isMember, options.uid, groupName), isPending: async.apply(Groups.isPending, options.uid, groupName), isInvited: async.apply(Groups.isInvited, options.uid, groupName), @@ -170,6 +180,7 @@ var async = require('async'), results.base.members = results.members; results.base.membersNextStart = stop + 1; results.base.pending = results.pending.filter(Boolean); + results.base.invited = results.invited.filter(Boolean); results.base.deleted = !!parseInt(results.base.deleted, 10); results.base.hidden = !!parseInt(results.base.hidden, 10); results.base.system = !!parseInt(results.base.system, 10); diff --git a/src/groups/membership.js b/src/groups/membership.js index 262d2becfd..51ee633787 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -101,7 +101,9 @@ module.exports = function(Groups) { function(next) { async.parallel({ exists: async.apply(Groups.exists, groupName), - isMember: async.apply(Groups.isMember, uid, groupName) + isMember: async.apply(Groups.isMember, uid, groupName), + isPending: async.apply(Groups.isPending, uid, groupName), + isInvited: async.apply(Groups.isInvited, uid, groupName) }, next); }, function(checks, next) { @@ -109,6 +111,10 @@ module.exports = function(Groups) { return next(new Error('[[error:no-group]]')); } else if (checks.isMember) { return next(new Error('[[error:group-already-member]]')); + } else if (type === 'invite' && checks.isInvited) { + return next(new Error('[[error:group-already-invited]]')); + } else if (type === 'request' && checks.isPending) { + return next(new Error('[[error:group-already-requested]]')); } db.setAdd(set, uid, next); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index a2f7c076df..6f2d364b0a 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -131,7 +131,35 @@ function acceptRejectAll(type, socket, data, callback) { } ], callback); }); -} +}; + +SocketGroups.issueInvite = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (err || !isOwner) { + return callback(err || new Error('[[error:no-privileges]]')); + } + + groups.invite(data.groupName, data.toUid, callback); + }); +}; + +SocketGroups.rescindInvite = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { + if (err || !isOwner) { + return callback(err || new Error('[[error:no-privileges]]')); + } + + groups.rejectMembership(data.groupName, data.toUid, callback); + }); +}; SocketGroups.acceptInvite = function(socket, data, callback) { if (!data) {