diff --git a/habitica-images b/habitica-images index 4ec7469f72..311cb01059 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit 4ec7469f72f59f0fdac74710d32add59958a540e +Subproject commit 311cb010591a9de368a1ab5db7506d2d3efbb10b diff --git a/package-lock.json b/package-lock.json index a773ba9059..e43131bc51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.267.1", + "version": "4.269.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cc971a4d52..fd55d4a746 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.267.1", + "version": "4.269.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.21.4", 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 92acf9023f..bbad7a8401 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 @@ -258,47 +258,6 @@ describe('POST /group/:groupId/join', () => { await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 2); }); - it('deletes previous party where the user was the only member', async () => { - const userToInvite = await generateUser(); - const oldParty = await userToInvite.post('/groups', { // add user to a party - name: 'Another Test Party', - type: 'party', - }); - - await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true); - await user.post(`/groups/${party._id}/invite`, { - uuids: [userToInvite._id], - }); - await userToInvite.post(`/groups/${party._id}/join`); - - await expect(user.get('/user')).to.eventually.have.nested.property('party._id', party._id); - await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false); - }); - - it('does not allow user to leave a party if a quest was active and they were the only member', async () => { - const userToInvite = await generateUser(); - const oldParty = await userToInvite.post('/groups', { // add user to a party - name: 'Another Test Party', - type: 'party', - }); - - await userToInvite.update({ - [`items.quests.${PET_QUEST}`]: 1, - }); - await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`); - - await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true); - await user.post(`/groups/${party._id}/invite`, { - uuids: [userToInvite._id], - }); - - await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('messageCannotLeaveWhileQuesting'), - }); - }); - it('invites joining member to active quest', async () => { await user.update({ [`items.quests.${PET_QUEST}`]: 1, 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 02f8c5e20c..58a478be00 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -1,6 +1,7 @@ import { v4 as generateUUID } from 'uuid'; import nconf from 'nconf'; import { + createAndPopulateGroup, generateUser, generateGroup, translate as t, @@ -581,20 +582,7 @@ describe('Post /groups/:groupId/invite', () => { }); }); - it('allow inviting a user to a party if they are partying solo', async () => { - const userToInvite = await generateUser(); - await userToInvite.post('/groups', { // add user to a party - name: 'Another Test Party', - type: 'party', - }); - - await inviter.post(`/groups/${party._id}/invite`, { - uuids: [userToInvite._id], - }); - expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id); - }); - - it('allow inviting a user to 2 different parties', async () => { + it('allows inviting a user to 2 different parties', async () => { // Create another inviter const inviter2 = await generateUser(); @@ -635,29 +623,47 @@ describe('Post /groups/:groupId/invite', () => { }); expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id); }); + }); + + describe('party size limits', () => { + let party, partyLeader; + + beforeEach(async () => { + group = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Party', + type: 'party', + privacy: 'private', + }, + // Generate party with 20 members + members: PARTY_LIMIT_MEMBERS - 10, + }); + party = group.group; + partyLeader = group.groupLeader; + }); it('allows 30 members in a party', async () => { const invitesToGenerate = []; - // Generate 29 users to invite (29 + leader = 30 members) - for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i += 1) { + // Generate 10 new invites + for (let i = 1; i < 10; i += 1) { invitesToGenerate.push(generateUser()); } const generatedInvites = await Promise.all(invitesToGenerate); // Invite users - expect(await inviter.post(`/groups/${party._id}/invite`, { + expect(await partyLeader.post(`/groups/${party._id}/invite`, { uuids: generatedInvites.map(invite => invite._id), })).to.be.an('array'); }).timeout(10000); - it('does not allow 30+ members in a party', async () => { + it('does not allow >30 members in a party', async () => { const invitesToGenerate = []; - // Generate 30 users to invite (30 + leader = 31 members) - for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) { + // Generate 11 invites + for (let i = 1; i < 11; i += 1) { invitesToGenerate.push(generateUser()); } const generatedInvites = await Promise.all(invitesToGenerate); // Invite users - await expect(inviter.post(`/groups/${party._id}/invite`, { + await expect(partyLeader.post(`/groups/${party._id}/invite`, { uuids: generatedInvites.map(invite => invite._id), })) .to.eventually.be.rejected.and.eql({ diff --git a/test/helpers/api-integration/requester.js b/test/helpers/api-integration/requester.js index ad01f35978..61c0b2fabf 100644 --- a/test/helpers/api-integration/requester.js +++ b/test/helpers/api-integration/requester.js @@ -53,7 +53,8 @@ function _requestMaker (user, method, additionalSets = {}) { if (user && user._id && user.apiToken) { request .set('x-api-user', user._id) - .set('x-api-key', user.apiToken); + .set('x-api-key', user.apiToken) + .set('x-client', 'habitica-web'); } if (!isEmpty(additionalSets)) { diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index c2c9f08756..8a5531760f 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -27905,6 +27905,31 @@ width: 114px; height: 90px; } +.back_mystery_202305 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202305.png'); + width: 114px; + height: 90px; +} +.headAccessory_mystery_202305 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_mystery_202305.png'); + width: 114px; + height: 90px; +} +.shop_back_mystery_202305 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_back_mystery_202305.png'); + width: 68px; + height: 68px; +} +.shop_headAccessory_mystery_202305 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_mystery_202305.png'); + width: 68px; + height: 68px; +} +.shop_set_mystery_202305 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202305.png'); + width: 68px; + height: 68px; +} .broad_armor_mystery_301404 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png'); width: 90px; diff --git a/website/client/src/assets/scss/icon.scss b/website/client/src/assets/scss/icon.scss index 92930f1124..3ab036333a 100644 --- a/website/client/src/assets/scss/icon.scss +++ b/website/client/src/assets/scss/icon.scss @@ -24,9 +24,9 @@ } } -.icon-16 { - width: 16px; - height: 16px; +.icon-10 { + width: 10px; + height: 10px; } .icon-12 { @@ -34,21 +34,26 @@ height: 12px; } +.icon-16 { + width: 16px; + height: 16px; +} + .icon-24 { width: 24px; height: 24px; } +.icon-32 { + width: 32px; + height: 32px; +} + .icon-48 { width: 48px; height: 48px; } -.icon-10 { - width: 10px; - height: 10px; -} - .inline { display: inline-block; } diff --git a/website/client/src/assets/svg/sync-2.svg b/website/client/src/assets/svg/sync-2.svg new file mode 100644 index 0000000000..61a77879df --- /dev/null +++ b/website/client/src/assets/svg/sync-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/client/src/components/admin-panel/user-support/cronAndAuth.vue b/website/client/src/components/admin-panel/user-support/cronAndAuth.vue index 8b107ebf21..624992ae70 100644 --- a/website/client/src/components/admin-panel/user-support/cronAndAuth.vue +++ b/website/client/src/components/admin-panel/user-support/cronAndAuth.vue @@ -22,6 +22,10 @@ Account created: {{ hero.auth.timestamps.created | formatDate }} +
+ User has employed third party tools. Last known usage: + {{ hero.flags.thirdPartyTools | formatDate }} +
"lastCron" value: {{ hero.lastCron | formatDate }} diff --git a/website/client/src/components/groups/createPartyModal.vue b/website/client/src/components/groups/createPartyModal.vue index 8b00d4e06c..17c0184331 100644 --- a/website/client/src/components/groups/createPartyModal.vue +++ b/website/client/src/components/groups/createPartyModal.vue @@ -11,9 +11,12 @@
-

