Notifications v2 and Bailey API (#9716)

* Added initial bailey api

* wip

* implement new panel header

* Fixed lint

* add ability to mark notification as seen

* add notification count, remove top badge from user and add ability to mark multiple notifications as seen

* add support dismissall and mark all as read

* do not dismiss actionable notif

* mark as seen when menu is opened instead of closed

* implement ordering, list of actionable notifications

* add groups messages and fix badges count

* add notifications for received cards

* send card received notification to target not sender

* rename notificaion field

* fix integration tests

* mark cards notifications as read and update tests

* add mystery items notifications

* add unallocated stats points notifications

* fix linting

* simplify code

* refactoring and fixes

* fix dropdown opening

* start splitting notifications into their own component

* add notifications for inbox messages

* fix unit tests

* fix default buttons styles

* add initial bailey support

* add title and tests to new stuff notification

* add notification if a group task needs more work

* add tests and fixes for marking a task as needing more work

* make sure user._v is updated

* remove console.log

* notification: hover status and margins

* start styling notifications, add separate files and basic functionalities

* fix tests

* start adding mystery items notification

* wip card notification

* fix cards text

* initial implementation inbox messages

* initial implementation group messages

* disable inbox notifications until mobile is ready

* wip group chat messages

* finish mystery and card notifications

* add bailey notification and fix a lot of stuff

* start adding guilds and parties invitations

* misc invitation fixes

* fix lint issues

* remove old code and add key to notifications

* fix tests

* remove unused code

* add link for public guilds invite

* starts to implement needs work notification design and feature

* fixes to needs work, add group task approved notification

* finish needs work feature

* lots of fixes

* implement quest notification

* bailey fixes and static page

* routing fixes

* fixes #      this.$store.dispatch(guilds:join, {groupId: group.id, type: party});

* read notifications on click

* chat notifications

* fix tests for chat notifications

* fix chat notification test

* fix tests

* fix tests (again)

* try awaiting

* remove only

* more sleep

* add bailey tests

* fix icons alignment

* fix issue with multiple points notifications

* remove merge code

* fix rejecting guild invitation

* make remove area bigger

* fix error with notifications and add migration

* fix migration

* fix typos

* add cleanup migration too

* notifications empty state, new counter color, fix marking messages as seen in guilds

* fixes

* add image and install correct packages

* fix mongoose version

* update bailey

* typo

* make sure chat is marked as read after other requests
This commit is contained in:
Matteo Pagliazzi
2018-01-31 11:55:39 +01:00
committed by GitHub
parent a85282763f
commit 33b249d078
98 changed files with 3003 additions and 1026 deletions
+6
View File
@@ -137,3 +137,9 @@
border: 0;
box-shadow: none;
}
.btn-small {
font-size: 12px;
line-height: 1.33;
padding: 4px 8px;
}
+9
View File
@@ -45,6 +45,15 @@
background-color: rgba(#d5c8ff, 0.32);
color: $purple-200;
}
&.dropdown-inactive {
cursor: default;
&:active, &:hover, &.active {
background-color: inherit;
color: inherit;
}
}
}
.dropdown + .dropdown {
+5
View File
@@ -23,6 +23,11 @@
height: 16px;
}
.icon-12 {
width: 12px;
height: 12px;
}
.icon-10 {
width: 10px;
height: 10px;
+19 -24
View File
@@ -1,31 +1,26 @@
.badge-svg {
left: calc((100% - 18px) / 2);
cursor: pointer;
color: $gray-400;
background: $white;
padding: 4.5px 6px;
left: calc((100% - 18px) / 2);
cursor: pointer;
color: $gray-400;
background: $white;
padding: 4.5px 6px;
&.item-selected-badge {
background: $purple-300;
color: $white;
}
&.item-selected-badge {
background: $purple-300;
color: $white;
}
}
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
color: #a5a1ac;
}
span.badge.badge-pill.badge-item.badge-svg:not(.item-selected-badge) {
color: #a5a1ac;
}
span.badge.badge-pill.badge-item.badge-svg.hide {
display: none;
}
.item:hover {
span.badge.badge-pill.badge-item.badge-svg.hide {
display: none;
display: block;
}
.item:hover {
span.badge.badge-pill.badge-item.badge-svg.hide {
display: block;
}
}
.icon-12 {
width: 12px;
height: 12px;
}
}
+23 -17
View File
@@ -1,26 +1,32 @@
@import '~client/assets/scss/colors.scss';
.container-fluid {
.container-fluid.static-view {
margin: 5em 2em 0 2em;
}
h1, h2 {
margin-top: 0.5em;
color: $purple-200;
}
.static-view {
h1, h2 {
margin-top: 0.5em;
color: $purple-200;
}
h3, h4 {
color: $purple-200;
}
h3, h4 {
color: $purple-200;
}
li, p {
font-size: 16px;
}
li, p {
font-size: 16px;
}
.media img {
margin: 1em;
}
.media img {
margin: 1em;
}
.strong {
font-weight: bold;
}
.strong {
font-weight: bold;
}
.center-block {
margin: 0 auto 1em auto;
}
}
+20 -1
View File
@@ -15,8 +15,27 @@ body {
color: $gray-200;
}
a {
a, a:not([href]):not([tabindex]) {
cursor: pointer;
&.standard-link {
color: $blue-10;
&:hover, &:active, &:focus {
text-decoration: underline;
}
&[disabled="disabled"] {
color: $gray-300;
text-decoration: none;
cursor: default;
}
}
&.small-link {
font-size: 12px;
line-height: 1.33;
}
}
// Headers
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="28" viewBox="0 0 23 28">
<g fill="none" fill-rule="evenodd">
<path fill="#50B5E9" d="M11.074 8.485l4.26-2.121-4.26-2.122L8.944 0l-2.13 4.242-4.258 2.122 4.259 2.12 2.13 4.243z"/>
<path fill="#9A62FF" d="M5.111 19.09l2.556-1.272-2.556-1.273L3.833 14l-1.277 2.545L0 17.818l2.556 1.273 1.277 2.545z"/>
<path fill="#FF6165" d="M12.778 25.455l2.555-1.273-2.555-1.273-1.278-2.545-1.278 2.545-2.555 1.273 2.555 1.273L11.5 28z"/>
<path fill="#FFB445" d="M19.593 15.697L23 14l-3.407-1.697-1.704-3.394-1.704 3.394L12.778 14l3.407 1.697 1.704 3.394z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 653 B

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="104" viewBox="0 0 256 104">
<g fill="none" fill-rule="evenodd">
<g opacity=".64" transform="translate(-44 -16)">
<rect width="346" height="136" rx="2"/>
<path fill="#3FDAA2" d="M232.359 84.179l3.074-1.341-2.917-1.655-1.341-3.075-1.655 2.918-3.075 1.34 2.918 1.656 1.34 3.074z" opacity=".96"/>
<path fill="#FF6165" d="M171.439 20.105l-.023 2.982 2.399-1.771 2.981.023-1.77-2.4.022-2.98-2.399 1.77-2.98-.022z" opacity=".84"/>
<path fill="#3FDAA2" d="M114.501 54.126l2.574.428-1.202-2.315.428-2.574-2.316 1.202-2.573-.427 1.202 2.315-.428 2.573z" opacity=".83"/>
<path fill="#50B5E9" d="M284.929 89.34l.173 6.333 4.962-3.939 6.334-.173-3.939-4.962-.173-6.334-4.962 3.939-6.334.173z" opacity=".73"/>
<path fill="#FFBE5D" d="M242.881 57.724l-3.984 5.397 6.708-.05 5.397 3.983-.05-6.708 3.983-5.397-6.708.051-5.397-3.984z" opacity=".82"/>
<path fill="#50B5E9" d="M125.165 110.829l-.589-4.433-3.193 3.13-4.433.59 3.13 3.193.59 4.433 3.193-3.13 4.433-.59z" opacity=".99"/>
<path fill="#50B5E9" d="M163.702 56.186l-3.901 5.91 7.068-.425 5.91 3.902-.425-7.069 3.901-5.909-7.068.425-5.909-3.902z"/>
<path fill="#FF6165" d="M206.14 107.367l3.404 2.9.278-4.463 2.9-3.404-4.463-.278-3.404-2.9-.278 4.463-2.9 3.404z" opacity=".84"/>
<path fill="#FF944C" d="M50.708 53.066l.62 3.675 2.568-2.7 3.675-.62-2.7-2.568-.62-3.675-2.568 2.7-3.675.62zM297.486 59.18l-.037-3.727-2.96 2.265-3.726.037 2.265 2.959.037 3.727 2.96-2.266 3.726-.037z" opacity=".93"/>
<path fill="#9A62FF" d="M95.481 86.952l5.13-.957-3.843-3.53-.957-5.128-3.53 3.842-5.128.957 3.843 3.53.956 5.128z" opacity=".99"/>
<path fill="#FFBE5D" d="M122.061 24.656l-.952 3.987 3.761-1.63 3.987.952-1.63-3.761.953-3.988-3.762 1.63-3.987-.952z"/>
<path fill="#3FDAA2" d="M49.863 86.74l4.692 1.207-1.849-4.478 1.208-4.692-4.478 1.849-4.692-1.208 1.849 4.478-1.208 4.692z" opacity=".96"/>
<path fill="#9A62FF" d="M226.63 30.447l5.026-1.4-4.135-3.18-1.4-5.027-3.181 4.136-5.026 1.4 4.135 3.18 1.4 5.027z" opacity=".99"/>
</g>
<path fill="#3FDAA2" d="M119.347 104c-2.031 0-3.971-.8-5.41-2.24L89 76.824l10.819-10.818 19.098 19.098L157.57 40l11.619 9.953-44.03 51.378a7.694 7.694 0 0 1-5.52 2.662c-.097.007-.195.007-.292.007"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -1,58 +1,58 @@
<template lang="pug">
b-modal#new-stuff(
v-if='user.flags.newStuff',
size='lg',
:hide-header='true',
:hide-footer='true',
)
.modal-body
new-stuff
.static-view(v-html='html')
.modal-footer
a.btn.btn-info(href='http://habitica.wikia.com/wiki/Whats_New', target='_blank') {{ this.$t('newsArchive') }}
button.btn.btn-secondary(@click='close()') {{ this.$t('cool') }}
button.btn.btn-secondary(@click='tellMeLater()') {{ this.$t('tellMeLater') }}
button.btn.btn-warning(@click='dismissAlert();') {{ this.$t('dismissAlert') }}
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/static.scss';
<style lang='scss'>
@import '~client/assets/scss/static.scss';
</style>
.modal-body {
padding-top: 2em;
}
<style lang='scss' scoped>
.modal-body {
padding-top: 2em;
}
</style>
<script>
import axios from 'axios';
import { mapState } from 'client/libs/store';
import markdown from 'client/directives/markdown';
import newStuff from 'client/components/static/newStuff';
export default {
components: {
newStuff,
data () {
return {
html: '',
};
},
computed: {
...mapState({user: 'user.data'}),
},
directives: {
markdown,
},
mounted () {
async mounted () {
this.$root.$on('bv::show::modal', async (modalId) => {
if (modalId !== 'new-stuff') return;
// Request the lastest news, but not locally incase they don't refresh
// let response = await axios.get('/static/new-stuff');
let response = await axios.get('/api/v3/news');
this.html = response.data.html;
});
},
destroyed () {
this.$root.$off('bv::show::modal');
},
methods: {
close () {
tellMeLater () {
this.$store.dispatch('user:newStuffLater');
this.$root.$emit('bv::hide::modal', 'new-stuff');
},
dismissAlert () {
this.$store.dispatch('user:set', {'flags.newStuff': false});
this.close();
this.$root.$emit('bv::hide::modal', 'new-stuff');
},
},
};
+15 -15
View File
@@ -87,21 +87,21 @@
.debug.float-left(v-if="!IS_PRODUCTION && isUserLoaded")
button.btn.btn-primary(@click="debugMenuShown = !debugMenuShown") Toggle Debug Menu
.debug-group(v-if="debugMenuShown")
a.btn.btn-default(@click="setHealthLow()") Health = 1
a.btn.btn-default(@click="addMissedDay(1)") +1 Missed Day
a.btn.btn-default(@click="addMissedDay(2)") +2 Missed Days
a.btn.btn-default(@click="addMissedDay(8)") +8 Missed Days
a.btn.btn-default(@click="addMissedDay(32)") +32 Missed Days
a.btn.btn-default(@click="addTenGems()") +10 Gems
a.btn.btn-default(@click="addHourglass()") +1 Mystic Hourglass
a.btn.btn-default(@click="addGold()") +500GP
a.btn.btn-default(@click="plusTenHealth()") + 10HP
a.btn.btn-default(@click="addMana()") +MP
a.btn.btn-default(@click="addLevelsAndGold()") +Exp +GP +MP
a.btn.btn-default(@click="addOneLevel()") +1 Level
a.btn.btn-default(@click="addQuestProgress()", tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
a.btn.btn-default(@click="makeAdmin()") Make Admin
a.btn.btn-default(@click="openModifyInventoryModal()") Modify Inventory
a.btn.btn-secondary(@click="setHealthLow()") Health = 1
a.btn.btn-secondary(@click="addMissedDay(1)") +1 Missed Day
a.btn.btn-secondary(@click="addMissedDay(2)") +2 Missed Days
a.btn.btn-secondary(@click="addMissedDay(8)") +8 Missed Days
a.btn.btn-secondary(@click="addMissedDay(32)") +32 Missed Days
a.btn.btn-secondary(@click="addTenGems()") +10 Gems
a.btn.btn-secondary(@click="addHourglass()") +1 Mystic Hourglass
a.btn.btn-secondary(@click="addGold()") +500GP
a.btn.btn-secondary(@click="plusTenHealth()") + 10HP
a.btn.btn-secondary(@click="addMana()") +MP
a.btn.btn-secondary(@click="addLevelsAndGold()") +Exp +GP +MP
a.btn.btn-secondary(@click="addOneLevel()") +1 Level
a.btn.btn-secondary(@click="addQuestProgress()", tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
a.btn.btn-secondary(@click="makeAdmin()") Make Admin
a.btn.btn-secondary(@click="openModifyInventoryModal()") Modify Inventory
.col-12.col-md-2.text-center
.logo.svg-icon(v-html='icons.gryphon')
.col-12.col-md-5.text-right
+18 -28
View File
@@ -427,14 +427,9 @@ export default {
},
},
mounted () {
if (this.isParty) this.searchId = 'party';
if (!this.searchId) this.searchId = this.groupId;
this.load();
if (this.user.newMessages[this.searchId]) {
this.$store.dispatch('chat:markChatSeen', {groupId: this.searchId});
this.$delete(this.user.newMessages, this.searchId);
}
},
beforeRouteUpdate (to, from, next) {
this.$set(this, 'searchId', to.params.groupId);
@@ -462,12 +457,6 @@ export default {
},
methods: {
load () {
if (this.isParty) {
this.searchId = 'party';
// @TODO: Set up from old client. Decide what we need and what we don't
// Check Desktop notifs
// Load invites
}
this.fetchGuild();
this.$root.$on('updatedGroup', group => {
@@ -550,6 +539,22 @@ export default {
const group = await this.$store.dispatch('guilds:getGroup', {groupId: this.searchId});
this.$set(this, 'group', group);
}
const groupId = this.searchId === 'party' ? this.user.party._id : this.searchId;
if (this.hasUnreadMessages(groupId)) {
// Delay by 1sec to make sure it returns after other requests that don't have the notification marked as read
setTimeout(() => {
this.$store.dispatch('chat:markChatSeen', {groupId});
this.$delete(this.user.newMessages, groupId);
}, 1000);
}
},
hasUnreadMessages (groupId) {
if (this.user.newMessages[groupId]) return true;
return this.user.notifications.some(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
});
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
@@ -575,8 +580,7 @@ export default {
if (this.group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {guildId: this.group._id, type: 'myGuilds'});
this.user.guilds.push(this.group._id);
await this.$store.dispatch('guilds:join', {groupId: this.group._id, type: 'guild'});
},
clickLeave () {
Analytics.track({
@@ -616,20 +620,6 @@ export default {
this.$store.state.upgradingGroup = this.group;
this.$router.push('/group-plans');
},
// @TODO: Move to notificatin component
async leaveOldPartyAndJoinNewParty () {
let newPartyName = 'where does this come from';
if (!confirm(`Are you sure you want to delete your party and join${newPartyName}?`)) return;
let keepChallenges = 'remain-in-challenges';
await this.$store.dispatch('guilds:leave', {
groupId: this.group._id,
keep: false,
keepChallenges,
});
await this.$store.dispatch('guilds:join', {groupId: this.group._id});
},
clickStartQuest () {
Analytics.track({
hitType: 'event',
@@ -130,7 +130,6 @@ import moment from 'moment';
import { mapState } from 'client/libs/store';
import groupUtilities from 'client/mixins/groupsUtilities';
import markdown from 'client/directives/markdown';
import findIndex from 'lodash/findIndex';
import gemIcon from 'assets/svg/gem.svg';
import goldGuildBadgeIcon from 'assets/svg/gold-guild-badge-large.svg';
import silverGuildBadgeIcon from 'assets/svg/silver-guild-badge-large.svg';
@@ -171,20 +170,12 @@ export default {
if (this.guild.cancelledPlan && !confirm(window.env.t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {guildId: this.guild._id, type: 'myGuilds'});
await this.$store.dispatch('guilds:join', {groupId: this.guild._id, type: 'guild'});
},
async leave () {
// @TODO: ask about challenges when we add challenges
await this.$store.dispatch('guilds:leave', {groupId: this.guild._id, type: 'myGuilds'});
},
async reject (invitationToReject) {
// @TODO: This needs to be in the notifications where users will now accept invites
let index = findIndex(this.user.invitations.guilds, function findInviteIndex (invite) {
return invite.id === invitationToReject.id;
});
this.user.invitations.guilds = this.user.invitations.guilds.splice(0, index);
await this.$store.dispatch('guilds:rejectInvite', {guildId: invitationToReject.id});
},
},
};
</script>
+6 -6
View File
@@ -57,16 +57,16 @@ div
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
.user-menu.d-flex.align-items-center
.item-with-icon(v-if="userHourglasses > 0")
.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
span {{ userHourglasses }}
.item-with-icon
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
.top-menu-icon.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
span {{userGems | roundBigNumber}}
.item-with-icon.gold
.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
.top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
span {{Math.floor(user.stats.gp * 100) / 100}}
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
.svg-icon(v-html="icons.sync")
.top-menu-icon.svg-icon(v-html="icons.sync")
notification-menu.item-with-icon
user-dropdown.item-with-icon
</template>
@@ -236,11 +236,11 @@ div
margin-right: 24px;
}
&:hover /deep/ .svg-icon {
&:hover /deep/ .top-menu-icon.svg-icon {
color: $white;
}
& /deep/ .svg-icon {
& /deep/ .top-menu-icon.svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
@@ -1,5 +1,7 @@
<template lang="pug" functional>
span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
span.message-count(
:class="{'top-count': props.top === true, 'top-count-gray': props.gray === true}"
) {{props.count}}
</template>
<style lang="scss">
@@ -24,4 +26,8 @@ span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
padding: 0.2em;
background-color: $red-50;
}
.message-count.top-count-gray {
background-color: $gray-200;
}
</style>
@@ -0,0 +1,162 @@
<template lang="pug">
.notification.dropdown-item.dropdown-separated.d-flex.justify-content-between(
@click="clicked"
)
.notification-icon.d-flex.justify-content-center.align-items-center(
v-if="hasIcon",
:class="{'is-not-bailey': isNotBailey}",
)
slot(name="icon")
.notification-content
slot(name="content")
.notification-remove(@click.stop="canRemove ? remove() : null",)
.svg-icon(
v-if="canRemove",
v-html="icons.close",
)
</template>
<style lang="scss"> // Not scoped because the classes could be used in i18n strings
@import '~client/assets/scss/colors.scss';
.notification-small {
font-size: 12px;
line-height: 1.33;
color: $gray-200;
}
.notification-ellipses {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.notification-bold {
font-weight: bold;
}
.notification-bold-blue {
font-weight: bold;
color: $blue-10;
}
.notification-bold-purple {
font-weight: bold;
color: $purple-300;
}
.notification-yellow {
color: #bf7d1a;
}
.notification-green {
color: #1CA372;
}
</style>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.notification {
width: 378px;
max-width: 100%;
padding: 9px 20px 10px 24px;
overflow: hidden;
&:active, &:hover {
color: inherit;
}
}
.notification-icon {
margin-right: 16px;
&.is-not-bailey {
width: 31px;
height: 32px;
}
}
.notifications-buttons {
margin-top: 12px;
.btn {
margin-right: 8px;
}
}
.notification-content {
// total distance from notification top and bottom edges are 15 and 16 pixels
margin-top: 6px;
margin-bottom: 6px;
flex-grow: 1;
white-space: normal;
font-size: 14px;
line-height: 1.43;
color: $gray-50;
max-width: calc(100% - 26px); // to make space for the close icon
}
.notification-remove {
// total distance from the notification top edge is 20 pixels
margin-top: 7px;
width: 18px;
height: 18px;
margin-left: 12px;
padding: 4px;
.svg-icon {
width: 10px;
height: 10px;
}
}
</style>
<script>
import closeIcon from 'assets/svg/close.svg';
import { mapActions, mapState } from 'client/libs/store';
export default {
props: ['notification', 'canRemove', 'hasIcon', 'readAfterClick'],
data () {
return {
icons: Object.freeze({
close: closeIcon,
}),
};
},
computed: {
...mapState({user: 'user.data'}),
isNotBailey () {
return this.notification.type !== 'NEW_STUFF';
},
},
methods: {
...mapActions({
readNotification: 'notifications:readNotification',
}),
clicked () {
if (this.readAfterClick === true) {
this.readNotification({notificationId: this.notification.id});
}
this.$emit('click');
},
remove () {
if (this.notification.type === 'NEW_CHAT_MESSAGE') {
const groupId = this.notification.data.group.id;
this.$store.dispatch('chat:markChatSeen', {groupId});
if (this.user.newMessages[groupId]) {
this.$delete(this.user.newMessages, groupId);
}
} else {
this.readNotification({notificationId: this.notification.id});
}
},
},
};
</script>
@@ -0,0 +1,35 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="true",
:notification="notification",
:read-after-click="true",
@click="action"
)
div(slot="content", v-html="$t('cardReceived', {card: cardString})")
div(slot="icon", :class="cardClass")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
cardString () {
return this.$t(`${this.notification.data.card}Card`);
},
cardClass () {
return `notif_inventory_special_${this.notification.data.card}`;
},
},
methods: {
action () {
this.$router.push({name: 'items'});
},
},
};
</script>
@@ -0,0 +1,3 @@
<template lang="pug" functional>
div {{ props.notification }}
</template>
@@ -0,0 +1,72 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
@click="action",
)
div(slot="content")
div(v-html="notification.data.message")
.notifications-buttons
.btn.btn-small.btn-success(@click.stop="approve()") {{ $t('approve') }}
.btn.btn-small.btn-warning(@click.stop="needsWork()") {{ $t('needsWork') }}
</template>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
...mapState({user: 'user.data'}),
// Check that the notification has all the necessary data (old ones are missing some fields)
notificationHasData () {
return Boolean(this.notification.data.groupTaskId && this.notification.data.userId);
},
},
methods: {
action () {
const groupId = this.notification.data.group.id;
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
},
async approve () {
// Redirect users to the group tasks page if the notification doesn't have data
if (!this.notificationHasData) {
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
groupId: this.notification.data.groupId,
}});
return;
}
if (!confirm(this.$t('confirmApproval'))) return;
this.$store.dispatch('tasks:approve', {
taskId: this.notification.data.groupTaskId,
userId: this.notification.data.userId,
});
},
async needsWork () {
// Redirect users to the group tasks page if the notification doesn't have data
if (!this.notificationHasData) {
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: {
groupId: this.notification.data.groupId,
}});
return;
}
if (!confirm(this.$t('confirmNeedsWork'))) return;
this.$store.dispatch('tasks:needsWork', {
taskId: this.notification.data.groupTaskId,
userId: this.notification.data.userId,
});
},
},
};
</script>
@@ -0,0 +1,27 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="true",
@click="action",
)
.notification-green(slot="content", v-html="notification.data.message")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
const groupId = this.notification.data.groupId;
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
},
},
};
</script>
@@ -0,0 +1,27 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="true",
@click="action",
)
.notification-yellow(slot="content", v-html="notification.data.message")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
const groupId = this.notification.data.group.id;
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId }});
},
},
};
</script>
@@ -0,0 +1,64 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
@click="action",
)
div(slot="content")
div(v-html="textString")
.notifications-buttons
.btn.btn-small.btn-success(@click.stop="accept()") {{ $t('accept') }}
.btn.btn-small.btn-danger(@click.stop="reject()") {{ $t('reject') }}
</template>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
...mapState({user: 'user.data'}),
isPublicGuild () {
if (this.notification.data.publicGuild === true) return true;
return false;
},
textString () {
const guild = this.notification.data.name;
if (this.isPublicGuild) {
return this.$t('invitedToPublicGuild', {guild});
} else {
return this.$t('invitedToPrivateGuild', {guild});
}
},
},
methods: {
action () {
if (!this.isPublicGuild) return;
const groupId = this.notification.data.id;
this.$router.push({ name: 'guild', params: { groupId } });
},
async accept () {
const group = this.notification.data;
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {groupId: group.id, type: 'guild'});
this.$router.push({ name: 'guild', params: { groupId: group.id } });
},
reject () {
this.$store.dispatch('guilds:rejectInvite', {groupId: this.notification.data.id, type: 'guild'});
},
},
};
</script>
@@ -0,0 +1,46 @@
<template lang="pug">
// Read automatically from the group page mounted hook
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="false",
@click="action"
)
div(slot="content", v-html="string")
</template>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
...mapState({user: 'user.data'}),
groupId () {
return this.notification.data.group.id;
},
isParty () {
return this.groupId === this.user.party._id;
},
string () {
const stringKey = this.isParty ? 'newMsgParty' : 'newMsgGuild';
return this.$t(stringKey, {name: this.notification.data.group.name});
},
},
methods: {
action () {
if (this.isParty) {
this.$router.push({ name: 'party' });
return;
}
this.$router.push({ name: 'guild', params: { groupId: this.groupId }});
},
},
};
</script>
@@ -0,0 +1,28 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
:read-after-click="true",
@click="action"
)
div(slot="content")
span(v-html="$t('userSentMessage', {user: notification.data.sender.name})")
.notification-small.notification-ellipses {{ notification.data.excerpt }}
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
this.$root.$emit('bv::show::modal', 'inbox-modal');
},
},
};
</script>
@@ -0,0 +1,33 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="true",
:notification="notification",
:read-after-click="true",
@click="action"
)
div(slot="content", v-html="$t('newSubscriberItem')")
div(slot="icon", :class="mysteryClass")
</template>
<script>
import BaseNotification from './base';
import moment from 'moment';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
mysteryClass () {
return `notif_inventory_present_${moment().format('MM')}`;
},
},
methods: {
action () {
this.$router.push({name: 'items'});
},
},
};
</script>
@@ -0,0 +1,29 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="true",
:notification="notification",
:read-after-click="true",
@click="action"
)
div(slot="content")
.notification-bold-purple {{ $t('newBaileyUpdate') }}
div {{ notification.data.title }}
.npc_bailey(slot="icon")
</template>
<script>
import BaseNotification from './base';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
methods: {
action () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
},
};
</script>
@@ -0,0 +1,42 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
)
div(slot="content")
div(v-html="$t('invitedToParty', {party: notification.data.name})")
.notifications-buttons
.btn.btn-small.btn-success(@click.stop="accept()") {{ $t('accept') }}
.btn.btn-small.btn-danger(@click.stop="reject()") {{ $t('reject') }}
</template>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
async accept () {
const group = this.notification.data;
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
return;
}
await this.$store.dispatch('guilds:join', {groupId: group.id, type: 'party'});
this.$router.push('/party');
},
reject () {
this.$store.dispatch('guilds:rejectInvite', {groupId: this.notification.data.id, type: 'party'});
},
},
};
</script>
@@ -0,0 +1,63 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="false",
:notification="notification",
@click="action",
)
div(slot="content")
.message(v-html="$t('invitedToQuest', {quest: questName})")
quest-info(:quest="questData", :small-version="true")
.notifications-buttons
.btn.btn-small.btn-success(@click.stop="questAccept()") {{ $t('accept') }}
.btn.btn-small.btn-danger(@click.stop="questReject()") {{ $t('reject') }}
</template>
<style lang="scss">
.message {
margin-bottom: 8px;
}
</style>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
import quests from 'common/script/content/quests';
import questInfo from 'client/components/shops/quests/questInfo';
export default {
props: ['notification', 'canRemove'],
components: {
BaseNotification,
questInfo,
},
computed: {
...mapState({user: 'user.data'}),
questData () {
return quests.quests[this.notification.data.quest];
},
questName () {
return this.questData.text();
},
},
methods: {
action () {
this.$router.push({ name: 'party' });
},
async questAccept () {
let quest = await this.$store.dispatch('quests:sendAction', {
groupId: this.notification.data.partyId,
action: 'quests/accept',
});
this.user.party.quest = quest;
},
async questReject () {
let quest = await this.$store.dispatch('quests:sendAction', {
groupId: this.notification.data.partyId,
action: 'quests/reject',
});
this.user.party.quest = quest;
},
},
};
</script>
@@ -0,0 +1,45 @@
<template lang="pug">
base-notification(
:can-remove="canRemove",
:has-icon="true",
:notification="notification",
:read-after-click="true",
@click="action"
)
div(slot="content", v-html="$t('unallocatedStatsPoints', {points: notification.data.points})")
.svg-icon(slot="icon", v-html="icons.sparkles")
</template>
<style lang="scss" scoped>
.svg-icon {
width: 23px;
height: 28px;
}
</style>
<script>
import BaseNotification from './base';
import sparklesIcon from 'assets/svg/sparkles.svg';
export default {
props: ['notification', 'canRemove'],
data () {
return {
icons: Object.freeze({
sparkles: sparklesIcon,
}),
};
},
components: {
BaseNotification,
},
methods: {
action () {
this.$root.$emit('habitica:show-profile', {
user: this.$store.state.user.data,
startingPage: 'stats',
});
},
},
};
</script>
@@ -1,289 +1,254 @@
<template lang="pug">
menu-dropdown.item-notifications(:right="true")
menu-dropdown.item-notifications(:right="true", @toggled="handleOpenStatusChange", :openStatus="openStatus")
div(slot="dropdown-toggle")
div(v-b-tooltip.hover.bottom="$t('notifications')")
message-count(v-if='notificationsCount > 0', :count="notificationsCount", :top="true")
.svg-icon.notifications(v-html="icons.notifications")
message-count(
v-if='notificationsCount > 0',
:count="notificationsCount",
:top="true",
:gray="!hasUnseenNotifications",
)
.top-menu-icon.svg-icon.notifications(v-html="icons.notifications")
div(slot="dropdown-content")
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
a.dropdown-item(v-if='user.party.quest && user.party.quest.RSVPNeeded')
div {{ $t('invitedTo', {name: quests.quests[user.party.quest.key].text()}) }}
div
button.btn.btn-primary(@click.stop='questAccept(user.party._id)') Accept
button.btn.btn-primary(@click.stop='questReject(user.party._id)') Reject
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
span.glyphicon.glyphicon-gift
span {{ $t('newSubscriberItem') }}
a.dropdown-item(v-for='(party, index) in user.invitations.parties', :key='party.id')
div
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: party.name}) }}
div
button.btn.btn-primary(@click.stop='accept(party, index, "party")') Accept
button.btn.btn-primary(@click.stop='reject(party, index, "party")') Reject
a.dropdown-item(v-if='user.flags.cardReceived', @click='go("/inventory/items")')
span.glyphicon.glyphicon-envelope
span {{ $t('cardReceived') }}
a.dropdown-item(@click.stop='clearCards()')
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds', :key='guild.id')
div
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: guild.name}) }}
div
button.btn.btn-primary(@click.stop='accept(guild, index, "guild")') Accept
button.btn.btn-primary(@click.stop='reject(guild, index, "guild")') Reject
a.dropdown-item(v-if='user.flags.classSelected && !user.preferences.disableClasses && user.stats.points',
@click='showProfile()')
span.glyphicon.glyphicon-plus-sign
span {{ $t('haveUnallocated', {points: user.stats.points}) }}
a.dropdown-item(v-for='message in userNewMessages', :key='message.key')
span(@click='navigateToGroup(message.key)')
span.glyphicon.glyphicon-comment
span {{message.name}}
span.clear-button(@click.stop='clearMessages(message.key)') Clear
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
span(:class="groupApprovalNotificationIcon(notification)")
span {{notification.data.message}}
span.clear-button(@click.stop='viewGroupApprovalNotification(notification)') Clear
.dropdown-item.dropdown-separated.d-flex.justify-content-between.dropdown-inactive.align-items-center(
@click.stop=""
)
h4.dropdown-title(v-once) {{ $t('notifications') }}
a.small-link.standard-link(@click="dismissAll", :disabled="notificationsCount === 0") {{ $t('dismissAll') }}
component(
:is="notification.type",
:key="notification.id",
v-for="notification in notifications",
:notification="notification",
:can-remove="!isActionable(notification)",
)
.dropdown-item.dropdown-separated.d-flex.justify-content-center.dropdown-inactive.no-notifications.flex-column(
v-if="notificationsCount === 0"
)
.svg-icon(v-html="icons.success")
h2 You're all caught up!
p The notification fairies give you a raucous round of applause! Well done!
</template>
<style lang='scss' scoped>
.clear-button {
margin-left: .5em;
@import '~client/assets/scss/colors.scss';
.dropdown-item {
padding: 16px 24px;
width: 378px;
}
.dropdown-title {
margin-bottom: 0px;
margin-right: 8px;
line-height: 1.5;
}
.no-notifications {
h2, p {
text-align: center;
color: $gray-200 !important;
}
h2 {
margin-top: 24px;
}
p {
white-space: normal;
margin-bottom: 43px;
margin-left: 24px;
margin-right: 24px;
}
.svg-icon {
margin: 0 auto;
width: 256px;
height: 104px;
}
}
</style>
<script>
import axios from 'axios';
import isEmpty from 'lodash/isEmpty';
import map from 'lodash/map';
import { mapState } from 'client/libs/store';
import * as Analytics from 'client/libs/analytics';
import { mapState, mapActions } from 'client/libs/store';
import quests from 'common/script/content/quests';
import notificationsIcon from 'assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import successImage from 'assets/svg/success.svg';
// Notifications
import NEW_STUFF from './notifications/newStuff';
import GROUP_TASK_NEEDS_WORK from './notifications/groupTaskNeedsWork';
import GUILD_INVITATION from './notifications/guildInvitation';
import PARTY_INVITATION from './notifications/partyInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
import CARD_RECEIVED from './notifications/cardReceived';
import NEW_INBOX_MESSAGE from './notifications/newInboxMessage';
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
export default {
components: {
MenuDropdown,
MessageCount,
},
directives: {
// bTooltip,
// One component for each type
NEW_STUFF, GROUP_TASK_NEEDS_WORK,
GUILD_INVITATION, PARTY_INVITATION, CHALLENGE_INVITATION,
QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED,
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
},
data () {
return {
icons: Object.freeze({
notifications: notificationsIcon,
success: successImage,
}),
quests,
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_NEEDS_WORK',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
// NOTE: Those not listed here won't be shown in the notification panel!
handledNotifications: [
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
],
};
},
computed: {
...mapState({user: 'user.data'}),
party () {
return {name: ''};
// return this.user.party;
notificationsOrder () {
// Returns a map of NOTIFICATION_TYPE -> POSITION
const orderMap = {};
this.handledNotifications.forEach((type, index) => {
orderMap[type] = index;
});
return orderMap;
},
userNewMessages () {
// @TODO: For some reason data becomes corrupted. We should fix this on the server
let userNewMessages = [];
for (let key in this.user.newMessages) {
let message = this.user.newMessages[key];
if (message && message.name && message.value) {
message.key = key;
userNewMessages.push(message);
notifications () {
// Convert the notifications not stored in user.notifications
const notifications = [];
// Parties invitations
notifications.push(...this.user.invitations.parties.map(partyInvitation => {
return {
type: 'PARTY_INVITATION',
data: partyInvitation,
// Create a custom id for notifications outside user.notifications (must be unique)
id: `custom-party-invitation-${partyInvitation.id}`,
};
}));
// Guilds invitations
notifications.push(...this.user.invitations.guilds.map(guildInvitation => {
return {
type: 'GUILD_INVITATION',
data: guildInvitation,
// Create a custom id for notifications outside user.notifications (must be unique)
id: `custom-guild-invitation-${guildInvitation.id}`,
};
}));
// Quest invitation
if (this.user.party.quest.RSVPNeeded === true) {
notifications.push({
type: 'QUEST_INVITATION',
data: {
quest: this.user.party.quest.key,
partyId: this.user.party._id,
},
// Create a custom id for notifications outside user.notifications (must be unique)
id: `custom-quest-invitation-${this.user.party._id}`,
});
}
const orderMap = this.notificationsOrder;
// Push the notifications stored in user.notifications
// skipping those not defined in the handledNotifications object
notifications.push(...this.user.notifications.filter(notification => {
if (notification.type === 'UNALLOCATED_STATS_POINTS') {
if (!this.user.flags.classSelected || this.user.preferences.disableClasses) return false;
}
}
return userNewMessages;
},
groupNotifications () {
return this.$store.state.groupNotifications;
return orderMap[notification.type] !== undefined;
}));
// Sort notifications
notifications.sort((a, b) => { // a and b are notifications
const aOrder = orderMap[a.type];
const bOrder = orderMap[b.type];
if (aOrder === bOrder) return 0; // Same position
if (aOrder > bOrder) return 1; // b is higher
if (aOrder < bOrder) return -1; // a is higher
});
return notifications;
},
// The total number of notification, shown inside the dropdown
notificationsCount () {
let count = 0;
if (this.user.invitations.parties) {
count += this.user.invitations.parties.length;
}
if (this.user.purchased.plan && this.user.purchased.plan.mysteryItems.length) {
count++;
}
if (this.user.invitations.guilds) {
count += this.user.invitations.guilds.length;
}
if (this.user.flags.classSelected && !this.user.preferences.disableClasses && this.user.stats.points) {
count += this.user.stats.points > 0 ? 1 : 0;
}
if (this.userNewMessages) {
count += Object.keys(this.userNewMessages).length;
}
count += this.groupNotifications.length;
return count;
return this.notifications.length;
},
hasUnseenNotifications () {
return this.notifications.some((notification) => {
return notification.seen === false ? true : false;
});
},
},
methods: {
// @TODO: I hate this function, we can do better with a hashmap
selectNotificationValue (mysteryValue, invitationValue, cardValue,
unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) {
let user = this.user;
...mapActions({
readNotifications: 'notifications:readNotifications',
seeNotifications: 'notifications:seeNotifications',
}),
handleOpenStatusChange (openStatus) {
this.openStatus = openStatus === true ? 1 : 0;
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
return mysteryValue;
} 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;
} else if (user.flags.classSelected && !(user.preferences && user.preferences.disableClasses) && user.stats.points) {
return unallocatedValue;
} else if (!isEmpty(user.newMessages)) {
return messageValue;
} else if (!isEmpty(this.groupNotifications)) {
let groupNotificationTypes = map(this.groupNotifications, 'type');
if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVAL') !== -1) {
return groupApprovalRequested;
} else if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVED') !== -1) {
return groupApproved;
// Mark notifications as seen when the menu is opened
if (openStatus) this.markAllAsSeen();
},
markAllAsSeen () {
const idsToSee = this.notifications.map(notification => {
// We check explicitly for notification.id not starting with `custom-` because some
// notification don't follow the standard
// (all those not stored in user.notifications)
if (notification.seen === false && notification.id && notification.id.indexOf('custom-') !== 0) {
return notification.id;
}
return noneValue;
} else {
return noneValue;
}
},
hasQuestProgress () {
let user = this.user;
if (user.party.quest) {
let userQuest = quests[user.party.quest.key];
}).filter(id => Boolean(id));
if (!userQuest) {
return false;
if (idsToSee.length > 0) this.seeNotifications({notificationIds: idsToSee});
},
dismissAll () {
const idsToRead = this.notifications.map(notification => {
// We check explicitly for notification.id not starting with `custom-` because some
// notification don't follow the standard
// (all those not stored in user.notifications)
if (!this.isActionable(notification) && notification.id.indexOf('custom-') !== 0) {
return notification.id;
}
if (userQuest.boss && user.party.quest.progress.up > 0) {
return true;
}
if (userQuest.collect && user.party.quest.progress.collectedItems > 0) {
return true;
}
}
return false;
},
getQuestInfo () {
let user = this.user;
let questInfo = {};
if (user.party.quest) {
let userQuest = quests[user.party.quest.key];
}).filter(id => Boolean(id));
this.openStatus = 0;
questInfo.title = userQuest.text();
if (userQuest.boss) {
questInfo.body = this.$t('questTaskDamage', { damage: user.party.quest.progress.up.toFixed(1) });
} else if (userQuest.collect) {
questInfo.body = this.$t('questTaskCollection', { items: user.party.quest.progress.collectedItems });
}
}
return questInfo;
if (idsToRead.length > 0) this.readNotifications({notificationIds: idsToRead});
},
clearMessages (key) {
this.$store.dispatch('chat:markChatSeen', {groupId: key});
this.$delete(this.user.newMessages, key);
},
clearCards () {
this.$store.dispatch('chat:clearCards');
},
iconClasses () {
return this.selectNotificationValue(
'glyphicon-gift',
'glyphicon-user',
'glyphicon-envelope',
'glyphicon-plus-sign',
'glyphicon-comment',
'glyphicon-comment inactive',
'glyphicon-question-sign',
'glyphicon-ok-sign'
);
},
hasNoNotifications () {
return this.selectNotificationValue(false, false, false, false, false, true, false, false);
},
viewGroupApprovalNotification (notification) {
this.$store.state.groupNotifications = this.groupNotifications.filter(groupNotif => {
return groupNotif.id !== notification.id;
});
axios.post('/api/v3/notifications/read', {
notificationIds: [notification.id],
});
},
groupApprovalNotificationIcon (notification) {
if (notification.type === 'GROUP_TASK_APPROVAL') {
return 'glyphicon glyphicon-question-sign';
} else if (notification.type === 'GROUP_TASK_APPROVED') {
return 'glyphicon glyphicon-ok-sign';
}
},
go (path) {
this.$router.push(path);
},
navigateToGroup (key) {
if (key === this.party._id || key === this.user.party._id) {
this.go('/party');
return;
}
this.$router.push({ name: 'guild', params: { groupId: key }});
},
async reject (group) {
await this.$store.dispatch('guilds:rejectInvite', {groupId: group.id});
// @TODO: User.sync();
},
async accept (group, index, type) {
if (group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
return;
}
if (type === 'party') {
// @TODO: pretty sure mutability is wrong. Need to check React docs
// @TODO mutation to store data should only happen through actions
this.user.invitations.parties.splice(index, 1);
Analytics.updateUser({partyID: group.id});
} else {
this.user.invitations.guilds.splice(index, 1);
}
if (type === 'party') {
this.user.party._id = group.id;
this.$router.push('/party');
} else {
this.user.guilds.push(group.id);
this.$router.push(`/groups/guild/${group.id}`);
}
// @TODO: check for party , type: 'myGuilds'
await this.$store.dispatch('guilds:join', {guildId: group.id});
},
async questAccept (partyId) {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/accept'});
this.user.party.quest = quest;
},
async questReject (partyId) {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/reject'});
this.user.party.quest = quest;
},
showProfile () {
this.$root.$emit('habitica:show-profile', {
user: this.user,
startingPage: 'stats',
});
isActionable (notification) {
return this.actionableNotifications.indexOf(notification.type) !== -1;
},
},
};
@@ -3,7 +3,7 @@ menu-dropdown.item-user(:right="true")
div(slot="dropdown-toggle")
div(v-b-tooltip.hover.bottom="$t('user')")
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
.svg-icon.user(v-html="icons.user")
.top-menu-icon.svg-icon.user(v-html="icons.user")
.user-dropdown(slot="dropdown-content")
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
h3 {{ user.profile.name }}
+34 -68
View File
@@ -156,6 +156,19 @@ export default {
let lastShownNotifications = [];
let alreadyReadNotification = [];
// A list of notifications handled by this component,
// NOTE: Those not listed here won't be handled at all!
const handledNotifications = {};
[
'GUILD_PROMPT', 'DROPS_ENABLED', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE',
].forEach(type => {
handledNotifications[type] = true;
});
return {
yesterDailies: [],
levelBeforeYesterdailies: 0,
@@ -165,54 +178,31 @@ export default {
alreadyReadNotification,
isRunningYesterdailies: false,
nextCron: null,
handledNotifications,
};
},
computed: {
...mapState({user: 'user.data'}),
// https://stackoverflow.com/questions/42133894/vue-js-how-to-properly-watch-for-nested-properties/42134176#42134176
baileyShouldShow () {
return this.user.flags.newStuff;
},
userHp () {
return this.user.stats.hp;
},
userExp () {
return this.user.stats.exp;
},
userGp () {
return this.user.stats.gp;
},
userMp () {
return this.user.stats.mp;
},
userLvl () {
return this.user.stats.lvl;
},
...mapState({
user: 'user.data',
userHp: 'user.data.stats.hp',
userExp: 'user.data.stats.exp',
userGp: 'user.data.stats.gp',
userMp: 'user.data.stats.mp',
userLvl: 'user.data.stats.lvl',
userNotifications: 'user.data.notifications',
userAchievements: 'user.data.achievements', // @TODO: does this watch deeply?
armoireEmpty: 'user.data.flags.armoireEmpty',
questCompleted: 'user.data.party.quest.completed',
}),
userClassSelect () {
return !this.user.flags.classSelected && this.user.stats.lvl >= 10;
},
userNotifications () {
return this.user.notifications;
},
userAchievements () {
// @TODO: does this watch deeply?
return this.user.achievements;
},
armoireEmpty () {
return this.user.flags.armoireEmpty;
},
questCompleted () {
return this.user.party.quest.completed;
},
invitedToQuest () {
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
},
},
watch: {
baileyShouldShow () {
if (this.user.needsCron) return;
this.$root.$emit('bv::show::modal', 'new-stuff');
},
userHp (after, before) {
if (after <= 0) {
this.playSound('Death');
@@ -419,9 +409,6 @@ export default {
this.scheduleNextCron();
this.handleUserNotifications(this.user.notifications);
},
transferGroupNotification (notification) {
this.$store.state.groupNotifications.push(notification);
},
async handleUserNotifications (after) {
if (this.$store.state.isRunningYesterdailies) return;
@@ -434,23 +421,21 @@ export default {
let notificationsToRead = [];
let scoreTaskNotification = [];
this.$store.state.groupNotifications = []; // Flush group notifictions
after.forEach((notification) => {
// This notification type isn't implemented here
if (!this.handledNotifications[notification.type]) return;
if (this.lastShownNotifications.indexOf(notification.id) !== -1) {
return;
}
// Some notifications are not marked read here, so we need to fix this system
// to handle notifications differently
if (['GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVAL'].indexOf(notification.type) === -1) {
this.lastShownNotifications.push(notification.id);
if (this.lastShownNotifications.length > 10) {
this.lastShownNotifications.splice(0, 9);
}
this.lastShownNotifications.push(notification.id);
if (this.lastShownNotifications.length > 10) {
this.lastShownNotifications.splice(0, 9);
}
let markAsRead = true;
// @TODO: Use factory function instead
switch (notification.type) {
case 'GUILD_PROMPT':
@@ -507,14 +492,6 @@ export default {
if (notification.data.mp) this.mp(notification.data.mp);
}
break;
case 'GROUP_TASK_APPROVAL':
this.transferGroupNotification(notification);
markAsRead = false;
break;
case 'GROUP_TASK_APPROVED':
this.transferGroupNotification(notification);
markAsRead = false;
break;
case 'SCORED_TASK':
// Search if it is a read notification
for (let i = 0; i < this.alreadyReadNotification.length; i++) {
@@ -538,16 +515,6 @@ export default {
this.$root.$emit('bv::show::modal', 'login-incentives');
}
break;
default:
if (notification.data.headerText && notification.data.bodyText) {
// @TODO:
// let modalScope = this.$new();
// modalScope.data = notification.data;
// this.openModal('generic', {scope: modalScope});
} else {
markAsRead = false; // If the notification is not implemented, skip it
}
break;
}
if (markAsRead) notificationsToRead.push(notification.id);
@@ -561,6 +528,7 @@ export default {
});
}
// @TODO this code is never run because userReadNotifsPromise is never true
if (userReadNotifsPromise) {
userReadNotifsPromise.then(() => {
// Only run this code for scoring approved tasks
@@ -589,8 +557,6 @@ export default {
});
}
this.user.notifications = []; // reset the notifications
this.checkUserAchievements();
},
},
@@ -46,7 +46,7 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
button.btn.btn-primary(@click='showStripe({gift, uuid: userReceivingGems._id})') {{ $t('card') }}
button.btn.btn-warning(@click='openPaypalGift({gift: gift, giftedTo: userReceivingGems._id})') PayPal
button.btn.btn-success(@click="amazonPaymentsInit({type: 'single', gift, giftedTo: userReceivingGems._id})") Amazon Payments
button.btn.btn-secondary(@click='close()') {{$t('cancel')}}
button.btn.btn-secondary(@click='close()') {{$t('cancel')}}
</template>
<style lang="scss">
+1 -2
View File
@@ -35,7 +35,7 @@
h5 {{ $t('characterBuild') }}
h6(v-once) {{ $t('class') + ': ' }}
// @TODO: what is classText
span(v-if='classText') {{ classText }}&nbsp;
// span(v-if='classText') {{ classText }}&nbsp;
button.btn.btn-danger.btn-xs(@click='changeClassForUser(true)', v-once) {{ $t('changeClass') }}
small.cost &nbsp; 3 {{ $t('gems') }}
// @TODO add icon span.Pet_Currency_Gem1x.inline-gems
@@ -291,7 +291,6 @@ export default {
// Guide.goto('intro', 0, true);
},
showBailey () {
this.user.flags.newStuff = true;
this.$root.$emit('bv::show::modal', 'new-stuff');
},
hasBackupAuthOption (networkKeyToCheck) {
@@ -1,40 +1,52 @@
<template lang="pug">
div.row
span.col-4(v-if="quest.collect") {{ $t('collect') + ':' }}
span.col-8(v-if="quest.collect")
.row(:class="{'small-version': smallVersion}")
template(v-if="quest.collect")
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('collect') + ':' }}
span.col-8
div(v-for="(collect, key) of quest.collect")
span {{ collect.count }} {{ getCollectText(collect) }}
span.col-4(v-if="quest.boss") {{ $t('bossHP') + ':' }}
span.col-8(v-if="quest.boss") {{ quest.boss.hp }}
template(v-if="quest.boss")
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('bossHP') + ':' }}
span.col-8 {{ quest.boss.hp }}
span.col-4 {{ $t('difficulty') + ':' }}
span.col-8
span.svg-icon.inline.icon-16(v-for="star of stars()", v-html="icons[star]")
span.title(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('difficulty') + ':' }}
span.col-8
.svg-icon.inline(
v-for="star of stars()", v-html="icons[star]",
:class="smallVersion ? 'icon-12' : 'icon-16'",
)
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/colors.scss';
.title {
text-align: left;
font-weight: bold;
white-space: nowrap;
}
.col-4{
text-align: left;
font-weight: bold;
white-space: nowrap;
height: 16px;
width: 80px;
}
.col-8 {
text-align: left;
}
.col-8:not(:last-child) {
margin-bottom: 4px;
}
span.svg-icon.inline.icon-16 {
margin-right: 4px;
.col-8 {
text-align: left;
}
.col-8:not(:last-child) {
margin-bottom: 4px;
}
.svg-icon {
margin-right: 4px;
}
.small-version {
font-size: 12px;
line-height: 1.33;
.svg-icon {
margin-top: 1px;
}
}
</style>
<script>
@@ -43,6 +55,15 @@
import svgStarEmpty from 'assets/svg/difficulty-star-empty.svg';
export default {
props: {
quest: {
type: Object,
},
smallVersion: {
type: Boolean,
default: false,
},
},
data () {
return {
icons: Object.freeze({
@@ -88,10 +109,5 @@
}
},
},
props: {
quest: {
type: Object,
},
},
};
</script>
+6 -2
View File
@@ -1,5 +1,5 @@
<template lang="pug">
#front
#front.static-view
noscript.banner {{ $t('jsDisabledHeadingFull') }}
br
a(href='http://www.enable-javascript.com/', target='_blank') {{ $t('jsDisabledLink') }}
@@ -118,8 +118,12 @@
.seamless_stars_varied_opacity_repeat
</template>
<style lang='scss'>
@import '~client/assets/scss/static.scss';
</style>
<style lang="scss" scoped>
@import '~client/assets/scss/static.scss';
@import '~client/assets/scss/colors.scss';
#front {
.form-text a {
+15 -74
View File
@@ -1,82 +1,23 @@
<template lang='pug'>
div
.media
.align-self-center.right-margin(:class='baileyClass')
.media-body
h1.align-self-center(v-markdown='$t("newStuff")')
h2 1/30/2018 - HABITICA BIRTHDAY CELEBRATION, LAST CHANCE FOR WINTER WONDERLAND ITEMS, AND CONTINUED RESOLUTION PLOT-LINE
hr
.promo_habit_birthday_2018.center-block
h3 Habitica Birthday Party!
p January 31st is Habitica's Birthday! Thank you so much for being a part of our community - it means a lot.
p Now come join us and the NPCs as we celebrate!
h4 Cake for Everybody!
p(v-markdown='"In honor of the festivities, everyone has been awarded an assortment of yummy cake to feed to your pets! Plus, for the next two days [Alexander the Merchant](/shops/market) is selling cake in his shop, and cake will sometimes drop when you complete your tasks. Cake works just like normal pet food, but if you want to know what type of pet likes each slice, [the wiki has spoilers](http://habitica.wikia.com/wiki/Food)."')
h4 Party Robes
p There are Party Robes available for free in the Rewards column! Don them with pride.
h4 Birthday Bash Achievement
p In honor of Habitica's birthday, everyone has been awarded the Habitica Birthday Bash achievement! This achievement stacks for each Birthday Bash you celebrate with us.
.media
.media-body
h3 Last Chance for Frost Sprite Set
p(v-markdown='"Reminder: this is the final day to [subscribe](/user/settings/subscription) and receive the Frost Sprite Set! Subscribing also lets you buy gems for gold. The longer your subscription, the more gems you get!"')
p Thanks so much for your support! You help keep Habitica running.
.small by Beffymaroo
h3 Last Chance for Starry Night and Holly Hatching Potions
p(v-markdown='"Reminder: this is the final day to [buy Starry Night and Holly Hatching Potions!](/shops/market) If they come back, it won\'t be until next year at the earliest, so don\'t delay!"')
.small by Vampitch, JinjooHat, Lemoness, and SabreCat
h3 Resolution Plot-Line: Broken Buildings
p Lemoness, SabreCat, and Beffymaroo call an important meeting to address the rumors that are flying about this strange outbreak of Habiticans who are suddenly losing all faith in their ability to complete their New Year's Resolutions.
p “Thank you all for coming,” Lemoness says. “I'm afraid that we have some very serious news to share, but we ask that you remain calm.
.promo_starry_potions.left-margin
p While it's natural to feel a little disheartened as the end of January approaches,” Beffymaroo says, “these sudden outbreaks appear to have some strange magical origin. We're still investigating the exact cause, but we do know that the buildings where the affected Habiticans live often seem to sustain some damage immediately before the attack.
p SabreCat clears his throat. For this reason, we strongly encourage everyone to stay away from broken-down structures, and if you feel any strange tremors or hear odd sounds, please report them immediately.
p(v-markdown='"“Stay safe, Habiticans.” Lemoness flashes her best comforting smile. “And remember that if your New Year\'s Resolution goals seem daunting, you can always seek support in the [New Year\'s Resolution Guild](https://habitica.com/groups/guild/6e6a8bd3-9f5f-4351-9188-9f11fcd80a99).”"')
p How mysterious! Hopefully they'll get to the bottom of this soon.
hr
.static-view(v-html="html")
</template>
<style lang='scss' scoped>
@import '~client/assets/scss/static.scss';
.center-block {
margin: 0 auto 1em auto;
}
.left-margin {
margin-left: 1em;
}
.right-margin {
margin-right: 1em;
}
.bottom-margin {
margin-bottom: 1em;
}
.small {
margin-bottom: 1em;
}
<style lang='scss'>
@import '~client/assets/scss/static.scss';
</style>
<script>
import markdown from 'client/directives/markdown';
import axios from 'axios';
export default {
data () {
let worldDmg = {
bailey: false,
};
return {
baileyClass: {
'npc_bailey_broken': worldDmg.bailey, // eslint-disable-line
'npc_bailey': !worldDmg.bailey, // eslint-disable-line
},
};
},
directives: {
markdown,
},
};
export default {
data () {
return {
html: '',
};
},
async mounted () {
let response = await axios.get('/api/v3/news');
this.html = response.data.html;
},
};
</script>
@@ -1,5 +1,5 @@
<template lang="pug">
.container-fluid
.container-fluid.static-view
.row
.col-md-6.offset-3
h1 {{ $t('overview') }}
@@ -11,9 +11,11 @@
p(v-markdown="$t('overviewQuestions')")
</template>
<style lang='scss' scoped>
<style lang='scss'>
@import '~client/assets/scss/static.scss';
</style>
<style lang='scss' scoped>
.container-fluid {
margin-top: 56px;
}
@@ -4,15 +4,15 @@ div
.claim-bottom-message.col-12
.task-unclaimed.d-flex.justify-content-between(v-if='!approvalRequested && !multipleApprovalsRequested')
span {{ message }}
a.text-right(@click='claim()', v-if='!userIsAssigned') Claim
a.text-right(@click='unassign()', v-if='userIsAssigned') Remove Claim
a.text-right(@click='claim()', v-if='!userIsAssigned') {{ $t('claim') }}
a.text-right(@click='unassign()', v-if='userIsAssigned') {{ $t('removeClaim') }}
.row.task-single-approval(v-if='approvalRequested')
.col-6.text-center
a(@click='approve()') Approve Task
// @TODO: Implement in v2 .col-6.text-center
a Needs work
a(@click='approve()') {{ $t('approveTask') }}
.col-6.text-center
a(@click='needsWork()') {{ $t('needsWork') }}
.text-center.task-multi-approval(v-if='multipleApprovalsRequested')
a(@click='showRequests()') View Requests
a(@click='showRequests()') {{ $t('viewRequests') }}
</template>
<style lang="scss", scoped>
@@ -116,15 +116,21 @@ export default {
approve () {
if (!confirm(this.$t('confirmApproval'))) return;
let userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:unassignTask', {
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(0, 1);
this.task.approvals.splice(0, 1);
},
reject () {
needsWork () {
if (!confirm(this.$t('confirmNeedsWork'))) return;
let userIdNeedsMoreWork = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(0, 1);
},
showRequests () {
this.$root.$emit('bv::show::modal', 'approval-modal');
@@ -1,11 +1,13 @@
<template lang="pug">
b-modal#approval-modal(title="Approve Task", size='md', :hide-footer="true")
b-modal#approval-modal(:title="$t('approveTask')", size='md', :hide-footer="true")
.modal-body
.row.approval(v-for='(approval, index) in task.approvals')
.col-8
strong {{approval.userId.profile.name}}
.col-2
button.btn.btn-primary(@click='approve(index)') Approve
button.btn.btn-primary(@click='approve(index)') {{ $t('approve') }}
.col-2
button.btn.btn-secondary(@click='needsWork(index)') {{ $t('needsWork') }}
.modal-footer
button.btn.btn-secondary(@click='close()') {{$t('close')}}
</template>
@@ -22,15 +24,24 @@ export default {
props: ['task'],
methods: {
approve (index) {
if (!confirm('Are you sure you want to approve this task?')) return;
if (!confirm(this.$t('confirmApproval'))) return;
let userIdToApprove = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:unassignTask', {
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(index, 1);
this.task.approvals.splice(index, 1);
},
needsWork (index) {
if (!confirm(this.$t('confirmNeedsWork'))) return;
let userIdNeedsMoreWork = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(index, 1);
},
close () {
this.$root.$emit('bv::hide::modal', 'approval-modal');
},
@@ -3,7 +3,7 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
-->
<template lang="pug">
.habitica-menu-dropdown.item-with-icon.dropdown(@click="toggleDropdown()", :class="{open: isDropdownOpen}")
.habitica-menu-dropdown.dropdown(@click="toggleDropdown()", :class="{open: isOpen}")
.habitica-menu-dropdown-toggle
slot(name="dropdown-toggle")
.dropdown-menu(:class="{'dropdown-menu-right': right}")
@@ -43,7 +43,7 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
&.open {
.dropdown-menu {
display: block;
margin-top: 16px;
margin-top: 8px;
}
}
}
@@ -51,12 +51,22 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
<script>
export default {
props: ['right'],
props: {
right: Boolean,
openStatus: Number,
},
data () {
return {
isDropdownOpen: false,
};
},
computed: {
isOpen () {
// Open status is a number so we can tell if the value was passed
if (this.openStatus !== undefined) return this.openStatus === 1 ? true : false;
return this.isDropdownOpen;
},
},
mounted () {
document.documentElement.addEventListener('click', this._clickOutListener);
},
@@ -65,12 +75,13 @@ export default {
},
methods: {
_clickOutListener (e) {
if (!this.$el.contains(e.target) && this.isDropdownOpen) {
if (!this.$el.contains(e.target) && this.isOpen) {
this.toggleDropdown();
}
},
toggleDropdown () {
this.isDropdownOpen = !this.isDropdownOpen;
this.isDropdownOpen = !this.isOpen;
this.$emit('toggled', this.isDropdownOpen);
},
},
};
-5
View File
@@ -86,8 +86,3 @@ export async function markChatSeen (store, payload) {
let response = await axios.post(url);
return response.data.data;
}
// @TODO: should this be here?
// function clearCards () {
// User.user._wrapped && User.set({'flags.cardReceived':false});
// }
+32 -6
View File
@@ -1,6 +1,7 @@
import axios from 'axios';
import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex';
import * as Analytics from 'client/libs/analytics';
export async function getPublicGuilds (store, payload) {
let params = {
@@ -49,12 +50,26 @@ export async function getGroup (store, payload) {
export async function join (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.guildId}/join`);
const groupId = payload.groupId;
const type = payload.type;
const user = store.state.user.data;
const invitations = user.invitations;
// @TODO: abstract for parties as well
store.state.user.data.guilds.push(payload.guildId);
if (payload.type === 'myGuilds') {
let response = await axios.post(`/api/v3/groups/${groupId}/join`);
if (type === 'guild') {
const invitationI = invitations.guilds.findIndex(i => i.id === groupId);
if (invitationI !== -1) invitations.guilds.splice(invitationI, 1);
user.guilds.push(groupId);
store.state.myGuilds.push(response.data.data);
} else if (type === 'party') {
const invitationI = invitations.parties.findIndex(i => i.id === groupId);
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
user.party._id = groupId;
Analytics.updateUser({partyID: groupId});
}
return response.data.data;
@@ -111,9 +126,20 @@ export async function update (store, payload) {
}
export async function rejectInvite (store, payload) {
let response = await axios.post(`/api/v3/groups/${payload.groupId}/reject-invite`);
const groupId = payload.groupId;
const type = payload.type;
const user = store.state.user.data;
const invitations = user.invitations;
// @TODO: refresh or add guild
let response = await axios.post(`/api/v3/groups/${groupId}/reject-invite`);
if (type === 'guild') {
const invitationI = invitations.guilds.findIndex(i => i.id === groupId);
if (invitationI !== -1) invitations.guilds.splice(invitationI, 1);
} else if (type === 'party') {
const invitationI = invitations.parties.findIndex(i => i.id === groupId);
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
}
return response;
}
+16 -2
View File
@@ -1,13 +1,27 @@
import axios from 'axios';
export async function readNotification (store, payload) {
let url = `api/v3/notifications/${payload.notificationId}/read`;
let url = `/api/v3/notifications/${payload.notificationId}/read`;
let response = await axios.post(url);
return response.data.data;
}
export async function readNotifications (store, payload) {
let url = 'api/v3/notifications/read';
let url = '/api/v3/notifications/read';
let response = await axios.post(url, {
notificationIds: payload.notificationIds,
});
return response.data.data;
}
export async function seeNotification (store, payload) {
let url = `/api/v3/notifications/${payload.notificationId}/see`;
let response = await axios.post(url);
return response.data.data;
}
export async function seeNotifications (store, payload) {
let url = '/api/v3/notifications/see';
let response = await axios.post(url, {
notificationIds: payload.notificationIds,
});
+5
View File
@@ -191,6 +191,11 @@ export async function unassignTask (store, payload) {
return response.data.data;
}
export async function needsWork (store, payload) {
let response = await axios.post(`/api/v3/tasks/${payload.taskId}/needs-work/${payload.userId}`);
return response.data.data;
}
export async function getGroupApprovals (store, payload) {
let response = await axios.get(`/api/v3/approvals/group/${payload.groupId}`);
return response.data.data;
+4
View File
@@ -120,6 +120,10 @@ export function openMysteryItem () {
return axios.post('/api/v3/user/open-mystery-item');
}
export function newStuffLater () {
return axios.post('/api/v3/news/tell-me-later');
}
export async function rebirth () {
let result = await axios.post('/api/v3/user/rebirth');
-1
View File
@@ -128,7 +128,6 @@ export default function () {
modalStack: [],
equipmentDrawerOpen: true,
groupPlans: [],
groupNotifications: [],
isRunningYesterdailies: false,
},
});