Allow Multiple Invites to Party (#8683)

* (server) Add parties array to store invites

* (server) Lint files

* Update joinGroup, rejectGroupInvite, _inviteByUUID, and remove clearPartyInvitation.js

* Update user schema: detailed 'invitations.parties' attributes

* Code improvement and do not let invite twice

* Check if the user is already invited earlier in the code

* Added message to invitation page, and show all invitations

* Added join party confirmation alert

* Small fixes

* Created test: allow inviting a user to 2 different parties

* Updated tests

* Update invitations.parties on more places

* Small adjustments

* Updates on invitations.party references

* Show all invitations when user is already in a party

* Fixed notifications counter

* Update both 'party' and 'parties' at _handleGroupInvitation

* Updated a test

* Fixed small mistake at _handleGroupInvitation

* More test update

* Update invitation.party when removing single invite and small adjust at view
This commit is contained in:
Mateus Etto
2017-07-19 22:45:28 -03:00
committed by Sabe Jones
parent 11a4c1c95d
commit cdc8473f60
17 changed files with 133 additions and 64 deletions

View File

@@ -220,7 +220,7 @@ describe('POST /group/:groupId/join', () => {
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.party.id');
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.parties[0].id');
});
it('increments memberCount when joining party', async () => {

View File

@@ -247,7 +247,7 @@ describe('POST /groups/:groupId/leave', () => {
let userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.party).to.be.empty;
expect(userWithoutInvitation.invitations.parties[0]).to.be.empty;
});
});

View File

@@ -107,7 +107,7 @@ describe('POST /group/:groupId/reject-invite', () => {
it('clears invitation from user', async () => {
await invitedUser.post(`/groups/${party._id}/reject-invite`);
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.party.id');
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.parties[0].id');
});
});
});

View File