+

{{ $t('playInPartyTitle') }} -

+

+
-
+
-

- {{ $t('wantToJoinPartyTitle') }} -

-

-
-
- -
-
- @ -
- {{ user.auth.local.username }} -
-
-
- {{ $t('copy') }} -
-
+ {{ $t('wantToJoinPartyTitle') }} + +

+

+
+
+ {{ $t('currentlyLookingForParty') }} +
+
+
+
- @@ -107,15 +134,27 @@ cursor: pointer; } + .green-bar { + height: 32px; + font-size: 14px; + font-weight: bold; + line-height: 1.71; + text-align: center; + color: $green-1; + background-color: $green-100; + border-radius: 2px; + padding: 4px 0px 4px 0px; + } + .grey-row { background-color: $gray-700; color: #4e4a57; padding: 2em; - border-radius: 0px 0px 2px 2px; + border-radius: 0px 0px 8px 8px; } - h2 { - color: $gray-100; + h1 { + color: $purple-300; } .header-wrap { @@ -132,10 +171,6 @@ border-radius: 2px 2px 0 0; image-rendering: optimizequality; } - - h2 { - color: $purple-200; - } } .heading { @@ -182,6 +217,21 @@ margin: 0.75rem auto 0.75rem 0.25rem; } + p { + line-height: 1.71; + } + + .red-link { + cursor: pointer; + font-size: 14px; + line-height: 1.71; + text-align: center; + color: $maroon-50; + &:hover { + text-decoration: underline; + } + } + .small { color: $gray-200; margin: auto 0.5rem auto 0.25rem; @@ -192,21 +242,35 @@ import { mapState } from '@/libs/store'; import * as Analytics from '@/libs/analytics'; import notifications from '@/mixins/notifications'; +import closeX from '../ui/closeX'; import copyIcon from '@/assets/svg/copy.svg'; export default { + components: { + closeX, + }, mixins: [notifications], data () { return { icons: Object.freeze({ copy: copyIcon, }), + seeking: false, }; }, computed: { ...mapState({ user: 'user.data' }), }, + mounted () { + this.seeking = Boolean(this.user.party.seeking); + Analytics.track({ + eventName: 'Start a Party button', + eventAction: 'Start a Party button', + eventCategory: 'behavior', + hitType: 'event', + }, { trackOnClient: true }); + }, methods: { async createParty () { const group = { @@ -223,7 +287,10 @@ export default { }); this.$root.$emit('bv::hide::modal', 'create-party-modal'); - this.$router.push('/party'); + await this.$router.push('/party'); + }, + close () { + this.$root.$emit('bv::hide::modal', 'create-party-modal'); }, copyUsername () { if (navigator.clipboard) { @@ -238,6 +305,12 @@ export default { } this.text(this.$t('usernameCopied')); }, + seekParty () { + this.$store.dispatch('user:set', { + 'party.seeking': !this.user.party.seeking ? new Date() : null, + }); + this.seeking = !this.seeking; + }, }, }; diff --git a/website/client/src/components/groups/group.vue b/website/client/src/components/groups/group.vue index 85dbf296af..a238ef5664 100644 --- a/website/client/src/components/groups/group.vue +++ b/website/client/src/components/groups/group.vue @@ -542,7 +542,8 @@ export default { await this.$store.dispatch('guilds:leave', data); if (this.isParty) { - this.$router.push({ name: 'tasks' }); + await this.$router.push({ name: 'tasks' }); + window.location.reload(true); } }, upgradeGroup () { diff --git a/website/client/src/components/groups/groupPlan.vue b/website/client/src/components/groups/groupPlan.vue index c628774d9e..6d84aecfc5 100644 --- a/website/client/src/components/groups/groupPlan.vue +++ b/website/client/src/components/groups/groupPlan.vue @@ -4,12 +4,12 @@
-

- Need more for your Group? +

+ {{ $t('groupPlanTitle') }}

-

+

{{ $t('groupBenefitsDescription') }}

@@ -24,8 +24,8 @@ src="~@/assets/images/group-plans/group-14@3x.png" >
-

{{ $t('teamBasedTasks') }}

-

Set up an easily-viewed shared task list for the group. Assign tasks to your fellow group members, or let them claim their own tasks to make it clear what everyone is working on!

+

{{ $t('teamBasedTasks') }}

+

{{ $t('teamBasedTasksListDesc') }}

@@ -35,8 +35,8 @@ src="~@/assets/images/group-plans/group-12@3x.png" >
-

Group Management Controls

-

Use task approvals to verify that a task that was really completed, add Group Managers to share responsibilities, and enjoy a private group chat for all team members.

+

{{ $t('groupManagementControls') }}

+

{{ $t('groupManagementControlsDesc') }}

@@ -46,8 +46,8 @@ src="~@/assets/images/group-plans/group-13@3x.png" >
-

In-Game Benefits

-

Group members get an exclusive Jackalope Mount, as well as full subscription benefits, including special monthly equipment sets and the ability to buy gems with gold.

+

{{ $t('inGameBenefits') }}

+

{{ $t('inGameBenefitsDesc') }}

diff --git a/website/client/src/components/groups/lookingForParty.vue b/website/client/src/components/groups/lookingForParty.vue new file mode 100644 index 0000000000..eda06f5759 --- /dev/null +++ b/website/client/src/components/groups/lookingForParty.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/website/client/src/components/groups/rightSidebar.vue b/website/client/src/components/groups/rightSidebar.vue index f5eafdcaf6..cab95cea2e 100644 --- a/website/client/src/components/groups/rightSidebar.vue +++ b/website/client/src/components/groups/rightSidebar.vue @@ -2,7 +2,7 @@