diff --git a/.gitignore b/.gitignore
index f1d956d2e2..306a751683 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@ dist-client
test/client/unit/coverage
test/client/e2e/reports
test/client-old/spec/mocks/translations.js
+yarn.lock
# Elastic Beanstalk Files
.elasticbeanstalk/*
diff --git a/test/api/v3/integration/challenges/POST-challenges.test.js b/test/api/v3/integration/challenges/POST-challenges.test.js
index c70b41806f..0176a729ff 100644
--- a/test/api/v3/integration/challenges/POST-challenges.test.js
+++ b/test/api/v3/integration/challenges/POST-challenges.test.js
@@ -304,14 +304,14 @@ describe('POST /challenges', () => {
expect(groupLeader.challenges.length).to.equal(0);
});
- it('awards achievement if this is creator\'s first challenge', async () => {
+ it('does not award joinedChallenge achievement for creating a challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
- expect(groupLeader.achievements.joinedChallenge).to.be.true;
+ expect(groupLeader.achievements.joinedChallenge).to.not.be.true;
});
it('sets summary to challenges name when not supplied', async () => {
diff --git a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js
index d2f321c1fa..cbbe45b546 100644
--- a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js
+++ b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js
@@ -77,7 +77,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
- 'chair', 'costume', 'sleep', 'background', 'tasks',
+ 'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -98,7 +98,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
- 'chair', 'costume', 'sleep', 'background', 'tasks',
+ 'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
diff --git a/test/api/v3/integration/members/GET-members_id.test.js b/test/api/v3/integration/members/GET-members_id.test.js
index 7a4f23fb80..1dca0284b9 100644
--- a/test/api/v3/integration/members/GET-members_id.test.js
+++ b/test/api/v3/integration/members/GET-members_id.test.js
@@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
- 'chair', 'costume', 'sleep', 'background', 'tasks',
+ 'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
diff --git a/test/api/v3/integration/notifications/prevent-multiple-notification.js b/test/api/v3/integration/notifications/prevent-multiple-notification.js
new file mode 100644
index 0000000000..6fcf655d55
--- /dev/null
+++ b/test/api/v3/integration/notifications/prevent-multiple-notification.js
@@ -0,0 +1,39 @@
+import {
+ createAndPopulateGroup,
+} from '../../../../helpers/api-integration/v3';
+
+describe('Prevent multiple notifications', () => {
+ let partyLeader, partyMembers, party;
+
+ before(async () => {
+ let { group, groupLeader, members } = await createAndPopulateGroup({
+ groupDetails: {
+ type: 'party',
+ privacy: 'private',
+ },
+ members: 4,
+ });
+
+ party = group;
+ partyLeader = groupLeader;
+ partyMembers = members;
+ });
+
+ it('does not add the same notification twice', async () => {
+ const multipleChatMessages = [];
+
+ for (let i = 0; i < 4; i++) {
+ for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex++) {
+ multipleChatMessages.push(
+ partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}`}),
+ );
+ }
+ }
+
+ await Promise.all(multipleChatMessages);
+
+ const userWithNotification = await partyLeader.get('/user');
+
+ expect(userWithNotification.notifications.length).to.be.eq(1);
+ });
+});
diff --git a/website/client/components/challenges/challengeDetail.vue b/website/client/components/challenges/challengeDetail.vue
index 7464cb16bc..62a429f64c 100644
--- a/website/client/components/challenges/challengeDetail.vue
+++ b/website/client/components/challenges/challengeDetail.vue
@@ -344,14 +344,14 @@ export default {
this.tasksByType[task.type].splice(index, 1);
},
showMemberModal () {
- // @TODO: Change these to options and add a custom event to members modal
- this.$store.state.memberModalOptions.challengeId = this.challenge._id;
- this.$store.state.memberModalOptions.groupId = 'challenge'; // @TODO: change these terrible settings
- this.$store.state.memberModalOptions.group = this.group;
- this.$store.state.memberModalOptions.memberCount = this.challenge.memberCount;
- this.$store.state.memberModalOptions.viewingMembers = this.members;
- this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
- this.$root.$emit('bv::show::modal', 'members-modal');
+ this.$root.$emit('habitica:show-member-modal', {
+ challengeId: this.challenge._id,
+ groupId: 'challenge', // @TODO: change these terrible settings
+ group: this.group,
+ memberCount: this.challenge.memberCount,
+ viewingMembers: this.members,
+ fetchMoreMembers: this.loadMembers,
+ });
},
async joinChallenge () {
this.user.challenges.push(this.searchId);
diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue
index 018b93e064..6f63f305b7 100644
--- a/website/client/components/groups/group.vue
+++ b/website/client/components/groups/group.vue
@@ -3,6 +3,7 @@
group-form-modal(v-if='isParty')
start-quest-modal(:group='this.group')
quest-details-modal(:group='this.group')
+ participant-list-modal(:group='this.group')
group-gems-modal
.col-12.col-sm-8.standard-page
.row
@@ -126,7 +127,7 @@
.sidebar {
background-color: $gray-600;
padding-bottom: 2em;
-
+
}
.buttons-wrapper {
@@ -262,6 +263,7 @@ import * as Analytics from 'client/libs/analytics';
import membersModal from './membersModal';
import startQuestModal from './startQuestModal';
import questDetailsModal from './questDetailsModal';
+import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupChallenges from '../challenges/groupChallenges';
import groupGemsModal from 'client/components/groups/groupGemsModal';
@@ -292,6 +294,7 @@ export default {
groupFormModal,
groupChallenges,
questDetailsModal,
+ participantListModal,
groupGemsModal,
questSidebarSection,
sidebarSection,
@@ -426,12 +429,13 @@ export default {
return this.$store.dispatch('members:getGroupMembers', payload);
},
showMemberModal () {
- this.$store.state.memberModalOptions.groupId = this.group._id;
- this.$store.state.memberModalOptions.group = this.group;
- this.$store.state.memberModalOptions.memberCount = this.group.memberCount;
- this.$store.state.memberModalOptions.viewingMembers = this.members;
- this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
- this.$root.$emit('bv::show::modal', 'members-modal');
+ this.$root.$emit('habitica:show-member-modal', {
+ groupId: this.group._id,
+ group: this.group,
+ memberCount: this.group.memberCount,
+ viewingMembers: this.members,
+ fetchMoreMembers: this.loadMembers,
+ });
},
fetchRecentMessages () {
this.fetchGuild();
@@ -538,24 +542,6 @@ export default {
this.$store.state.upgradingGroup = this.group;
this.$router.push('/group-plans');
},
- clickStartQuest () {
- Analytics.track({
- hitType: 'event',
- eventCategory: 'button',
- eventAction: 'click',
- eventLabel: 'Start a Quest',
- });
-
- let hasQuests = find(this.user.items.quests, (quest) => {
- return quest > 0;
- });
-
- if (hasQuests) {
- this.$root.$emit('bv::show::modal', 'start-quest-modal');
- return;
- }
- // $rootScope.$state.go('options.inventory.quests');
- },
showGroupGems () {
this.$root.$emit('bv::show::modal', 'group-gems-modal');
},
diff --git a/website/client/components/groups/membersModal.vue b/website/client/components/groups/membersModal.vue
index 1ce8bc98af..011a9ec95b 100644
--- a/website/client/components/groups/membersModal.vue
+++ b/website/client/components/groups/membersModal.vue
@@ -278,7 +278,20 @@ export default {
};
},
mounted () {
- this.getMembers();
+ this.$root.$on('habitica:show-member-modal', (data) => {
+ // @TODO: Remove store
+ this.$store.state.memberModalOptions.challengeId = data.challengeId;
+ this.$store.state.memberModalOptions.groupId = data.groupId;
+ this.$store.state.memberModalOptions.group = data.group;
+ this.$store.state.memberModalOptions.memberCount = data.memberCount;
+ this.$store.state.memberModalOptions.viewingMembers = data.viewingMembers;
+ this.$store.state.memberModalOptions.fetchMoreMembers = data.fetchMoreMembers;
+ this.$root.$emit('bv::show::modal', 'members-modal');
+ this.getMembers();
+ });
+ },
+ destroyed () {
+ this.$root.$off('habitica:show-member-modal');
},
computed: {
...mapState({user: 'user.data'}),
@@ -363,6 +376,7 @@ export default {
});
this.invites = invites;
}
+
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
this.members = this.$store.state.memberModalOptions.viewingMembers;
}
diff --git a/website/client/components/groups/participantListModal.vue b/website/client/components/groups/participantListModal.vue
new file mode 100644
index 0000000000..1495730bb1
--- /dev/null
+++ b/website/client/components/groups/participantListModal.vue
@@ -0,0 +1,93 @@
+
+ b-modal#participant-list(size='md', :hide-footer='true')
+ .header-wrap(slot="modal-header")
+ .row
+ .col-6
+ h1(v-once) {{ $t('participantsTitle') }}
+ .col-6
+ button(type="button" aria-label="Close" class="close", @click='close()')
+ span(aria-hidden="true") ×
+ .row(v-for='member in participants')
+ .col-12.no-padding-left
+ member-details(:member='member')
+ .modal-footer
+ button.btn.btn-primary(@click='close()') {{ $t('close') }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/website/client/components/groups/questSidebarSection.vue b/website/client/components/groups/questSidebarSection.vue
index 45b5fbc212..8c09ee5c94 100644
--- a/website/client/components/groups/questSidebarSection.vue
+++ b/website/client/components/groups/questSidebarSection.vue
@@ -24,6 +24,9 @@ sidebar-section(:title="$t('questDetailsTitle')")
h3(v-once) {{ questData.text() }}
.quest-box
.collect-info(v-if='questData.collect')
+ .row
+ .col-12
+ a.float-right(@click="openParticipantList()") {{ $t('participantsTitle') }}
.row(v-for='(value, key) in questData.collect')
.col-2
div(:class="'quest_' + questData.key + '_' + key")
@@ -38,7 +41,7 @@ sidebar-section(:title="$t('questDetailsTitle')")
.col-6
h4.float-left(v-once) {{ questData.boss.name() }}
.col-6
- span.float-right(v-once) {{ $t('participantsTitle') }}
+ a.float-right(@click="openParticipantList()") {{ $t('participantsTitle') }}
.row
.col-12
.grey-progress-bar
@@ -134,6 +137,12 @@ sidebar-section(:title="$t('questDetailsTitle')")
padding: .5em;
margin-bottom: 1em;
+ a {
+ font-family: 'Roboto Condensed', sans-serif;
+ font-weight: bold;
+ color: $gray-10;
+ }
+
svg: {
width: 100%;
height: 100%;
@@ -258,6 +267,9 @@ export default {
openQuestDetails () {
this.$root.$emit('bv::show::modal', 'quest-details');
},
+ openParticipantList () {
+ this.$root.$emit('bv::show::modal', 'participant-list');
+ },
async questAbort () {
if (!confirm(this.$t('sureAbort'))) return;
if (!confirm(this.$t('doubleSureAbort'))) return;
diff --git a/website/client/components/groups/startQuestModal.vue b/website/client/components/groups/startQuestModal.vue
index 66bee4afe1..9d13f2d0b3 100644
--- a/website/client/components/groups/startQuestModal.vue
+++ b/website/client/components/groups/startQuestModal.vue
@@ -148,8 +148,13 @@ export default {
};
},
mounted () {
- let questKeys = Object.keys(this.user.items.quests);
- this.selectedQuest = questKeys[0];
+ const userQuests = this.user.items.quests;
+ for (const key in userQuests) {
+ if (userQuests[key] > 0) {
+ this.selectedQuest = key;
+ break;
+ }
+ }
this.$root.$on('selectQuest', this.selectQuest);
},
@@ -177,13 +182,14 @@ export default {
let groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest;
- const response = await this.$store.dispatch('guilds:inviteToQuest', {groupId, key});
- const quest = response.data.data;
-
- if (this.$store.state.party.data) this.$store.state.party.data.quest = quest;
-
- this.loading = false;
+ try {
+ const response = await this.$store.dispatch('guilds:inviteToQuest', {groupId, key});
+ const quest = response.data.data;
+ if (this.$store.state.party.data) this.$store.state.party.data.quest = quest;
+ } finally {
+ this.loading = false;
+ }
this.$root.$emit('bv::hide::modal', 'start-quest-modal');
},
},
diff --git a/website/client/components/header/index.vue b/website/client/components/header/index.vue
index 4fa0e750cf..fb7e2965c0 100644
--- a/website/client/components/header/index.vue
+++ b/website/client/components/header/index.vue
@@ -176,11 +176,11 @@ export default {
}
},
showPartyMembers () {
- // Set the party details for the members-modal component
- this.$store.state.memberModalOptions.groupId = this.user.party._id;
- this.$store.state.memberModalOptions.viewingMembers = this.partyMembers;
- this.$store.state.memberModalOptions.group = this.user.party;
- this.$root.$emit('bv::show::modal', 'members-modal');
+ this.$root.$emit('habitica:show-member-modal', {
+ groupId: this.user.party._id,
+ viewingMembers: this.partyMembers,
+ group: this.user.party,
+ });
},
setPartyMembersWidth ($event) {
if (this.currentWidth !== $event.width) {
diff --git a/website/client/components/inventory/stable/hatchingModal.vue b/website/client/components/inventory/stable/hatchingModal.vue
new file mode 100644
index 0000000000..cfd89d47bc
--- /dev/null
+++ b/website/client/components/inventory/stable/hatchingModal.vue
@@ -0,0 +1,84 @@
+
+b-modal#hatching-modal(@change="resetHatchablePet($event)")
+ div.content(v-if="hatchablePet")
+ div.potionEggGroup
+ div.potionEggBackground
+ div(:class="'Pet_HatchingPotion_'+hatchablePet.potionKey")
+ div.potionEggBackground
+ div(:class="'Pet_Egg_'+hatchablePet.eggKey")
+ h4.title {{ hatchablePet.name }}
+ div.text(v-html="$t('hatchDialogText', { potionName: hatchablePet.potionName, eggName: hatchablePet.eggName, petName: hatchablePet.name })")
+ span.svg-icon.icon-10(v-html="icons.close", slot="modal-header", @click="closeHatchPetDialog()")
+ div(slot="modal-footer")
+ button.btn.btn-primary(@click="hatchPet(hatchablePet)") {{ $t('hatch') }}
+ button.btn.btn-secondary.btn-flat(@click="closeHatchPetDialog()") {{ $t('cancel') }}
+
+
+
+
+
diff --git a/website/client/components/inventory/stable/index.vue b/website/client/components/inventory/stable/index.vue
index 641e091b1d..8024afae6f 100644
--- a/website/client/components/inventory/stable/index.vue
+++ b/website/client/components/inventory/stable/index.vue
@@ -1,245 +1,205 @@
- // @TODO: breakdown to componentes and use some SOLID
- .row.stable(v-mousePosition="30", @mouseMoved="mouseMoved($event)")
- .standard-sidebar.d-none.d-sm-block
- div
- #npmMattStable.npc_matt
- b-popover(
- triggers="hover",
- placement="right",
- target="npmMattStable"
- )
- h4.popover-content-title(v-once) {{ $t('mattBoch') }}
- .popover-content-text(v-once) {{ $t('mattBochText1') }}
-
+.row.stable(v-mousePosition="30", @mouseMoved="mouseMoved($event)")
+ .standard-sidebar.d-none.d-sm-block
+ div
+ #npmMattStable.npc_matt
+ b-popover(
+ triggers="hover",
+ placement="right",
+ target="npmMattStable"
+ )
+ h4.popover-content-title(v-once) {{ $t('mattBoch') }}
+ .popover-content-text(v-once) {{ $t('mattBochText1') }}
+ .form-group
+ input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
+ .form
+ h2(v-once) {{ $t('filter') }}
+ h3(v-once) {{ $t('pets') }}
.form-group
- input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
-
- .form
- h2(v-once) {{ $t('filter') }}
- h3(v-once) {{ $t('pets') }}
- .form-group
- .form-check(
- v-for="petGroup in petGroups",
- :key="petGroup.key"
- )
- .custom-control.custom-checkbox
- input.custom-control-input(
- type="checkbox",
- v-model="viewOptions[petGroup.key].selected",
- :disabled="viewOptions[petGroup.key].animalCount == 0",
- :id="petGroup.key",
- )
- label.custom-control-label(v-once, :for="petGroup.key") {{ petGroup.label }}
- h3(v-once) {{ $t('mounts') }}
- .form-group
- .form-check(
- v-for="mountGroup in mountGroups",
- :key="mountGroup.key"
- )
- .custom-control.custom-checkbox
- input.custom-control-input(
- type="checkbox",
- v-model="viewOptions[mountGroup.key].selected",
- :disabled="viewOptions[mountGroup.key].animalCount == 0",
- :id="mountGroup.key",
- )
- label.custom-control-label(v-once, :for="mountGroup.key") {{ mountGroup.label }}
-
- div.form-group.clearfix
- h3.float-left {{ $t('hideMissing') }}
- toggle-switch.float-right(
- :checked="hideMissing",
- @change="updateHideMissing"
- )
- .standard-page
- .clearfix
- h1.float-left.mb-4.page-header(v-once) {{ $t('stable') }}
-
- div.float-right
- span.dropdown-label {{ $t('sortBy') }}
- b-dropdown(:text="$t(selectedSortBy)", right=true)
- b-dropdown-item(
- v-for="sort in sortByItems",
- @click="selectedSortBy = sort",
- :active="selectedSortBy === sort",
- :key="sort"
- ) {{ $t(sort) }}
-
- h2.mb-3
- | {{ $t('pets') }}
- |
- span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
-
- div(
- v-for="(petGroup, index) in petGroups",
- v-if="viewOptions[petGroup.key].selected",
- :key="petGroup.key"
- )
- h4(v-if="viewOptions[petGroup.key].animalCount !== 0") {{ petGroup.label }}
-
- .pet-row.d-flex(
- v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)",
- v-if='index === 0 || $_openedItemRows_isToggled(petGroup.key)')
- .pet-group(
- v-for='item in group'
- v-drag.drop.food="item.key",
- @itemDragOver="onDragOver($event, item)",
- @itemDropped="onDrop($event, item)",
- @itemDragLeave="onDragLeave()",
- :class="{'last': item.isLastInRow}"
- )
- petItem(
- :item="item",
- :itemContentClass="getPetItemClass(item)",
- :popoverPosition="'top'",
- :progress="item.progress",
- :emptyItem="!item.isOwned()",
- :showPopover="currentDraggingFood == null",
- :highlightBorder="highlightPet == item.key",
- @click="petClicked(item)"
- )
- span(slot="popoverContent")
- div.hatchablePopover(v-if="item.isHatchable()")
- h4.popover-content-title {{ item.name }}
- div.popover-content-text(v-html="$t('haveHatchablePet', { potion: item.potionName, egg: item.eggName })")
- div.potionEggGroup
- div.potionEggBackground
- div(:class="'Pet_HatchingPotion_'+item.potionKey")
- div.potionEggBackground
- div(:class="'Pet_Egg_'+item.eggKey")
- div(v-else)
- h4.popover-content-title {{ item.name }}
- template(slot="itemBadge", slot-scope="context")
- starBadge(:selected="item.key === currentPet", :show="item.isOwned()", @click="selectPet(item)")
-
- .btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
- | {{ $_openedItemRows_isToggled(petGroup.key) ? $t('showLess') : $t('showMore') }}
-
- h2
- | {{ $t('mounts') }}
- |
- span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
-
- div(
- v-for="mountGroup in mountGroups",
- v-if="viewOptions[mountGroup.key].selected",
- :key="mountGroup.key"
- )
- h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
-
- .pet-row.d-flex(v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
- v-if='index === 0 || $_openedItemRows_isToggled(mountGroup.key)')
- .pet-group(v-for='item in group')
- mountItem(
- :item="item",
- :itemContentClass="item.isOwned() ? ('Mount_Icon_' + item.key) : 'PixelPaw GreyedOut'",
- :key="item.key",
- :popoverPosition="'top'",
- :emptyItem="!item.isOwned()",
- :showPopover="true",
- @click="selectMount(item)"
- )
- span(slot="popoverContent")
- h4.popover-content-title {{ item.name }}
- template(slot="itemBadge", slot-scope="context")
- starBadge(
- :selected="item.key === currentMount",
- :show="item.isOwned()",
- @click="selectMount(item)",
- )
-
- .btn.btn-flat.btn-show-more(@click="setShowMore(mountGroup.key)", v-if='mountGroup.key !== "specialMounts"')
- | {{ $_openedItemRows_isToggled(mountGroup.key) ? $t('showLess') : $t('showMore') }}
-
- drawer(
- :title="$t('quickInventory')",
- :errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? ((selectedDrawerTab === 0) ? $t('noFoodAvailable') : $t('noSaddlesAvailable')) : null"
- )
- div(slot="drawer-header")
- .drawer-tab-container
- .drawer-tab.text-right
- a.drawer-tab-text(
- @click="selectedDrawerTab = 0",
- :class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
- ) {{ drawerTabs[0].label }}
- .clearfix
- .drawer-tab.float-left
- a.drawer-tab-text(
- @click="selectedDrawerTab = 1",
- :class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
- ) {{ drawerTabs[1].label }}
-
- #petLikeToEatStable.drawer-help-text(v-once)
- | {{ $t('petLikeToEat') + ' ' }}
- span.svg-icon.inline.icon-16(v-html="icons.information")
- b-popover(
- target="petLikeToEatStable"
- placement="top"
- )
- .popover-content-text(v-html="$t('petLikeToEatText')", v-once)
- drawer-slider(
- :items="drawerTabs[selectedDrawerTab].items",
- :scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
- slot="drawer-slider",
- :itemWidth=94,
- :itemMargin=24,
- :itemType="selectedDrawerTab"
+ .form-check(
+ v-for="petGroup in petGroups",
+ :key="petGroup.key"
)
- template(slot="item", slot-scope="context")
- foodItem(
- :item="context.item",
- :itemCount="userItems.food[context.item.key]",
- :active="currentDraggingFood == context.item",
- @itemDragEnd="onDragEnd()",
- @itemDragStart="onDragStart($event, context.item)",
- @itemClick="onFoodClicked($event, context.item)"
+ .custom-control.custom-checkbox
+ input.custom-control-input(
+ type="checkbox",
+ v-model="viewOptions[petGroup.key].selected",
+ :disabled="viewOptions[petGroup.key].animalCount == 0",
+ :id="petGroup.key",
)
- b-modal#welcome-modal(
- :ok-only="true",
- :ok-title="$t('gotIt')",
- :visible="!hideDialog",
- :hide-header="true",
- @hide="hideFlag()"
- )
- div.content
- div.npc_matt
- h1.page-header(v-once) {{ $t('welcomeStable') }}
- div.content-text(v-once) {{ $t('welcomeStableText') }}
- b-modal#hatching-modal(
- @change="resetHatchablePet($event)"
- )
- div.content(v-if="hatchablePet")
- div.potionEggGroup
- div.potionEggBackground
- div(:class="'Pet_HatchingPotion_'+hatchablePet.potionKey")
- div.potionEggBackground
- div(:class="'Pet_Egg_'+hatchablePet.eggKey")
- h4.title {{ hatchablePet.name }}
- div.text(v-html="$t('hatchDialogText', { potionName: hatchablePet.potionName, eggName: hatchablePet.eggName, petName: hatchablePet.name })")
+ label.custom-control-label(v-once, :for="petGroup.key") {{ petGroup.label }}
+ h3(v-once) {{ $t('mounts') }}
+ .form-group
+ .form-check(
+ v-for="mountGroup in mountGroups",
+ :key="mountGroup.key"
+ )
+ .custom-control.custom-checkbox
+ input.custom-control-input(
+ type="checkbox",
+ v-model="viewOptions[mountGroup.key].selected",
+ :disabled="viewOptions[mountGroup.key].animalCount == 0",
+ :id="mountGroup.key",
+ )
+ label.custom-control-label(v-once, :for="mountGroup.key") {{ mountGroup.label }}
- span.svg-icon.icon-10(v-html="icons.close", slot="modal-header", @click="closeHatchPetDialog()")
+ div.form-group.clearfix
+ h3.float-left {{ $t('hideMissing') }}
+ toggle-switch.float-right(
+ :checked="hideMissing",
+ @change="updateHideMissing"
+ )
+ .standard-page
+ .clearfix
+ h1.float-left.mb-4.page-header(v-once) {{ $t('stable') }}
- div(slot="modal-footer")
- button.btn.btn-primary(@click="hatchPet(hatchablePet)") {{ $t('hatch') }}
- button.btn.btn-secondary.btn-flat(@click="closeHatchPetDialog()") {{ $t('cancel') }}
+ div.float-right
+ span.dropdown-label {{ $t('sortBy') }}
+ b-dropdown(:text="$t(selectedSortBy)", right=true)
+ b-dropdown-item(
+ v-for="sort in sortByItems",
+ @click="selectedSortBy = sort",
+ :active="selectedSortBy === sort",
+ :key="sort"
+ ) {{ $t(sort) }}
- hatchedPetDialog(
- :hideText="true",
- )
+ h2.mb-3
+ | {{ $t('pets') }}
+ |
+ span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
- div.foodInfo(ref="dragginFoodInfo")
- div(v-if="currentDraggingFood != null")
- div.food-icon(:class="'Pet_Food_'+currentDraggingFood.key")
- div.popover
- div.popover-content {{ $t('dragThisFood', {foodName: currentDraggingFood.text() }) }}
+ div(v-for="(petGroup, index) in petGroups",
+ v-if="viewOptions[petGroup.key].selected",
+ :key="petGroup.key")
+ h4(v-if="viewOptions[petGroup.key].animalCount !== 0") {{ petGroup.label }}
- div.foodInfo.mouse(ref="clickFoodInfo", v-if="foodClickMode")
- div(v-if="currentDraggingFood != null")
- div.food-icon(:class="'Pet_Food_'+currentDraggingFood.key")
- div.popover
- div.popover-content {{ $t('clickOnPetToFeed', {foodName: currentDraggingFood.text() }) }}
- mount-raised-modal
+ .pet-row.d-flex(
+ v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)",
+ v-if='index === 0 || $_openedItemRows_isToggled(petGroup.key)')
+ .pet-group(
+ v-for='item in group'
+ v-drag.drop.food="item.key",
+ @itemDragOver="onDragOver($event, item)",
+ @itemDropped="onDrop($event, item)",
+ @itemDragLeave="onDragLeave()",
+ :class="{'last': item.isLastInRow}"
+ )
+ petItem(
+ :item="item",
+ :itemContentClass="getPetItemClass(item)",
+ :popoverPosition="'top'",
+ :progress="item.progress",
+ :emptyItem="!item.isOwned()",
+ :showPopover="currentDraggingFood == null",
+ :highlightBorder="highlightPet == item.key",
+ @click="petClicked(item)"
+ )
+ span(slot="popoverContent")
+ div.hatchablePopover(v-if="item.isHatchable()")
+ h4.popover-content-title {{ item.name }}
+ div.popover-content-text(v-html="$t('haveHatchablePet', { potion: item.potionName, egg: item.eggName })")
+ div.potionEggGroup
+ div.potionEggBackground
+ div(:class="'Pet_HatchingPotion_'+item.potionKey")
+ div.potionEggBackground
+ div(:class="'Pet_Egg_'+item.eggKey")
+ div(v-else)
+ h4.popover-content-title {{ item.name }}
+ template(slot="itemBadge", slot-scope="context")
+ starBadge(:selected="item.key === currentPet", :show="item.isOwned()", @click="selectPet(item)")
+
+ .btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
+ | {{ $_openedItemRows_isToggled(petGroup.key) ? $t('showLess') : $t('showMore') }}
+
+ h2
+ | {{ $t('mounts') }}
+ |
+ span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
+
+ div(v-for="mountGroup in mountGroups",
+ v-if="viewOptions[mountGroup.key].selected",
+ :key="mountGroup.key")
+ h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
+
+ .pet-row.d-flex(v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
+ v-if='index === 0 || $_openedItemRows_isToggled(mountGroup.key)')
+ .pet-group(v-for='item in group')
+ mountItem(
+ :item="item",
+ :itemContentClass="item.isOwned() ? ('Mount_Icon_' + item.key) : 'PixelPaw GreyedOut'",
+ :key="item.key",
+ :popoverPosition="'top'",
+ :emptyItem="!item.isOwned()",
+ :showPopover="true",
+ @click="selectMount(item)"
+ )
+ span(slot="popoverContent")
+ h4.popover-content-title {{ item.name }}
+ template(slot="itemBadge", slot-scope="context")
+ starBadge(
+ :selected="item.key === currentMount",
+ :show="item.isOwned()",
+ @click="selectMount(item)",
+ )
+
+ .btn.btn-flat.btn-show-more(@click="setShowMore(mountGroup.key)", v-if='mountGroup.key !== "specialMounts"')
+ | {{ $_openedItemRows_isToggled(mountGroup.key) ? $t('showLess') : $t('showMore') }}
+
+ drawer(:title="$t('quickInventory')",
+ :errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? ((selectedDrawerTab === 0) ? $t('noFoodAvailable') : $t('noSaddlesAvailable')) : null")
+ div(slot="drawer-header")
+ .drawer-tab-container
+ .drawer-tab.text-right
+ a.drawer-tab-text(
+ @click="selectedDrawerTab = 0",
+ :class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
+ ) {{ drawerTabs[0].label }}
+ .clearfix
+ .drawer-tab.float-left
+ a.drawer-tab-text(
+ @click="selectedDrawerTab = 1",
+ :class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
+ ) {{ drawerTabs[1].label }}
+
+ #petLikeToEatStable.drawer-help-text(v-once)
+ | {{ $t('petLikeToEat') + ' ' }}
+ span.svg-icon.inline.icon-16(v-html="icons.information")
+ b-popover(
+ target="petLikeToEatStable"
+ placement="top"
+ )
+ .popover-content-text(v-html="$t('petLikeToEatText')", v-once)
+ drawer-slider(
+ :items="drawerTabs[selectedDrawerTab].items",
+ :scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
+ slot="drawer-slider",
+ :itemWidth=94,
+ :itemMargin=24,
+ :itemType="selectedDrawerTab"
+ )
+ template(slot="item", slot-scope="context")
+ foodItem(
+ :item="context.item",
+ :itemCount="userItems.food[context.item.key]",
+ :active="currentDraggingFood == context.item",
+ @itemDragEnd="onDragEnd()",
+ @itemDragStart="onDragStart($event, context.item)",
+ @itemClick="onFoodClicked($event, context.item)"
+ )
+ hatchedPetDialog(:hideText="true")
+ div.foodInfo(ref="dragginFoodInfo")
+ div(v-if="currentDraggingFood != null")
+ div.food-icon(:class="'Pet_Food_'+currentDraggingFood.key")
+ div.popover
+ div.popover-content {{ $t('dragThisFood', {foodName: currentDraggingFood.text() }) }}
+ div.foodInfo.mouse(ref="clickFoodInfo", v-if="foodClickMode")
+ div(v-if="currentDraggingFood != null")
+ div.food-icon(:class="'Pet_Food_'+currentDraggingFood.key")
+ div.popover
+ div.popover-content {{ $t('clickOnPetToFeed', {foodName: currentDraggingFood.text() }) }}
+ mount-raised-modal
+ welcome-modal
+ hatching-modal(:hatchablePet.sync='hatchablePet')
diff --git a/website/client/components/inventory/stable/welcomeModal.vue b/website/client/components/inventory/stable/welcomeModal.vue
new file mode 100644
index 0000000000..178da7c3c6
--- /dev/null
+++ b/website/client/components/inventory/stable/welcomeModal.vue
@@ -0,0 +1,63 @@
+
+b-modal#welcome-modal(:ok-only="true",
+ :ok-title="$t('gotIt')",
+ :visible="!hideDialog",
+ :hide-header="true",
+ @hide="hideFlag()")
+ div.content
+ div.npc_matt
+ h1.page-header(v-once) {{ $t('welcomeStable') }}
+ div.content-text(v-once) {{ $t('welcomeStableText') }}
+
+
+
+
+
diff --git a/website/client/components/tasks/column.vue b/website/client/components/tasks/column.vue
index ba69531d99..4d8fa21933 100644
--- a/website/client/components/tasks/column.vue
+++ b/website/client/components/tasks/column.vue
@@ -37,7 +37,7 @@
draggable.sortable-tasks(
ref="tasksList",
@update='taskSorted',
- :options='{disabled: activeFilter.label === "scheduled"}',
+ :options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}',
class="sortable-tasks"
)
task(
diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue
index aa09db6fa0..c824c06c28 100644
--- a/website/client/components/tasks/taskModal.vue
+++ b/website/client/components/tasks/taskModal.vue
@@ -1,7 +1,7 @@
- form(v-if="task", @submit.stop.prevent="submit()")
- b-modal#task-modal(size="sm", @hidden="onClose()", @show="handleOpen()", @shown="focusInput()")
- .task-modal-header(slot="modal-header", :class="cssClass('bg')")
+ form(v-if="task", @submit.stop.prevent="submit()", @click="handleClick($event)")
+ b-modal#task-modal(v-bind:no-close-on-esc="showTagsSelect", v-bind:no-close-on-backdrop="showTagsSelect", size="sm", @hidden="onClose()", @show="handleOpen()", @shown="focusInput()")
+ .task-modal-header(slot="modal-header", :class="cssClass('bg')", @click="handleClick($event)")
.clearfix
h1.float-left {{ title }}
.float-right.d-flex.align-items-center
@@ -23,7 +23,7 @@
a(target="_blank", href="http://habitica.wikia.com/wiki/Markdown_Cheat_Sheet") {{ $t('markdownHelpLink') }}
textarea.form-control(v-model="task.notes", rows="3")
- .task-modal-content
+ .task-modal-content(@click="handleClick($event)")
.option.mt-0(v-if="task.type === 'reward'")
.form-group
label(v-once) {{ $t('cost') }}
@@ -147,7 +147,7 @@
.category-label(v-for='tagName in truncatedSelectedTags', :title="tagName", v-markdown='tagName')
.tags-more(v-if='remainingSelectedTags.length > 0') +{{ $t('more', { count: remainingSelectedTags.length }) }}
.dropdown-toggle
- tags-popup(v-if="showTagsSelect", :tags="user.tags", v-model="task.tags", @close='closeTagsPopup()')
+ tags-popup(ref="popup", v-if="showTagsSelect", :tags="user.tags", v-model="task.tags", @close='closeTagsPopup()')
.option(v-if="task.type === 'habit'")
.form-group
@@ -242,7 +242,7 @@
.svg-icon.d-inline-b(v-html="icons.destroy")
span {{ $t('deleteTask') }}
- .task-modal-footer.d-flex.justify-content-center.align-items-center(slot="modal-footer")
+ .task-modal-footer.d-flex.justify-content-center.align-items-center(slot="modal-footer", @click="handleClick($event)")
.cancel-task-btn(v-once, @click="cancel()") {{ $t('cancel') }}
button.btn.btn-primary(type="submit", v-once) {{ $t('save') }}
@@ -714,6 +714,9 @@ export default {
},
};
},
+ mounted () {
+ this.showAdvancedOptions = !this.user.preferences.advancedCollapsed;
+ },
watch: {
task () {
this.syncTask();
@@ -803,6 +806,12 @@ export default {
return this.selectedTags.slice(this.maxTags);
},
},
+ created () {
+ document.addEventListener('keyup', this.handleEsc);
+ },
+ destroyed () {
+ document.removeEventListener('keyup', this.handleEsc);
+ },
methods: {
...mapActions({saveTask: 'tasks:save', destroyTask: 'tasks:destroy', createTask: 'tasks:create'}),
async syncTask () {
@@ -988,6 +997,16 @@ export default {
focusInput () {
this.$refs.inputToFocus.focus();
},
+ handleEsc (e) {
+ if (e.keyCode === 27 && this.showTagsSelect) {
+ this.closeTagsPopup();
+ }
+ },
+ handleClick (e) {
+ if (this.$refs.popup && !this.$refs.popup.$el.contains(e.target)) {
+ this.closeTagsPopup();
+ }
+ },
},
};
diff --git a/website/client/mixins/petMixin.js b/website/client/mixins/petMixin.js
new file mode 100644
index 0000000000..8ea3a658cf
--- /dev/null
+++ b/website/client/mixins/petMixin.js
@@ -0,0 +1,13 @@
+export default {
+ methods: {
+ closeHatchPetDialog () {
+ this.$root.$emit('bv::hide::modal', 'hatching-modal');
+ },
+ hatchPet (pet) {
+ this.closeHatchPetDialog();
+
+ this.$store.dispatch('common:hatch', {egg: pet.eggKey, hatchingPotion: pet.potionKey});
+ this.text(this.$t('hatchedPet', {egg: pet.eggName, potion: pet.potionName}));
+ },
+ },
+};
diff --git a/website/server/libs/challenges/index.js b/website/server/libs/challenges/index.js
index 9feb91b925..764cdf907b 100644
--- a/website/server/libs/challenges/index.js
+++ b/website/server/libs/challenges/index.js
@@ -79,8 +79,6 @@ export async function createChallenge (user, req, res) {
let challengeValidationErrors = challenge.validateSync();
if (challengeValidationErrors) throw challengeValidationErrors;
- addUserJoinChallengeNotification(user);
-
let results = await Promise.all([challenge.save({
validateBeforeSave: false, // already validated
}), group.save(), user.save()]);
diff --git a/website/server/models/user/index.js b/website/server/models/user/index.js
index 6875d58df8..5521f21290 100644
--- a/website/server/models/user/index.js
+++ b/website/server/models/user/index.js
@@ -7,7 +7,7 @@ require('./methods');
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
- preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks profile stats
+ preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats
achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected`;
// The minimum amount of data needed when populating multiple users
diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js
index 183f881d34..078ae60cab 100644
--- a/website/server/models/user/methods.js
+++ b/website/server/models/user/methods.js
@@ -7,8 +7,8 @@ import {
model as Group,
} from '../group';
-import { defaults, map, flatten, flow, compact, uniq, partialRight } from 'lodash';
-import { model as UserNotification } from '../userNotification';
+import {defaults, map, flatten, flow, compact, uniq, partialRight} from 'lodash';
+import {model as UserNotification} from '../userNotification';
import schema from './schema';
import payments from '../../libs/payments/payments';
import amazonPayments from '../../libs/payments/amazon';
diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js
index 5f0ac2c09f..15b4cec4d6 100644
--- a/website/server/models/userNotification.js
+++ b/website/server/models/userNotification.js
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
import baseModel from '../libs/baseModel';
import { v4 as uuid } from 'uuid';
import validator from 'validator';
+import _ from 'lodash';
const NOTIFICATION_TYPES = [
'DROPS_ENABLED',
@@ -74,13 +75,22 @@ export let schema = new Schema({
schema.statics.convertNotificationsToSafeJson = function convertNotificationsToSafeJson (notifications) {
if (!notifications) return notifications;
- return notifications.filter(n => {
+ let filteredNotifications = notifications.filter(n => {
// Exclude notifications with a nullish value
if (!n) return false;
// Exclude notifications without an id or a type
if (!n.id || !n.type) return false;
return true;
- }).map(n => {
+ });
+
+ filteredNotifications = _.uniqWith(filteredNotifications, (val, otherVal) => {
+ if (val.type === otherVal.type && val.type === 'NEW_CHAT_MESSAGE') {
+ return val.data.group.id === otherVal.data.group.id;
+ }
+ return false;
+ });
+
+ return filteredNotifications.map(n => {
return n.toJSON();
});
};