@@ -177,13 +177,13 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
it('can remove other invites', async () => {
expect(partyInvitedUser.invitations.party).to.not.be.empty;
expect(partyInvitedUser.invitations.parties[0]).to.not.be.empty;
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
let invitedUserWithoutInvite = await partyInvitedUser.get('/user');
expect(invitedUserWithoutInvite.invitations.party).to.be.empty;
expect(invitedUserWithoutInvite.invitations.parties[0]).to.be.empty;
});
it('removes new messages from a member who is removed', async () => {

View File

@@ -440,7 +440,38 @@ describe('Post /groups/:groupId/invite', () => {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allow inviting a user to 2 different parties', async () => {
// Create another inviter
let inviter2 = await generateUser();
// Create user to invite
let userToInvite = await generateUser();
// Create second group
let party2 = await inviter2.post('/groups', {
name: 'Test Party 2',
type: 'party',
});
// Invite to first party
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
// Invite to second party
await inviter2.post(`/groups/${party2._id}/invite`, {
uuids: [userToInvite._id],
});
// Get updated user
let invitedUser = await userToInvite.get('/user');
expect(invitedUser.invitations.parties.length).to.equal(2);
expect(invitedUser.invitations.parties[0].id).to.equal(party._id);
expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
});
it('allow inviting a user if party id is not associated with a real party', async () => {
@@ -451,7 +482,7 @@ describe('Post /groups/:groupId/invite', () => {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allows 30 members in a party', async () => {

View File

@@ -509,11 +509,9 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
expect(user.invitations.party).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {

View File

@@ -14,7 +14,7 @@ angular.module('habitrpg')
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
return mysteryValue;
} else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {
} else if ((user.invitations.parties && user.invitations.parties.length > 0) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {
return invitationValue;
} else if (user.flags.cardReceived) {
return cardValue;
@@ -76,8 +76,8 @@ angular.module('habitrpg')
$scope.getNotificationsCount = function() {
var count = 0;
if($scope.user.invitations.party && $scope.user.invitations.party.id){
count++;
if($scope.user.invitations.parties){
count += $scope.user.invitations.parties.length;
}
if($scope.user.purchased.plan && $scope.user.purchased.plan.mysteryItems.length){

View File

@@ -1,7 +1,7 @@
'use strict';
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social', 'Achievement', 'Members', 'Tasks',
function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Achievement, Members, Tasks) {
habitrpg.controller("PartyCtrl", ['$rootScope','$scope','$window','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social', 'Achievement', 'Members', 'Tasks',
function($rootScope, $scope, $window, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Achievement, Members, Tasks) {
var PARTY_LOADING_MESSAGES = 4;
@@ -129,6 +129,10 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
return;
}
if (!$window.confirm(window.env.t('joinPartyConfirmationText', {partyName: party.name}))){
return;
}
Groups.Group.join(party.id)
.then(function (response) {
$rootScope.party = $scope.group = response.data.data;

View File

@@ -36,7 +36,9 @@
"invite": "Invite",
"leave": "Leave",
"invitedTo": "Invited to <%= name %>",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party, reject all other party invitations and join <%= partyName %>?",
"partyInvitationsText": "You have <%= numberInvites %> party invitations! Choose wisely, because you can only be in one party at a time.",
"joinPartyConfirmationText": "Are you sure you want to join the party \"<%= partyName %>\"? You can only be in one party at a time. If you join, all other party invitations will be rejected.",
"invitationAcceptedHeader": "Your Invitation has been Accepted",
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
"joinNewParty": "Join New Party",

View File

@@ -46,6 +46,7 @@ async function _handleGroupInvitation (user, invite) {
if (group.type === 'party') {
user.invitations.party = {id: group._id, name: group.name, inviter};
user.invitations.parties.push(user.invitations.party);
} else {
user.invitations.guilds.push({id: group._id, name: group.name, inviter});
}

View File

@@ -480,29 +480,35 @@ api.joinGroup = {
let isUserInvited = false;
if (group.type === 'party' && group._id === user.invitations.party.id) {
inviter = user.invitations.party.inviter;
user.invitations.party = {}; // Clear invite
user.markModified('invitations.party');
if (group.type === 'party') {
// Check if was invited to party
let inviterParty = _.find(user.invitations.parties, {id: group._id});
if (inviterParty) {
inviter = inviterParty.inviter;
// invite new user to pending quest
if (group.quest.key && !group.quest.active) {
user.party.quest.RSVPNeeded = true;
user.party.quest.key = group.quest.key;
group.quest.members[user._id] = null;
group.markModified('quest.members');
// Clear all invitations of new user
user.invitations.parties = [];
user.invitations.party = {};
// invite new user to pending quest
if (group.quest.key && !group.quest.active) {
user.party.quest.RSVPNeeded = true;
user.party.quest.key = group.quest.key;
group.quest.members[user._id] = null;
group.markModified('quest.members');
}
// If user was in a different party (when partying solo you can be invited to a new party)
// make them leave that party before doing anything
if (user.party._id) {
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
if (userPreviousParty) await userPreviousParty.leave(user);
}
user.party._id = group._id; // Set group as user's party
isUserInvited = true;
}
// If user was in a different party (when partying solo you can be invited to a new party)
// make him leave that party before doing anything
if (user.party._id) {
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
if (userPreviousParty) await userPreviousParty.leave(user);
}
user.party._id = group._id; // Set group as user's party
isUserInvited = true;
} else if (group.type === 'guild') {
let hasInvitation = removeFromArray(user.invitations.guilds, { id: group._id });
@@ -636,8 +642,9 @@ api.rejectGroupInvite = {
let groupId = req.params.groupId;
let isUserInvited = false;
if (groupId === user.invitations.party.id) {
user.invitations.party = {};
let hasPartyInvitation = removeFromArray(user.invitations.parties, { id: groupId });
if (hasPartyInvitation) {
user.invitations.party = user.invitations.parties.length > 0 ? user.invitations.parties[user.invitations.parties.length - 1] : {};
user.markModified('invitations.party');
isUserInvited = true;
} else {
@@ -804,7 +811,7 @@ api.removeGroupMember = {
}
let isInvited;
if (member.invitations.party && member.invitations.party.id === group._id) {
if (_.find(member.invitations.parties, {id: group._id})) {
isInvited = 'party';
} else if (_.findIndex(member.invitations.guilds, {id: group._id}) !== -1) {
isInvited = 'guild';
@@ -849,7 +856,8 @@ api.removeGroupMember = {
removeFromArray(member.invitations.guilds, { id: group._id });
}
if (isInvited === 'party') {
member.invitations.party = {};
removeFromArray(member.invitations.parties, { id: group._id });
member.invitations.party = member.invitations.parties.length > 0 ? member.invitations.parties[member.invitations.parties.length - 1] : {};
member.markModified('invitations.party');
}
} else {
@@ -888,7 +896,8 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
userToInvite.invitations.guilds.push(guildInvite);
} else if (group.type === 'party') {
if (userToInvite.invitations.party.id) {
// Do not add to invitations.parties array if the user is already invited to that party
if (_.find(userToInvite.invitations.parties, {id: group._id})) {
throw new NotAuthorized(res.t('userAlreadyPendingInvitation'));
}
@@ -901,6 +910,8 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
let partyInvite = {id: group._id, name: group.name, inviter: inviter._id};
if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
userToInvite.invitations.parties.push(partyInvite);
userToInvite.invitations.party = partyInvite;
}
@@ -943,7 +954,7 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
if (group.type === 'guild') {
return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1];
} else if (group.type === 'party') {
return userInvited.invitations.party;
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
}
}

View File

@@ -390,7 +390,8 @@ schema.methods.removeGroupInvitations = async function removeGroupInvitations ()
let userUpdates = usersToRemoveInvitationsFrom.map(user => {
if (group.type === 'party') {
user.invitations.party = {};
removeFromArray(user.invitations.parties, { id: group._id });
user.invitations.party = user.invitations.parties.length > 0 ? user.invitations.parties[user.invitations.parties.length - 1] : {};
this.markModified('invitations.party');
} else {
removeFromArray(user.invitations.guilds, { id: group._id });

View File

@@ -384,6 +384,24 @@ let schema = new Schema({
party: {type: Schema.Types.Mixed, default: () => {
return {};
}},
parties: [{
id: {
type: String,
ref: 'Group',
required: true,
validate: [validator.isUUID, 'Invalid uuid.'],
},
name: {
type: String,
required: true,
},
inviter: {
type: String,
ref: 'User',
required: true,
validate: [validator.isUUID, 'Invalid uuid.'],
},
}],
},
guilds: [{type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.']}],

View File

@@ -1,8 +1,9 @@
- var newParty = 'User.user.invitations.party'
.containter-fluid(ng-if='#{newParty}.id && group._id')
.row.text-center
.col-sm-6.col-sm-offset-3.alert.alert-warning
p {{::env.t('invitedToNewParty', { partyName: #{newParty}.name })}}
p
button.btn.btn-success(ng-click='leaveOldPartyAndJoinNewParty(#{newParty}.id, #{newParty}.name)')=env.t('joinNewParty')
button.btn.btn-default(ng-click='reject(#{newParty})')=env.t('declineInvitation')
- var newParties = 'User.user.invitations.parties'
.containter-fluid(ng-if='#{newParties}.length > 0 && group._id')
div(ng-repeat='partyInvite in #{newParties}')
.row.text-center
.col-sm-6.col-sm-offset-3.alert.alert-warning
p {{::env.t('invitedToNewParty', { partyName: partyInvite.name })}}
p
button.btn.btn-success(ng-click='leaveOldPartyAndJoinNewParty(partyInvite.id, partyInvite.name)')=env.t('joinNewParty')
button.btn.btn-default(ng-click='reject(partyInvite)')=env.t('declineInvitation')

View File

@@ -1,8 +1,10 @@
.container-fluid(ng-show='user.invitations.party.id')
h2=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
.container-fluid(ng-show='user.invitations.parties.length > 0')
h3=env.t('partyInvitationsText', {numberInvites: '{{user.invitations.parties.length}}'})
a.btn.btn-success(
data-type='party',
ng-click='join(user.invitations.party)'
)=env.t('accept')
a.btn.btn-danger(ng-click='reject(user.invitations.party)')=env.t('reject')
div(ng-repeat='partyInvite in user.invitations.parties')
h2=env.t('invitedTo', {name: '{{partyInvite.name}}'})
a.btn.btn-success(
data-type='party',
ng-click='join(partyInvite)'
)=env.t('accept')
a.btn.btn-danger(ng-click='reject(partyInvite)')=env.t('reject')

View File

@@ -1,4 +1,4 @@
.container.text-center(ng-hide='user.invitations.party.id')
.container.text-center(ng-hide='user.invitations.parties.length > 0')
.row.row-margin: .col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="inviteOrStartParty(group)")=env.t("startPartyWithFriends")

View File

@@ -198,10 +198,10 @@ nav.toolbar(ng-controller='MenuCtrl')
a(ng-click='$state.go("options.inventory.drops"); ')
span.glyphicon.glyphicon-gift
span=env.t('newSubscriberItem')
li(ng-if='user.invitations.party.id')
li(ng-repeat='party in user.invitations.parties')
a(ui-sref='options.social.party')
span.glyphicon.glyphicon-user
span=env.t('invitedTo', {name: '{{user.invitations.party.name}}'})
span=env.t('invitedTo', {name: '{{party.name}}'})
li(ng-if='user.flags.cardReceived')
a(ng-click='$state.go("options.inventory.drops"); ')
span.glyphicon.glyphicon-envelope