diff --git a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js index d6fe6a7a6d..33b1b7f5a4 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js @@ -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 () => { diff --git a/test/api/v3/integration/groups/POST-groups_groupId_leave.js b/test/api/v3/integration/groups/POST-groups_groupId_leave.js index 8ef0651d40..c6b2c96193 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_leave.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_leave.js @@ -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; }); }); diff --git a/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js b/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js index 18f83fe9c9..626c6ff896 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js @@ -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'); }); }); }); diff --git a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js index 59bf23e3d8..e846a59290 100644 --- a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js +++ b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js @@ -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 () => { diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js index cab63ac970..debe0d122f 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -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 () => { diff --git a/test/api/v3/integration/user/auth/POST-register_local.test.js b/test/api/v3/integration/user/auth/POST-register_local.test.js index 18eedd7c0f..e56a6b132c 100644 --- a/test/api/v3/integration/user/auth/POST-register_local.test.js +++ b/test/api/v3/integration/user/auth/POST-register_local.test.js @@ -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 () => { diff --git a/website/client-old/js/controllers/menuCtrl.js b/website/client-old/js/controllers/menuCtrl.js index 5df3c43942..4239eb5374 100644 --- a/website/client-old/js/controllers/menuCtrl.js +++ b/website/client-old/js/controllers/menuCtrl.js @@ -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){ diff --git a/website/client-old/js/controllers/partyCtrl.js b/website/client-old/js/controllers/partyCtrl.js index 146b12021b..a90f96f43c 100644 --- a/website/client-old/js/controllers/partyCtrl.js +++ b/website/client-old/js/controllers/partyCtrl.js @@ -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; diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 99338176d7..e6347161ae 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -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", diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index 8990a40c98..9fbb8fff24 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -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}); } diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 42ebcea2cb..71d9dea596 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -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]; } } diff --git a/website/server/models/group.js b/website/server/models/group.js index e60afdbbfb..cafeb87abf 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -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 }); diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 454d697b14..aa5481d769 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -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.']}], diff --git a/website/views/options/social/party/leave-party-and-join-another.jade b/website/views/options/social/party/leave-party-and-join-another.jade index b7571613a4..3d9e19c815 100644 --- a/website/views/options/social/party/leave-party-and-join-another.jade +++ b/website/views/options/social/party/leave-party-and-join-another.jade @@ -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') diff --git a/website/views/options/social/party/party-invitation.jade b/website/views/options/social/party/party-invitation.jade index 0bfd72b6d8..841384ebe6 100644 --- a/website/views/options/social/party/party-invitation.jade +++ b/website/views/options/social/party/party-invitation.jade @@ -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') diff --git a/website/views/options/social/party/start-a-party.jade b/website/views/options/social/party/start-a-party.jade index b6946ed0aa..72b9591436 100644 --- a/website/views/options/social/party/start-a-party.jade +++ b/website/views/options/social/party/start-a-party.jade @@ -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") diff --git a/website/views/shared/header/menu.jade b/website/views/shared/header/menu.jade index bcdede415a..86978421cb 100644 --- a/website/views/shared/header/menu.jade +++ b/website/views/shared/header/menu.jade @@ -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