Merge branch 'release' into sabrecat/timetrav-bgs
This commit is contained in:
@@ -28,7 +28,10 @@
|
||||
</div>
|
||||
<div
|
||||
id="app"
|
||||
:class="{'casting-spell': castingSpell}"
|
||||
:class="{
|
||||
'casting-spell': castingSpell,
|
||||
'resting': showRestingBanner
|
||||
}"
|
||||
>
|
||||
<banned-account-modal />
|
||||
<amazon-payments-modal v-if="!isStaticPage" />
|
||||
@@ -66,7 +69,10 @@
|
||||
</div>
|
||||
<notifications-display />
|
||||
<app-menu />
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="container-fluid"
|
||||
:class="{'no-margin': noMargin}"
|
||||
>
|
||||
<app-header />
|
||||
<buyModal
|
||||
:item="selectedItemToBuy || {}"
|
||||
@@ -83,7 +89,7 @@
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<app-footer />
|
||||
<app-footer v-if="!hideFooter" />
|
||||
<audio
|
||||
id="sound"
|
||||
ref="sound"
|
||||
@@ -97,13 +103,20 @@
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
#app {
|
||||
height: calc(100% - 56px); /* 56px is the menu */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.resting {
|
||||
--banner-resting-height: #{$restingToolbarHeight};
|
||||
}
|
||||
|
||||
&.giftingBanner {
|
||||
--banner-gifting-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#loading-screen-inapp {
|
||||
@@ -148,6 +161,13 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.notification {
|
||||
border-radius: 1000px;
|
||||
background-color: $green-10;
|
||||
@@ -160,7 +180,7 @@
|
||||
|
||||
.resting-banner {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
height: $restingToolbarHeight;
|
||||
background-color: $blue-10;
|
||||
top: 0;
|
||||
z-index: 1300;
|
||||
@@ -302,7 +322,13 @@ export default {
|
||||
return this.$t(`tip${tipNumber}`);
|
||||
},
|
||||
showRestingBanner () {
|
||||
return !this.bannerHidden && this.user.preferences.sleep;
|
||||
return !this.bannerHidden && this.user && this.user.preferences.sleep;
|
||||
},
|
||||
noMargin () {
|
||||
return ['privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
hideFooter () {
|
||||
return ['privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
.promo_armoire_backgrounds_202001 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -175px;
|
||||
background-position: 0px -323px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_g1g1_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -471px;
|
||||
background-position: -403px -471px;
|
||||
width: 357px;
|
||||
height: 144px;
|
||||
}
|
||||
.promo_mystery_202001 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -241px -616px;
|
||||
background-position: -241px -619px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_seasonal_shop {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -521px -619px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_snowballs {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
@@ -24,25 +30,25 @@
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -358px -471px;
|
||||
background-position: -761px -471px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_winter_potions_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -175px;
|
||||
background-position: 0px -175px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_quests_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -323px;
|
||||
background-position: -424px -175px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_wonderland_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -323px;
|
||||
background-position: 0px -471px;
|
||||
width: 402px;
|
||||
height: 147px;
|
||||
}
|
||||
@@ -52,9 +58,27 @@
|
||||
width: 468px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_wintery_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -684px -619px;
|
||||
width: 152px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_wintery_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -323px;
|
||||
width: 420px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_wintery_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -449px -338px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.scene_list {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -616px;
|
||||
background-position: 0px -619px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 95 KiB |
@@ -7,3 +7,6 @@ $npc_quests_flavor: 'nye';
|
||||
$npc_seasonal_flavor: 'nye';
|
||||
$npc_timetravelers_flavor: 'winter';
|
||||
$npc_tavern_flavor: 'nye';
|
||||
|
||||
$restingToolbarHeight: 40px;
|
||||
$menuToolbarHeight: 56px;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M15.7 14.3l-4.8-4.8c.7-1 1.1-2.2 1.1-3.5 0-3.3-2.7-6-6-6S0 2.7 0 6s2.7 6 6 6c1.3 0 2.5-.4 3.5-1.1l4.8 4.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4zM6 10c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="24" viewBox="0 0 32 24">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#9A62FF" d="M28 0H4C1.79 0 0 1.79 0 4v16c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V4c0-2.21-1.79-4-4-4z"/>
|
||||
<path fill="#BDA8FF" d="M28 20H4V4l12 10L28 4z"/>
|
||||
<path fill="#D5C8FF" d="M28 20H4V7l12 10L28 7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
@@ -32,7 +32,7 @@
|
||||
:avatar-only="true"
|
||||
:with-background="true"
|
||||
/>
|
||||
<!-- @TOOD: Sleep +generatedAvatar({sleep:true})-->
|
||||
<!-- @TODO: Sleep +generatedAvatar({sleep:true})-->
|
||||
<span class="knockout"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,8 +71,8 @@ export default {
|
||||
this.$root.$emit('bv::hide::modal', 'streak');
|
||||
},
|
||||
suppressModals () {
|
||||
const surpress = !!this.user.preferences.suppressModals.streak;
|
||||
this.$store.dispatch('user:set', { 'preferences.suppressModals.streak': surpress });
|
||||
const suppress = !!this.user.preferences.suppressModals.streak;
|
||||
this.$store.dispatch('user:set', { 'preferences.suppressModals.streak': suppress });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<template>
|
||||
<div class="form-wrapper">
|
||||
<div class="warning-banner d-flex" v-if="forgotPassword && preOutage">
|
||||
<div class="warning-box ml-auto my-auto mr-2 d-flex">
|
||||
<div
|
||||
class="svg-icon exclamation m-auto"
|
||||
v-html="icons.exclamation"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-auto my-auto">
|
||||
Habitica emails will be temporarily unavailable on <strong>January 11, 2020</strong> from
|
||||
<strong>1:00 - 7:00 AM EST</strong>.
|
||||
</div>
|
||||
</div>
|
||||
<div id="top-background">
|
||||
<div class="seamless_stars_varied_opacity_repeat"></div>
|
||||
</div>
|
||||
@@ -546,15 +559,36 @@
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
color: $white;
|
||||
background-color: $maroon-100;
|
||||
height: 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
font-weight: bold;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.exclamation {
|
||||
width: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import moment from 'moment';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
@@ -576,6 +610,7 @@ export default {
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
exclamation,
|
||||
gryphon,
|
||||
habiticaIcon,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
@@ -635,6 +670,9 @@ export default {
|
||||
|| this.passwordInvalid
|
||||
|| this.passwordConfirmInvalid;
|
||||
},
|
||||
preOutage () {
|
||||
return moment.utc().isBefore('2020-01-12');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
@@ -700,7 +738,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: implement langauge and invite accepting
|
||||
// @TODO: implement language and invite accepting
|
||||
// var url = ApiUrl.get() + "/api/v4/user/auth/local/register";
|
||||
// if (location.search && location.search.indexOf('Invite=') !== -1)
|
||||
// { // matches groupInvite and partyInvite
|
||||
|
||||
@@ -294,8 +294,9 @@
|
||||
justify-content: space-evenly;
|
||||
background-color: $gray-700;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
border-radius: .25em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
.value {
|
||||
@@ -326,6 +327,8 @@
|
||||
}
|
||||
|
||||
> div.muted {
|
||||
margin: 8px;
|
||||
|
||||
.value {
|
||||
opacity: 0.5;
|
||||
font-size: 20px;
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showCategorySelect && creating"
|
||||
v-if="showCategorySelect"
|
||||
class="category-box"
|
||||
>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
|
||||
@@ -68,6 +68,17 @@
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.col-10.standard-page {
|
||||
// full width on smaller devices
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header-row {
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
|
||||
@@ -88,6 +88,17 @@
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header-row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.col-10.standard-page {
|
||||
// full width on smaller devices
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header-row {
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="!inbox && user.contributor.admin && msg.flagCount"
|
||||
v-if="user.contributor.admin && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
{{ flagCountDescription }}
|
||||
@@ -27,8 +27,7 @@
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip
|
||||
:title="msg.timestamp | date"
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4">({{ msg.client }})</span>
|
||||
</p>
|
||||
@@ -37,21 +36,12 @@
|
||||
class="text"
|
||||
v-html="atHighlight(parseMarkdown(msg.text))"
|
||||
></div>
|
||||
<div
|
||||
v-if="isMessageReported && (inbox === true)"
|
||||
class="reported"
|
||||
>
|
||||
<span v-once>{{ $t('reportedMessage') }}</span>
|
||||
<br>
|
||||
<span v-once>{{ $t('canDeleteNow') }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="!inbox"
|
||||
class="action d-flex align-items-center"
|
||||
@click="copyAsTodo(msg)"
|
||||
>
|
||||
@@ -62,7 +52,7 @@
|
||||
<div>{{ $t('copyAsTodo') }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="(inbox || (user.flags.communityGuidelinesAccepted && msg.uuid !== 'system'))
|
||||
v-if="(user.flags.communityGuidelinesAccepted && msg.uuid !== 'system')
|
||||
&& (!isMessageReported || user.contributor.admin)"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
@@ -77,7 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.uuid === user._id || inbox || user.contributor.admin"
|
||||
v-if="msg.uuid === user._id || user.contributor.admin"
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
@@ -91,7 +81,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!inbox"
|
||||
v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}"
|
||||
class="ml-auto d-flex"
|
||||
>
|
||||
@@ -121,7 +110,7 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!msg.likes[user._id] && !inbox">{{ $t('like') }}</span>
|
||||
<span v-if="!msg.likes[user._id]">{{ $t('like') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,15 +194,9 @@
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.reported {
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
@@ -244,10 +227,6 @@ export default {
|
||||
},
|
||||
props: {
|
||||
msg: {},
|
||||
inbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupId: {},
|
||||
},
|
||||
data () {
|
||||
@@ -311,6 +290,10 @@ export default {
|
||||
if (this.msg.flagCount < CHAT_FLAG_FROM_SHADOW_MUTE) return 'Message hidden';
|
||||
return 'Message hidden (shadow-muted)';
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
@@ -372,11 +355,6 @@ export default {
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
if (this.inbox) {
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('chat:deleteChat', {
|
||||
groupId: this.groupId,
|
||||
chatId: message.id,
|
||||
|
||||
@@ -35,13 +35,11 @@
|
||||
v-for="msg in messages"
|
||||
v-if="chat && canViewFlag(msg)"
|
||||
:key="msg.id"
|
||||
:class="{row: inbox}"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-if="user._id !== msg.uuid"
|
||||
class="d-flex"
|
||||
:class="{'flex-grow-1': inbox}"
|
||||
>
|
||||
<avatar
|
||||
v-if="msg.userStyles
|
||||
@@ -51,16 +49,13 @@
|
||||
:avatar-only="true"
|
||||
:override-top-padding="'14px'"
|
||||
:hide-class-badge="true"
|
||||
:class="{'inbox-avatar-left': inbox}"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div
|
||||
class="card"
|
||||
:class="{'col-10': inbox}"
|
||||
>
|
||||
<chat-card
|
||||
:msg="msg"
|
||||
:inbox="inbox"
|
||||
:group-id="groupId"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@@ -72,15 +67,12 @@
|
||||
<div
|
||||
v-if="user._id === msg.uuid"
|
||||
class="d-flex"
|
||||
:class="{'flex-grow-1': inbox}"
|
||||
>
|
||||
<div
|
||||
class="card"
|
||||
:class="{'col-10': inbox}"
|
||||
>
|
||||
<chat-card
|
||||
:msg="msg"
|
||||
:inbox="inbox"
|
||||
:group-id="groupId"
|
||||
@message-liked="messageLiked"
|
||||
@message-removed="messageRemoved"
|
||||
@@ -95,7 +87,6 @@
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
:class="{'inbox-avatar-right': inbox}"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
</div>
|
||||
@@ -144,16 +135,6 @@
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.inbox-avatar-left {
|
||||
margin-left: -1rem;
|
||||
margin-right: 2.5rem;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.inbox-avatar-right {
|
||||
margin-left: -3.5rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
@@ -209,10 +190,6 @@ export default {
|
||||
},
|
||||
props: {
|
||||
chat: {},
|
||||
inbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupType: {},
|
||||
groupId: {},
|
||||
groupName: {},
|
||||
@@ -260,12 +237,6 @@ export default {
|
||||
this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight);
|
||||
// disable scroll
|
||||
container.style.overflowY = 'hidden';
|
||||
|
||||
const canLoadMore = this.inbox && !this.isLoading && this.canLoadMore;
|
||||
if (canLoadMore) {
|
||||
await this.$emit('triggerLoad');
|
||||
this.handleScrollBack = true;
|
||||
}
|
||||
},
|
||||
canViewFlag (message) {
|
||||
if (message.uuid === this.user._id) return true;
|
||||
@@ -282,7 +253,7 @@ export default {
|
||||
const promises = [];
|
||||
const noProfilesLoaded = Object.keys(this.cachedProfileData).length === 0;
|
||||
|
||||
// @TODO: write an explination
|
||||
// @TODO: write an explanation
|
||||
// @TODO: Remove this after enough messages are cached
|
||||
if (
|
||||
!noProfilesLoaded
|
||||
@@ -380,11 +351,6 @@ export default {
|
||||
this.chat.splice(chatIndex, 1, message);
|
||||
},
|
||||
messageRemoved (message) {
|
||||
if (this.inbox) {
|
||||
this.$emit('message-removed', message);
|
||||
return;
|
||||
}
|
||||
|
||||
const chatIndex = findIndex(this.chat, chatMessage => chatMessage.id === message.id);
|
||||
this.chat.splice(chatIndex, 1);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="footer text-center">
|
||||
<button
|
||||
v-if="user.contributor.admin && abuseObject.flagCount > 0"
|
||||
v-if="user.contributor.admin"
|
||||
class="pull-left btn btn-danger"
|
||||
@click="clearFlagCount()"
|
||||
>
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
<div class="task-option">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="excercise"
|
||||
id="exercise"
|
||||
v-model="taskCategories"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
@@ -391,7 +391,7 @@
|
||||
<label
|
||||
v-once
|
||||
class="custom-control-label"
|
||||
for="excercise"
|
||||
for="exercise"
|
||||
>{{ $t('exercise') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
|
||||
import FaceAvatar from './faceAvatar.vue';
|
||||
import Avatar from './avatar.vue';
|
||||
import { userStyles } from '../../config/storybook/mock.data';
|
||||
import content from '../../../common/script/content/index';
|
||||
import getters from '@/store/getters';
|
||||
|
||||
|
||||
storiesOf('Face Avatar', module)
|
||||
.add('simple', () => ({
|
||||
components: { FaceAvatar },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<face-avatar :member="user"></face-avatar>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
user: userStyles,
|
||||
};
|
||||
},
|
||||
}))
|
||||
.add('compare', () => ({
|
||||
components: { FaceAvatar, Avatar },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<face-avatar :member="user"></face-avatar>
|
||||
<avatar :member="user"></avatar>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
user: userStyles,
|
||||
};
|
||||
},
|
||||
state: {
|
||||
content,
|
||||
},
|
||||
store: {
|
||||
getters,
|
||||
state: {
|
||||
content,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div
|
||||
class="face-avatar"
|
||||
:style="{width, height}"
|
||||
>
|
||||
<div class="character-sprites">
|
||||
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
|
||||
<template v-for="(klass, item) in visualBuffs">
|
||||
<span
|
||||
v-if="member.stats.buffs[item] && showVisualBuffs"
|
||||
:key="klass"
|
||||
:class="klass"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Show flower ALL THE TIME!!!-->
|
||||
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
|
||||
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
|
||||
<!-- Show avatar only if not currently affected by visual buff-->
|
||||
<template v-if="showAvatar()">
|
||||
<span :class="[skinClass]"></span><span :class="['head_0']"></span>
|
||||
<template v-for="type in ['bangs', 'base', 'mustache', 'beard']">
|
||||
<span
|
||||
:key="type"
|
||||
:class="[getHairClass(type)]"
|
||||
></span>
|
||||
</template>
|
||||
<span :class="[getGearClass('body')]"></span>
|
||||
<span :class="[getGearClass('eyewear')]"></span>
|
||||
<span :class="[getGearClass('head')]"></span>
|
||||
<span :class="[getGearClass('headAccessory')]"></span>
|
||||
<span :class="['hair_flower_' + member.preferences.hair.flower]"></span>
|
||||
</template>
|
||||
<!-- Resting--><span
|
||||
v-if="member.preferences.sleep"
|
||||
class="zzz"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.face-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: solid 2px currentColor;
|
||||
border-radius: 18px;
|
||||
image-rendering: pixelated;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.character-sprites {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: -25px -41px;
|
||||
}
|
||||
|
||||
.character-sprites span {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
avatarOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideClassBadge: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withBackground: {
|
||||
type: Boolean,
|
||||
},
|
||||
overrideAvatarGear: {
|
||||
type: Object,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 147,
|
||||
},
|
||||
showVisualBuffs: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
flatGear: 'content.gear.flat',
|
||||
}),
|
||||
hasClass () {
|
||||
return this.$store.getters['members:hasClass'](this.member);
|
||||
},
|
||||
isBuffed () {
|
||||
return this.$store.getters['members:isBuffed'](this.member);
|
||||
},
|
||||
visualBuffs () {
|
||||
return {
|
||||
snowball: 'snowman',
|
||||
spookySparkles: 'ghost',
|
||||
shinySeed: `avatar_floral_${this.member.stats.class}`,
|
||||
seafoam: 'seafoam_star',
|
||||
};
|
||||
},
|
||||
skinClass () {
|
||||
const baseClass = `skin_${this.member.preferences.skin}`;
|
||||
|
||||
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
|
||||
},
|
||||
costumeClass () {
|
||||
return this.member.preferences.costume ? 'costume' : 'equipped';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getGearClass (gearType) {
|
||||
let result = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
|
||||
result = this.overrideAvatarGear[gearType];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
showAvatar () {
|
||||
if (!this.showVisualBuffs) return true;
|
||||
|
||||
const { buffs } = this.member.stats;
|
||||
|
||||
return !buffs.snowball && !buffs.spookySparkles && !buffs.shinySeed && !buffs.seafoam;
|
||||
},
|
||||
getHairClass (type) {
|
||||
const hairPref = this.member.preferences.hair;
|
||||
|
||||
return `hair_${type}_${hairPref[type]}_${hairPref.color}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -499,7 +499,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
groupId () {
|
||||
// @TOOD: We might not need this since groupId is computed now
|
||||
// @TODO: We might not need this since groupId is computed now
|
||||
this.getMembers();
|
||||
},
|
||||
challengeId () {
|
||||
@@ -536,13 +536,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
sendMessage (member) {
|
||||
this.$root.$emit('habitica::new-inbox-message', {
|
||||
userIdToMessage: member._id,
|
||||
displayName: member.profile.name,
|
||||
username: member.auth.local.username,
|
||||
backer: member.backer,
|
||||
contributor: member.contributor,
|
||||
this.$store.dispatch('user:newPrivateMessageTo', {
|
||||
member,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||||
this.$router.push('/private-messages');
|
||||
},
|
||||
async searchMembers (searchTerm = '') {
|
||||
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div
|
||||
id="app-header"
|
||||
class="row"
|
||||
:class="{'hide-header': $route.name === 'groupPlan'}"
|
||||
:class="{'hide-header': hideHeader}"
|
||||
>
|
||||
<members-modal :hide-badge="true" />
|
||||
<member-details
|
||||
@@ -171,6 +171,9 @@ export default {
|
||||
sortedPartyMembers () {
|
||||
return orderBy(this.partyMembers, [this.user.party.order], [this.user.party.orderAscending]);
|
||||
},
|
||||
hideHeader () {
|
||||
return ['groupPlan', 'privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
if (this.user.party && this.user.party._id) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<inbox-modal />
|
||||
<creator-intro />
|
||||
<profileModal />
|
||||
<report-flag-modal />
|
||||
@@ -408,6 +407,7 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/utils.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.chevron {
|
||||
@@ -438,7 +438,7 @@
|
||||
}
|
||||
|
||||
.topbar {
|
||||
max-height: 56px;
|
||||
max-height: $menuToolbarHeight;
|
||||
|
||||
.currency-tray {
|
||||
margin-left: auto;
|
||||
@@ -721,7 +721,6 @@ import chevronDownIcon from '@/assets/svg/chevron-down.svg';
|
||||
import logo from '@/assets/svg/logo.svg';
|
||||
|
||||
import creatorIntro from '../creatorIntro';
|
||||
import InboxModal from '../userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import profileModal from '../userMenu/profileModal';
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
@@ -733,7 +732,6 @@ import userDropdown from './userDropdown';
|
||||
export default {
|
||||
components: {
|
||||
creatorIntro,
|
||||
InboxModal,
|
||||
notificationMenu,
|
||||
profileModal,
|
||||
reportFlagModal,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:can-remove="canRemove"
|
||||
:has-icon="true"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
:read-after-click="false"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'inbox-modal');
|
||||
this.$router.push('/private-messages');
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -139,7 +139,7 @@ import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
|
||||
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_INBOX_MESSAGE from './notifications/newPrivateMessage';
|
||||
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
|
||||
import WORLD_BOSS from './notifications/worldBoss';
|
||||
import VERIFY_USERNAME from './notifications/verifyUsername';
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<a
|
||||
class="nav-link dropdown-item
|
||||
dropdown-separated d-flex justify-content-between align-items-center"
|
||||
@click.prevent="showInbox()"
|
||||
@click.prevent="showPrivateMessages()"
|
||||
>
|
||||
<div>{{ $t('messages') }}</div>
|
||||
<message-count
|
||||
@@ -43,7 +43,7 @@
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="showAvatar('backgrounds', '2019')"
|
||||
@click="showAvatar('backgrounds', '2020')"
|
||||
>{{ $t('backgrounds') }}</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@@ -163,10 +163,15 @@ export default {
|
||||
this.$store.state.avatarEditorOptions.subpage = subpage;
|
||||
this.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||
},
|
||||
showInbox () {
|
||||
showPrivateMessages () {
|
||||
markPMSRead(this.user);
|
||||
axios.post('/api/v4/user/mark-pms-read');
|
||||
this.$root.$emit('bv::show::modal', 'inbox-modal');
|
||||
|
||||
if (this.$router.history.current.name === 'privateMessages') {
|
||||
this.$root.$emit('pm::refresh');
|
||||
} else {
|
||||
this.$router.push('/private-messages');
|
||||
}
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$router.push({ name: startingPage });
|
||||
|
||||
@@ -341,7 +341,7 @@ export default {
|
||||
const { firstRender } = viewOptions;
|
||||
const { itemsInFirstPosition } = viewOptions;
|
||||
|
||||
// Render selected items in first postion only for the first render
|
||||
// Render selected items in first position only for the first render
|
||||
if (itemsInFirstPosition.indexOf(ownedItem.key) !== -1 && firstRender === false) {
|
||||
gearItemsByType[type].unshift(ownedItem);
|
||||
} else if (isEquipped === true && firstRender === true) {
|
||||
@@ -382,7 +382,7 @@ export default {
|
||||
const { firstRender } = viewOptions;
|
||||
const { itemsInFirstPosition } = viewOptions;
|
||||
|
||||
// Render selected items in first postion only for the first render
|
||||
// Render selected items in first position only for the first render
|
||||
if (itemsInFirstPosition.indexOf(ownedItem.key) !== -1 && firstRender === false) {
|
||||
gearItemsByClass[klass].unshift(ownedItem);
|
||||
} else if (isEquipped === true && firstRender === true) {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversation"
|
||||
:class="{active: activeKey === uuid}"
|
||||
@click="$emit('click', {})"
|
||||
@mouseleave="hideDropDown()"
|
||||
>
|
||||
<div class="user">
|
||||
<user-label
|
||||
:backer="backer"
|
||||
:contributor="contributor"
|
||||
:name="displayName"
|
||||
/><span
|
||||
v-if="username"
|
||||
class="username"
|
||||
>@{{ username }}</span>
|
||||
<div
|
||||
v-if="lastMessageDate"
|
||||
class="time"
|
||||
>
|
||||
{{ lastMessageDate | timeAgo }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-row">
|
||||
<div class="messagePreview">
|
||||
{{ lastMessageText }}
|
||||
</div>
|
||||
<div
|
||||
v-if="userLoggedIn.id !== uuid"
|
||||
class="actions"
|
||||
>
|
||||
<b-dropdown
|
||||
ref="dropdown"
|
||||
class="action-dropdown"
|
||||
right
|
||||
toggle-class="btn-flat action-padding"
|
||||
no-caret
|
||||
variant="link"
|
||||
size="lg"
|
||||
>
|
||||
<template slot="button-content">
|
||||
<div
|
||||
class="svg-icon inline dots"
|
||||
v-html="icons.dots"
|
||||
></div>
|
||||
</template>
|
||||
<b-dropdown-item @click="block()">
|
||||
<span class="dropdown-icon-item">
|
||||
<div
|
||||
class="svg-icon inline"
|
||||
v-html="icons.remove"
|
||||
></div><span class="text">{{ $t('block') }}</span></span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import userLabel from '../userLabel';
|
||||
|
||||
import dots from '@/assets/svg/dots.svg';
|
||||
import remove from '@/assets/svg/remove.svg';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userLabel,
|
||||
},
|
||||
props: [
|
||||
'activeKey', 'uuid', 'backer', 'displayName',
|
||||
'username', 'contributor', 'lastMessageText',
|
||||
'lastMessageDate',
|
||||
],
|
||||
computed: {
|
||||
...mapState({
|
||||
userLoggedIn: 'user.data',
|
||||
}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
dots,
|
||||
remove,
|
||||
}),
|
||||
};
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideDropDown () {
|
||||
const { dropdown } = this.$refs;
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.hide();
|
||||
}
|
||||
},
|
||||
block () {
|
||||
this.$store.dispatch('user:block', {
|
||||
uuid: this.uuid,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.action-padding {
|
||||
height: 24px !important;
|
||||
width: 24px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.action-dropdown {
|
||||
.dropdown-item {
|
||||
padding: 12px 16px;
|
||||
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dots {
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
|
||||
svg path {
|
||||
fill: $purple-300
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.conversation {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #EEE;
|
||||
|
||||
.actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #f1edff;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 20px;
|
||||
|
||||
.user-label {
|
||||
flex: 1;
|
||||
flex-grow: 0;
|
||||
margin-right: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex: 2;
|
||||
text-align: end;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
//width: 100%;
|
||||
height: 30px;
|
||||
margin-right: 40px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-100;
|
||||
overflow: hidden;
|
||||
|
||||
// text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
|
||||
.messagePreview {
|
||||
flex: 1;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: none;
|
||||
width: 16px;
|
||||
margin-top: 4px;
|
||||
|
||||
.dots {
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<user-link
|
||||
:user-id="msg.uuid"
|
||||
:name="msg.user"
|
||||
:backer="msg.backer"
|
||||
:contributor="msg.contributor"
|
||||
/>
|
||||
<p class="time">
|
||||
<span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>@{{ msg.username }}</span><span
|
||||
v-if="msg.username"
|
||||
class="mr-1"
|
||||
>•</span>
|
||||
<span
|
||||
v-b-tooltip.hover="messageDate"
|
||||
>{{ msg.timestamp | timeAgo }} </span>
|
||||
<span v-if="msg.client && user.contributor.level >= 4"> ({{ msg.client }})</span>
|
||||
</p>
|
||||
<div
|
||||
class="text"
|
||||
v-html="atHighlight(parseMarkdown(msg.text))"
|
||||
></div>
|
||||
<div
|
||||
v-if="isMessageReported"
|
||||
class="reported"
|
||||
>
|
||||
<span v-once>{{ $t('reportedMessage') }}</span><br>
|
||||
<span v-once>{{ $t('canDeleteNow') }}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="msg.id"
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
v-if="!isMessageReported"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('report') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.delete"
|
||||
></div>
|
||||
<div v-once>
|
||||
{{ $t('delete') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
.at-text {
|
||||
color: #6133b4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
color: $gray-200;
|
||||
margin-right: 1em;
|
||||
font-size: 12px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
color: $gray-300;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: $gray-50;
|
||||
text-align: left !important;
|
||||
min-height: 0rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.reported {
|
||||
margin-top: 18px;
|
||||
color: $red-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import { mapState } from '@/libs/store';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
import reportIcon from '@/assets/svg/report.svg';
|
||||
import { highlightUsers } from '../../libs/highlightUsers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userLink,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
},
|
||||
props: {
|
||||
msg: {},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
delete: deleteIcon,
|
||||
report: reportIcon,
|
||||
}),
|
||||
reported: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
isMessageReported () {
|
||||
return (this.msg.flags && this.msg.flags[this.user.id]) || this.reported;
|
||||
},
|
||||
messageDate () {
|
||||
const date = moment(this.msg.timestamp).toDate();
|
||||
return date.toString();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$emit('message-card-mounted');
|
||||
},
|
||||
methods: {
|
||||
report () {
|
||||
this.$root.$on('habitica:report-result', data => {
|
||||
if (data.ok) {
|
||||
this.reported = true;
|
||||
}
|
||||
|
||||
this.$root.$off('habitica:report-result');
|
||||
});
|
||||
|
||||
this.$root.$emit('habitica::report-chat', {
|
||||
message: this.msg,
|
||||
groupId: 'privateMessage',
|
||||
});
|
||||
},
|
||||
async remove () {
|
||||
if (!window.confirm(this.$t('areYouSureDeleteMessage'))) return;
|
||||
|
||||
const message = this.msg;
|
||||
this.$emit('message-removed', message);
|
||||
|
||||
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
|
||||
},
|
||||
atHighlight (text) {
|
||||
return highlightUsers(text, this.user.auth.local.username, this.user.profile.name);
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<perfect-scrollbar
|
||||
ref="container"
|
||||
class="container-fluid"
|
||||
:class="{'disable-perfect-scroll': disablePerfectScroll}"
|
||||
:options="psOptions"
|
||||
>
|
||||
<div class="row loadmore">
|
||||
<div v-if="canLoadMore && !isLoading">
|
||||
<div class="loadmore-divider"></div>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="triggerLoad()"
|
||||
>
|
||||
{{ $t('loadEarlierMessages') }}
|
||||
</button>
|
||||
<div class="loadmore-divider"></div>
|
||||
</div>
|
||||
<h2
|
||||
v-show="isLoading"
|
||||
class="col-12 loading"
|
||||
>
|
||||
{{ $t('loading') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
v-for="(msg) in messages"
|
||||
:key="msg.id"
|
||||
class="row message-row"
|
||||
:class="{ 'margin-right': user._id !== msg.uuid}"
|
||||
>
|
||||
<div
|
||||
v-if="user._id !== msg.uuid"
|
||||
class="d-flex flex-grow-1"
|
||||
>
|
||||
<avatar
|
||||
v-if="msg.userStyles || (cachedProfileData[msg.uuid]
|
||||
&& !cachedProfileData[msg.uuid].rejected)"
|
||||
class="avatar-left"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]"
|
||||
:avatar-only="true"
|
||||
:override-top-padding="'14px'"
|
||||
:hide-class-badge="true"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
<div class="card card-right">
|
||||
<message-card
|
||||
:msg="msg"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="user._id === msg.uuid"
|
||||
class="d-flex flex-grow-1"
|
||||
>
|
||||
<div class="card card-left">
|
||||
<message-card
|
||||
:msg="msg"
|
||||
@message-removed="messageRemoved"
|
||||
@show-member-modal="showMemberModal"
|
||||
@message-card-mounted="itemWasMounted"
|
||||
/>
|
||||
</div>
|
||||
<avatar
|
||||
v-if="msg.userStyles
|
||||
|| (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)"
|
||||
class="avatar-right"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</perfect-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
|
||||
|
||||
.disable-perfect-scroll {
|
||||
overflow-y: inherit !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 15%;
|
||||
min-width: 8rem;
|
||||
height: 120px;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
.avatar-right {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0rem;
|
||||
width: 684px;
|
||||
}
|
||||
.message-row {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
|
||||
&:not(.margin-right) {
|
||||
.d-flex {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1400px) {
|
||||
.message-row {
|
||||
margin-left: -15px;
|
||||
margin-right: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-left {
|
||||
border: 1px solid $purple-500;
|
||||
}
|
||||
|
||||
.card-right {
|
||||
border: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.hr-middle {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-family: 'Roboto Condensed';
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $gray-200;
|
||||
background-color: $gray-700;
|
||||
padding: .2em;
|
||||
margin-top: .2em;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.loadmore {
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loadmore-divider {
|
||||
height: 1px;
|
||||
background-color: $gray-500;
|
||||
flex: 1;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { PerfectScrollbar } from 'vue2-perfect-scrollbar';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import messageCard from './messageCard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
messageCard,
|
||||
PerfectScrollbar,
|
||||
},
|
||||
props: {
|
||||
chat: {},
|
||||
isLoading: Boolean,
|
||||
canLoadMore: Boolean,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentDayDividerDisplay: moment().day(),
|
||||
cachedProfileData: {},
|
||||
currentProfileLoadedCount: 0,
|
||||
currentProfileLoadedEnd: 10,
|
||||
loading: false,
|
||||
handleScrollBack: false,
|
||||
lastOffset: -1,
|
||||
disablePerfectScroll: false,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.loadProfileCache();
|
||||
|
||||
this.$el.addEventListener('selectstart', () => this.handleSelectStart());
|
||||
this.$el.addEventListener('mouseup', () => this.handleSelectChange());
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$el.removeEventListener('selectstart', () => this.handleSelectStart());
|
||||
this.$el.removeEventListener('mouseup', () => this.handleSelectChange());
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
// @TODO: We need a different lazy load mechnism.
|
||||
// But honestly, adding a paging route to chat would solve this
|
||||
messages () {
|
||||
this.loadProfileCache();
|
||||
return this.chat;
|
||||
},
|
||||
psOptions () {
|
||||
return {
|
||||
suppressScrollX: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
this.loadProfileCache(window.scrollY / 1000);
|
||||
},
|
||||
async triggerLoad () {
|
||||
const container = this.$refs.container.$el;
|
||||
|
||||
// get current offset
|
||||
this.lastOffset = container.scrollTop - (container.scrollHeight - container.clientHeight);
|
||||
// disable scroll
|
||||
// container.style.overflowY = 'hidden';
|
||||
|
||||
const canLoadMore = !this.isLoading && this.canLoadMore;
|
||||
|
||||
if (canLoadMore) {
|
||||
const triggerLoadResult = this.$emit('triggerLoad');
|
||||
|
||||
await triggerLoadResult;
|
||||
|
||||
this.handleScrollBack = true;
|
||||
}
|
||||
},
|
||||
loadProfileCache: debounce(function loadProfileCache (screenPosition) {
|
||||
this._loadProfileCache(screenPosition);
|
||||
}, 1000),
|
||||
async _loadProfileCache (screenPosition) {
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
|
||||
const promises = [];
|
||||
const noProfilesLoaded = Object.keys(this.cachedProfileData).length === 0;
|
||||
|
||||
// @TODO: write an explination
|
||||
// @TODO: Remove this after enough messages are cached
|
||||
if (!noProfilesLoaded && screenPosition
|
||||
&& Math.floor(screenPosition) + 1 > this.currentProfileLoadedEnd / 10) {
|
||||
this.currentProfileLoadedEnd = 10 * (Math.floor(screenPosition) + 1);
|
||||
} else if (!noProfilesLoaded && screenPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aboutToCache = {};
|
||||
this.messages.forEach(message => {
|
||||
const { uuid } = message;
|
||||
|
||||
if (message.userStyles) {
|
||||
this.$set(this.cachedProfileData, uuid, message.userStyles);
|
||||
}
|
||||
|
||||
if (Boolean(uuid) && !this.cachedProfileData[uuid] && !aboutToCache[uuid]) {
|
||||
if (uuid === 'system' || this.currentProfileLoadedCount === this.currentProfileLoadedEnd) return;
|
||||
aboutToCache[uuid] = {};
|
||||
promises.push(axios.get(`/api/v4/members/${uuid}`));
|
||||
this.currentProfileLoadedCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
results.forEach(result => {
|
||||
// We could not load the user. Maybe they were deleted.
|
||||
// So, let's cache empty so we don't try again
|
||||
if (!result || !result.data || result.status >= 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = result.data.data;
|
||||
this.$set(this.cachedProfileData, userData._id, userData);
|
||||
});
|
||||
|
||||
// Merge in any attempts that were rejected so we don't attempt again
|
||||
for (const uuid in aboutToCache) {
|
||||
if (!this.cachedProfileData[uuid]) {
|
||||
this.$set(this.cachedProfileData, uuid, { rejected: true });
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
displayDivider (message) {
|
||||
if (this.currentDayDividerDisplay !== moment(message.timestamp).day()) {
|
||||
this.currentDayDividerDisplay = moment(message.timestamp).day();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async showMemberModal (memberId) {
|
||||
let profile = this.cachedProfileData[memberId];
|
||||
|
||||
if (!profile._id) {
|
||||
const result = await this.$store.dispatch('members:fetchMember', { memberId });
|
||||
if (result.response && result.response.status === 404) {
|
||||
return this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('messageDeletedUser'),
|
||||
type: 'error',
|
||||
timeout: false,
|
||||
});
|
||||
}
|
||||
this.cachedProfileData[memberId] = result.data.data;
|
||||
profile = result.data.data;
|
||||
}
|
||||
|
||||
// Open the modal only if the data is available
|
||||
if (profile && !profile.rejected) {
|
||||
this.$router.push({ name: 'userProfile', params: { userId: profile._id } });
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
itemWasMounted: debounce(function itemWasMounted () {
|
||||
if (this.handleScrollBack) {
|
||||
this.handleScrollBack = false;
|
||||
|
||||
const container = this.$refs.container.$el;
|
||||
const offset = container.scrollHeight - container.clientHeight;
|
||||
|
||||
const newOffset = offset + this.lastOffset;
|
||||
|
||||
container.scrollTo(0, newOffset);
|
||||
// enable scroll again
|
||||
// container.style.overflowY = 'scroll';
|
||||
}
|
||||
}, 50),
|
||||
messageRemoved (message) {
|
||||
this.$emit('message-removed', message);
|
||||
},
|
||||
handleSelectStart () {
|
||||
this.disablePerfectScroll = true;
|
||||
},
|
||||
handleSelectChange () {
|
||||
this.disablePerfectScroll = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -28,39 +28,38 @@
|
||||
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<table class="table"></table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<th>
|
||||
<span>{{ $t('email') }}</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>{{ $t('push') }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="notification in notificationsIds"
|
||||
:key="notification"
|
||||
>
|
||||
<td>
|
||||
<span>{{ $t(notification) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="user.preferences.emailNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('emailNotifications', notification)"
|
||||
>
|
||||
</td>
|
||||
<td v-if="onlyEmailsIds.indexOf(notification) === -1">
|
||||
<input
|
||||
v-model="user.preferences.pushNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('pushNotifications', notification)"
|
||||
>
|
||||
<hr>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<th>
|
||||
<span>{{ $t('email') }}</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>{{ $t('push') }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="notification in notificationsIds" :key="notification">
|
||||
<td>
|
||||
<span>{{ $t(notification) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="user.preferences.emailNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('emailNotifications', notification)"
|
||||
>
|
||||
</td>
|
||||
<td v-if="onlyEmailsIds.indexOf(notification) === -1">
|
||||
<input
|
||||
v-model="user.preferences.pushNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('pushNotifications', notification)"
|
||||
>
|
||||
<td v-else>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -74,6 +73,8 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
notificationsIds: Object.freeze([
|
||||
'majorUpdates',
|
||||
'onboarding',
|
||||
'newPM',
|
||||
'wonChallenge',
|
||||
'giftedGems',
|
||||
@@ -85,8 +86,6 @@ export default {
|
||||
'invitedQuest',
|
||||
'importantAnnouncements',
|
||||
'weeklyRecaps',
|
||||
'onboarding',
|
||||
'majorUpdates',
|
||||
'subscriptionReminders',
|
||||
]),
|
||||
// list of email-only notifications
|
||||
|
||||
@@ -812,7 +812,8 @@ export default {
|
||||
this.localAuth.username = this.user.auth.local.username;
|
||||
this.user.flags.verifiedUsername = true;
|
||||
} else if (attribute === 'email') {
|
||||
this.user.auth.local.email = updates[attribute];
|
||||
this.user.auth.local.email = updates.newEmail;
|
||||
window.alert(this.$t('emailSuccess'));
|
||||
}
|
||||
},
|
||||
async changeDisplayName (newName) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="task-modal"
|
||||
:no-close-on-esc="showTagsSelect"
|
||||
:no-close-on-backdrop="showTagsSelect"
|
||||
:no-close-on-esc="true"
|
||||
:no-close-on-backdrop="true"
|
||||
size="sm"
|
||||
@hidden="onClose()"
|
||||
@show="handleOpen()"
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
import { withKnobs, number } from '@storybook/addon-knobs';
|
||||
|
||||
import CountBadge from './countBadge.vue';
|
||||
|
||||
storiesOf('Count Badge', module)
|
||||
const stories = storiesOf('Count Badge', module);
|
||||
|
||||
stories.addDecorator(withKnobs);
|
||||
|
||||
stories
|
||||
.add('simple', () => ({
|
||||
components: { CountBadge },
|
||||
template: `
|
||||
@@ -19,9 +24,9 @@ storiesOf('Count Badge', module)
|
||||
<count-badge :count="count" :show="true"></count-badge>
|
||||
</div>
|
||||
`,
|
||||
data () {
|
||||
return {
|
||||
count: 3,
|
||||
};
|
||||
props: {
|
||||
count: {
|
||||
default: number('Count', 3),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="popover-box">
|
||||
<div
|
||||
:id="containerId"
|
||||
class="clearfix"
|
||||
class="clearfix toggle-switch-outer"
|
||||
>
|
||||
<div
|
||||
v-if="label"
|
||||
class="float-left toggle-switch-description"
|
||||
:class="hoverText ? 'hasPopOver' : ''"
|
||||
>
|
||||
{{ label }}
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
<div class="toggle-switch float-left">
|
||||
<input
|
||||
@@ -53,9 +53,7 @@
|
||||
}
|
||||
|
||||
.toggle-switch-description {
|
||||
height: 20px;
|
||||
|
||||
&.hasPopOver {
|
||||
&.hasPopOver span {
|
||||
border-bottom: 1px dashed $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="displayName"
|
||||
v-b-tooltip.hover.top="tierTitle"
|
||||
class="user-label"
|
||||
:class="levelStyle()"
|
||||
>
|
||||
{{ displayName }}
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="tierIcon()"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.user-label.no-tier {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementsLib from '@/../../common/script/libs/achievements';
|
||||
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
|
||||
import tier1 from '@/assets/svg/tier-1.svg';
|
||||
import tier2 from '@/assets/svg/tier-2.svg';
|
||||
import tier3 from '@/assets/svg/tier-3.svg';
|
||||
import tier4 from '@/assets/svg/tier-4.svg';
|
||||
import tier5 from '@/assets/svg/tier-5.svg';
|
||||
import tier6 from '@/assets/svg/tier-6.svg';
|
||||
import tier7 from '@/assets/svg/tier-7.svg';
|
||||
import tier8 from '@/assets/svg/tier-mod.svg';
|
||||
import tier9 from '@/assets/svg/tier-staff.svg';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
props: ['user', 'name', 'backer', 'contributor', 'hideTooltip'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayName () {
|
||||
if (this.name) {
|
||||
return this.name;
|
||||
}
|
||||
if (this.user && this.user.profile) {
|
||||
return this.user.profile.name;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
level () {
|
||||
if (this.contributor) {
|
||||
return this.contributor.level;
|
||||
} if (this.user && this.user.contributor) {
|
||||
return this.user.contributor.level;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
isNPC () {
|
||||
if (this.backer) {
|
||||
return this.backer.level;
|
||||
} if (this.user && this.user.backer) {
|
||||
return this.user.backer.level;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tierIcon () {
|
||||
if (this.isNPC) {
|
||||
return this.icons.tierNPC;
|
||||
}
|
||||
return this.icons[`tier${this.level}`];
|
||||
},
|
||||
tierTitle () {
|
||||
return this.hideTooltip ? '' : achievementsLib.getContribText(this.contributor, this.isNPC) || '';
|
||||
},
|
||||
levelStyle () {
|
||||
return this.userLevelStyleFromLevel(this.level, this.isNPC);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,698 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="inbox-modal"
|
||||
title
|
||||
:hide-footer="true"
|
||||
size="lg"
|
||||
@shown="onModalShown"
|
||||
@hide="onModalHide"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
class="header-wrap container align-items-center"
|
||||
>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-2">
|
||||
<div
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h2
|
||||
v-once
|
||||
class="text-center"
|
||||
>
|
||||
{{ $t('messages') }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 offset-3">
|
||||
<toggle-switch
|
||||
class="float-right"
|
||||
:label="optTextSet.switchDescription"
|
||||
:checked="!user.inbox.optOut"
|
||||
:hover-text="optTextSet.popoverText"
|
||||
@change="toggleOpt()"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<div class="close">
|
||||
<span
|
||||
class="svg-icon inline icon-10"
|
||||
aria-hidden="true"
|
||||
@click="close()"
|
||||
v-html="icons.svgClose"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 sidebar">
|
||||
<div class="search-section">
|
||||
<b-form-input
|
||||
v-model="search"
|
||||
:placeholder="$t('search')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="filtersConversations.length === 0"
|
||||
class="empty-messages text-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
<h4 v-once>
|
||||
{{ $t('emptyMessagesLine1') }}
|
||||
</h4>
|
||||
<p v-if="!user.flags.chatRevoked">
|
||||
{{ $t('emptyMessagesLine2') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="filtersConversations.length > 0"
|
||||
class="conversations"
|
||||
>
|
||||
<div
|
||||
v-for="conversation in filtersConversations"
|
||||
:key="conversation.key"
|
||||
class="conversation"
|
||||
:class="{active: selectedConversation.key === conversation.key}"
|
||||
@click="selectConversation(conversation.key)"
|
||||
>
|
||||
<div>
|
||||
<h3 :class="userLevelStyle(conversation)">
|
||||
{{ conversation.name }}
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="tierIcon(conversation)"
|
||||
></div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="time">
|
||||
<span
|
||||
v-if="conversation.username"
|
||||
class="mr-1"
|
||||
>@{{ conversation.username }} •</span>
|
||||
<span v-if="conversation.date">{{ conversation.date | timeAgo }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="messagePreview"
|
||||
>
|
||||
{{ conversation.lastMessageText
|
||||
? removeTags(parseMarkdown(conversation.lastMessageText)) : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-8 messages d-flex flex-column justify-content-between">
|
||||
<div
|
||||
v-if="!selectedConversation.key"
|
||||
class="empty-messages text-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
<h4>{{ placeholderTexts.title }}</h4>
|
||||
<p v-html="placeholderTexts.description"></p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedConversation && selectedConversationMessages.length === 0"
|
||||
class="empty-messages text-center"
|
||||
>
|
||||
<p>{{ $t('beginningOfConversation', {userName: selectedConversation.name}) }}</p>
|
||||
</div>
|
||||
<chat-messages
|
||||
v-if="selectedConversation && selectedConversationMessages.length > 0"
|
||||
ref="chatscroll"
|
||||
class="message-scroll"
|
||||
:chat="selectedConversationMessages"
|
||||
:inbox="true"
|
||||
:can-load-more="canLoadMore"
|
||||
:is-loading="messagesLoading"
|
||||
@message-removed="messageRemoved"
|
||||
@triggerLoad="infiniteScrollTrigger"
|
||||
/>
|
||||
<div
|
||||
v-if="user.inbox.optOut && selectedConversation.key"
|
||||
class="pm-disabled-caption text-center"
|
||||
>
|
||||
<h4>{{ $t('PMDisabledCaptionTitle') }}</h4>
|
||||
<p>{{ $t('PMDisabledCaptionText') }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedConversation.key && !user.flags.chatRevoked"
|
||||
class="new-message-row"
|
||||
>
|
||||
<textarea
|
||||
v-model="newMessage"
|
||||
maxlength="3000"
|
||||
@keyup.ctrl.enter="sendPrivateMessage()"
|
||||
></textarea>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="sendPrivateMessage()"
|
||||
>
|
||||
{{ $t('send') }}
|
||||
</button>
|
||||
<div class="row">
|
||||
<span class="ml-3">{{ currentLength }} / 3000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#inbox-modal .modal-body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.header-wrap {
|
||||
padding: 0.5em;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0rem;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.envelope {
|
||||
color: $gray-400 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-700;
|
||||
min-height: 540px;
|
||||
padding: 0;
|
||||
|
||||
.search-section {
|
||||
padding: 1em;
|
||||
box-shadow: 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.messages {
|
||||
position: relative;
|
||||
padding-left: 0;
|
||||
padding-bottom: 6em;
|
||||
height: 540px;
|
||||
}
|
||||
|
||||
.message-scroll {
|
||||
max-height: 500px;
|
||||
overflow-x: scroll;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.to-form input {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
margin-top: 10em;
|
||||
color: $gray-400;
|
||||
padding: 1em;
|
||||
|
||||
h4 {
|
||||
color: $gray-400;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.envelope {
|
||||
width: 30px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.pm-disabled-caption {
|
||||
|
||||
padding-top: 1em;
|
||||
background-color: $gray-700;
|
||||
z-index: 2;
|
||||
|
||||
h4, p {
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.new-message-row {
|
||||
background-color: $gray-700;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 88px;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
|
||||
textarea {
|
||||
height: 80%;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
button {
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
box-shadow: none;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.conversations {
|
||||
max-height: 400px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
padding: 1.5em;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.conversation.active {
|
||||
border: 1px solid $purple-400;
|
||||
}
|
||||
|
||||
.conversation:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import axios from 'axios';
|
||||
import { mapState } from '@/libs/store';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import messageIcon from '@/assets/svg/message.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import tier1 from '@/assets/svg/tier-1.svg';
|
||||
import tier2 from '@/assets/svg/tier-2.svg';
|
||||
import tier3 from '@/assets/svg/tier-3.svg';
|
||||
import tier4 from '@/assets/svg/tier-4.svg';
|
||||
import tier5 from '@/assets/svg/tier-5.svg';
|
||||
import tier6 from '@/assets/svg/tier-6.svg';
|
||||
import tier7 from '@/assets/svg/tier-7.svg';
|
||||
import tier8 from '@/assets/svg/tier-mod.svg';
|
||||
import tier9 from '@/assets/svg/tier-staff.svg';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
chatMessages,
|
||||
toggleSwitch,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(new Date(value)).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
messageIcon,
|
||||
svgClose,
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
}),
|
||||
displayCreate: true,
|
||||
selectedConversation: {},
|
||||
search: '',
|
||||
newMessage: '',
|
||||
showPopover: false,
|
||||
messages: [],
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
loadedConversations: [],
|
||||
loaded: false,
|
||||
messagesLoading: false,
|
||||
initiatedConversation: null,
|
||||
updateConversionsCounter: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
canLoadMore () {
|
||||
return this.selectedConversation && this.selectedConversation.canLoadMore;
|
||||
},
|
||||
conversations () {
|
||||
const inboxGroup = groupBy(this.loadedConversations, 'uuid');
|
||||
|
||||
// Add placeholder for new conversations
|
||||
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
||||
inboxGroup[this.initiatedConversation.uuid] = [{
|
||||
uuid: this.initiatedConversation.uuid,
|
||||
user: this.initiatedConversation.user,
|
||||
username: this.initiatedConversation.username,
|
||||
contributor: this.initiatedConversation.contributor,
|
||||
id: '',
|
||||
text: '',
|
||||
timestamp: new Date(),
|
||||
}];
|
||||
}
|
||||
// Create conversation objects
|
||||
const convos = [];
|
||||
for (const key of Object.keys(inboxGroup)) {
|
||||
const recentMessage = inboxGroup[key][0];
|
||||
|
||||
const convoModel = {
|
||||
key: recentMessage.uuid,
|
||||
// Handles case where from user sent
|
||||
// the only message or the to user sent the only message
|
||||
name: recentMessage.user,
|
||||
username: !recentMessage.text ? recentMessage.username : recentMessage.toUserName,
|
||||
date: recentMessage.timestamp,
|
||||
lastMessageText: recentMessage.text,
|
||||
canLoadMore: true,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
convos.push(convoModel);
|
||||
}
|
||||
|
||||
return convos;
|
||||
},
|
||||
// Separate from selectedConversation which
|
||||
// is not computed so messages don't update automatically
|
||||
selectedConversationMessages () {
|
||||
// Vue-subscribe to changes
|
||||
const subScribeToUpdate = this.messagesLoading || this.updateConversionsCounter > -1;
|
||||
|
||||
const selectedConversationKey = this.selectedConversation.key;
|
||||
const selectedConversation = this.messagesByConversation[selectedConversationKey];
|
||||
this.messages = selectedConversation || []; // eslint-disable-line vue/no-side-effects-in-computed-properties, max-len
|
||||
|
||||
const ordered = orderBy(this.messages, [m => m.timestamp], ['asc']);
|
||||
|
||||
if (subScribeToUpdate) {
|
||||
return ordered;
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
filtersConversations () {
|
||||
// Vue-subscribe to changes
|
||||
const subScribeToUpdate = this.updateConversionsCounter > -1;
|
||||
|
||||
const filtered = subScribeToUpdate && !this.search
|
||||
? this.conversations
|
||||
: filter(
|
||||
this.conversations,
|
||||
conversation => conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1,
|
||||
);
|
||||
|
||||
const ordered = orderBy(filtered, [o => moment(o.date).toDate()], ['desc']);
|
||||
|
||||
return ordered;
|
||||
},
|
||||
currentLength () {
|
||||
return this.newMessage.length;
|
||||
},
|
||||
placeholderTexts () {
|
||||
if (this.user.flags.chatRevoked) {
|
||||
return {
|
||||
title: this.$t('PMPlaceholderTitleRevoked'),
|
||||
description: this.$t('chatPrivilegesRevoked'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: this.$t('PMPlaceholderTitle'),
|
||||
description: this.$t('PMPlaceholderDescription'),
|
||||
};
|
||||
},
|
||||
optTextSet () {
|
||||
if (!this.user.inbox.optOut) {
|
||||
return {
|
||||
switchDescription: this.$t('PMReceive'),
|
||||
popoverText: this.$t('PMEnabledOptPopoverText'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
switchDescription: this.$t('PMReceive'),
|
||||
popoverText: this.$t('PMDisabledOptPopoverText'),
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::new-inbox-message', data => {
|
||||
this.$root.$emit('bv::show::modal', 'inbox-modal');
|
||||
|
||||
// Wait for messages to be loaded
|
||||
const unwatchLoaded = this.$watch('loaded', loaded => {
|
||||
if (!loaded) return;
|
||||
|
||||
const conversation = this.conversations.find(convo => convo.key === data.userIdToMessage);
|
||||
if (loaded) setImmediate(() => unwatchLoaded());
|
||||
|
||||
if (conversation) {
|
||||
this.selectConversation(data.userIdToMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initiatedConversation = {
|
||||
uuid: data.userIdToMessage,
|
||||
user: data.displayName,
|
||||
username: data.username,
|
||||
backer: data.backer,
|
||||
contributor: data.contributor,
|
||||
};
|
||||
|
||||
this.selectConversation(data.userIdToMessage);
|
||||
}, { immediate: true });
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica::new-inbox-message');
|
||||
},
|
||||
methods: {
|
||||
async onModalShown () {
|
||||
this.loaded = false;
|
||||
|
||||
const conversationRes = await axios.get('/api/v4/inbox/conversations');
|
||||
this.loadedConversations = conversationRes.data.data;
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
onModalHide () {
|
||||
// reset everything
|
||||
this.loadedConversations = [];
|
||||
this.loaded = false;
|
||||
this.initiatedConversation = null;
|
||||
this.messagesByConversation = {};
|
||||
this.selectedConversation = {};
|
||||
},
|
||||
messageRemoved (message) {
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
const messageIndex = messages.findIndex(msg => msg.id === message.id);
|
||||
if (messageIndex !== -1) messages.splice(messageIndex, 1);
|
||||
if (this.selectedConversationMessages.length === 0) {
|
||||
this.initiatedConversation = {
|
||||
uuid: this.selectedConversation.key,
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
backer: this.selectedConversation.backer,
|
||||
contributor: this.selectedConversation.contributor,
|
||||
};
|
||||
}
|
||||
},
|
||||
toggleClick () {
|
||||
this.displayCreate = !this.displayCreate;
|
||||
},
|
||||
toggleOpt () {
|
||||
this.$store.dispatch('user:togglePrivateMessagesOpt');
|
||||
},
|
||||
async selectConversation (key) {
|
||||
const convoFound = this.conversations.find(conversation => conversation.key === key);
|
||||
|
||||
this.selectedConversation = convoFound || {};
|
||||
|
||||
if (!this.messagesByConversation[this.selectedConversation.key]) {
|
||||
await this.loadMessages();
|
||||
}
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
},
|
||||
sendPrivateMessage () {
|
||||
if (!this.newMessage) return;
|
||||
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
messages.push({
|
||||
sent: true,
|
||||
text: this.newMessage,
|
||||
timestamp: new Date(),
|
||||
toUser: this.selectedConversation.name,
|
||||
toUserName: this.selectedConversation.username,
|
||||
toUserContributor: this.selectedConversation.contributor,
|
||||
toUserBacker: this.selectedConversation.backer,
|
||||
toUUID: this.selectedConversation.uuid,
|
||||
|
||||
id: '-1', // will be updated once the result is back
|
||||
likes: {},
|
||||
ownerId: this.user._id,
|
||||
uuid: this.user._id,
|
||||
fromUUID: this.user._id,
|
||||
user: this.user.profile.name,
|
||||
username: this.user.auth.local.username,
|
||||
contributor: this.user.contributor,
|
||||
backer: this.user.backer,
|
||||
});
|
||||
|
||||
// Remove the placeholder message
|
||||
if (
|
||||
this.initiatedConversation
|
||||
&& this.initiatedConversation.uuid === this.selectedConversation.key
|
||||
) {
|
||||
this.loadedConversations.unshift(this.initiatedConversation);
|
||||
this.initiatedConversation = null;
|
||||
}
|
||||
|
||||
this.selectedConversation.lastMessageText = this.newMessage;
|
||||
this.selectedConversation.date = new Date();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
|
||||
this.$store.dispatch('members:sendPrivateMessage', {
|
||||
toUserId: this.selectedConversation.key,
|
||||
message: this.newMessage,
|
||||
}).then(response => {
|
||||
const newMessage = response.data.data.message;
|
||||
const messageToReset = messages[messages.length - 1];
|
||||
messageToReset.id = newMessage.id; // just set the id, all other infos already set
|
||||
Object.assign(messages[messages.length - 1], messageToReset);
|
||||
this.updateConversionsCounter += 1;
|
||||
});
|
||||
|
||||
this.newMessage = '';
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'inbox-modal');
|
||||
},
|
||||
tierIcon (message) {
|
||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||
if (isNPC) {
|
||||
return this.icons.tierNPC;
|
||||
}
|
||||
if (!message.contributor) return null;
|
||||
return this.icons[`tier${message.contributor.level}`];
|
||||
},
|
||||
removeTags (html) {
|
||||
const tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
if (this.canLoadMore) {
|
||||
this.messagesLoading = true;
|
||||
}
|
||||
|
||||
return this.loadMore();
|
||||
},
|
||||
loadMore () {
|
||||
this.selectedConversation.page += 1;
|
||||
return this.loadMessages();
|
||||
},
|
||||
async loadMessages () {
|
||||
this.messagesLoading = true;
|
||||
|
||||
const requestUrl = `/api/v4/inbox/paged-messages?conversation=${this.selectedConversation.key}&page=${this.selectedConversation.page}`;
|
||||
const res = await axios.get(requestUrl);
|
||||
const loadedMessages = res.data.data;
|
||||
|
||||
this.messagesByConversation[this.selectedConversation.key] = this.messagesByConversation[this.selectedConversation.key] || []; // eslint-disable-line max-len
|
||||
const loadedMessagesToAdd = loadedMessages
|
||||
.filter(m => this.messagesByConversation[this.selectedConversation.key].findIndex(mI => mI.id === m.id) === -1); // eslint-disable-line max-len
|
||||
this.messagesByConversation[this.selectedConversation.key].push(...loadedMessagesToAdd);
|
||||
|
||||
// only show the load more Button if the max count was returned
|
||||
this.selectedConversation.canLoadMore = loadedMessages.length === 10;
|
||||
this.messagesLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -859,13 +859,12 @@ export default {
|
||||
window.history.replaceState(null, null, '');
|
||||
},
|
||||
sendMessage () {
|
||||
this.$root.$emit('habitica::new-inbox-message', {
|
||||
userIdToMessage: this.user._id,
|
||||
displayName: this.user.profile.name,
|
||||
username: this.user.auth.local.username,
|
||||
backer: this.user.backer,
|
||||
contributor: this.user.contributor,
|
||||
this.$store.dispatch('user:newPrivateMessageTo', {
|
||||
member: this.user,
|
||||
});
|
||||
|
||||
this.$router.push('/private-messages');
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
},
|
||||
getProgressDisplay () {
|
||||
// let currentLoginDay = Content.loginIncentives[this.user.loginIncentives];
|
||||
|
||||
@@ -29,7 +29,7 @@ export function setUpLogging () { // eslint-disable-line import/prefer-default-e
|
||||
Vue.config.errorHandler = (err, vm, info) => {
|
||||
console.error('Unhandled error in Vue.js code.');
|
||||
console.error('Error:', err);
|
||||
console.error('Component where it occured:', vm);
|
||||
console.error('Component where it occurred:', vm);
|
||||
console.error('Info:', info);
|
||||
|
||||
_LTracker.push({
|
||||
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
}
|
||||
|
||||
return null;
|
||||
// @TOOD: User.sync();
|
||||
// @TODO: User.sync();
|
||||
},
|
||||
castCancel () {
|
||||
this.potionClickMode = false;
|
||||
|
||||
@@ -0,0 +1,867 @@
|
||||
<template>
|
||||
<div id="private-message">
|
||||
<div class="floating-header-shadow"></div>
|
||||
<div class="header-bar d-flex w-100">
|
||||
<!-- changing w-25 would also need changes in .left-header.w-25 -->
|
||||
<div class="d-flex w-25 left-header">
|
||||
<div
|
||||
v-once
|
||||
class="mail-icon svg-icon"
|
||||
v-html="icons.mail"
|
||||
></div>
|
||||
<h2
|
||||
v-once
|
||||
class="flex-fill text-center mail-icon-label"
|
||||
>
|
||||
{{ $t('messages') }}
|
||||
</h2>
|
||||
<div class="placeholder svg-icon">
|
||||
<!-- placeholder -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex w-75 selected-conversion">
|
||||
<face-avatar
|
||||
v-if="selectedConversation.userStyles"
|
||||
:member="selectedConversation.userStyles"
|
||||
:class="selectedConversationFaceAvatarClass"
|
||||
/>
|
||||
<user-label
|
||||
:backer="selectedConversation.backer"
|
||||
:contributor="selectedConversation.contributor"
|
||||
:name="selectedConversation.name"
|
||||
:hide-tooltip="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex content">
|
||||
<div class="w-25 sidebar d-flex flex-column">
|
||||
<div class="disable-background">
|
||||
<toggle-switch
|
||||
:label="optTextSet.switchDescription"
|
||||
:checked="this.user.inbox.optOut"
|
||||
:hover-text="optTextSet.popoverText"
|
||||
@change="toggleOpt()"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-section">
|
||||
<b-form-input
|
||||
v-model="search"
|
||||
class="input-search"
|
||||
:placeholder="$t('search')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="filtersConversations.length === 0"
|
||||
class="empty-messages m-auto text-center empty-sidebar"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
<h4 v-once>
|
||||
{{ $t('emptyMessagesLine1') }}
|
||||
</h4>
|
||||
<p v-if="!user.flags.chatRevoked">
|
||||
{{ $t('emptyMessagesLine2') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="filtersConversations.length > 0"
|
||||
class="conversations"
|
||||
>
|
||||
<conversation-item
|
||||
v-for="conversation in filtersConversations"
|
||||
:key="conversation.key"
|
||||
:active-key="selectedConversation.key"
|
||||
:contributor="conversation.contributor"
|
||||
:backer="conversation.backer"
|
||||
:uuid="conversation.key"
|
||||
:display-name="conversation.name"
|
||||
:username="conversation.username"
|
||||
:last-message-date="conversation.date"
|
||||
:last-message-text="conversation.lastMessageText
|
||||
? removeTags(parseMarkdown(conversation.lastMessageText)) : ''"
|
||||
@click="selectConversation(conversation.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-75 messages-column d-flex flex-column align-items-center">
|
||||
<div
|
||||
v-if="!selectedConversation.key"
|
||||
class="empty-messages full-height m-auto text-center"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon envelope"
|
||||
v-html="icons.messageIcon"
|
||||
></div>
|
||||
<h4>{{ placeholderTexts.title }}</h4>
|
||||
<p v-html="placeholderTexts.description"></p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedConversation.key && selectedConversationMessages.length === 0"
|
||||
class="empty-messages full-height mt-auto text-center"
|
||||
>
|
||||
<avatar
|
||||
v-if="selectedConversation.userStyles"
|
||||
:member="selectedConversation.userStyles"
|
||||
:avatar-only="true"
|
||||
sprites-margin="0 0 0 -45px"
|
||||
class="center-avatar"
|
||||
/>
|
||||
<h3>{{ $t('beginningOfConversation', {userName: selectedConversation.name}) }}</h3>
|
||||
<p>{{ $t('beginningOfConversationReminder') }}</p>
|
||||
</div>
|
||||
<messageList
|
||||
v-if="selectedConversation && selectedConversationMessages.length > 0"
|
||||
ref="chatscroll"
|
||||
class="message-scroll"
|
||||
:chat="selectedConversationMessages"
|
||||
:can-load-more="canLoadMore"
|
||||
:is-loading="messagesLoading"
|
||||
@message-removed="messageRemoved"
|
||||
@triggerLoad="infiniteScrollTrigger"
|
||||
/>
|
||||
<div
|
||||
v-if="user.inbox.optOut"
|
||||
class="pm-disabled-caption text-center"
|
||||
>
|
||||
<h4>{{ $t('PMDisabledCaptionTitle') }}</h4>
|
||||
<p>{{ $t('PMDisabledCaptionText') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="selectedConversation.key && !user.flags.chatRevoked"
|
||||
class="new-message-row d-flex align-items-center"
|
||||
>
|
||||
<textarea
|
||||
v-model="newMessage"
|
||||
class="flex-fill"
|
||||
:placeholder="$t('needsTextPlaceholder')"
|
||||
maxlength="3000"
|
||||
:class="{'has-content': newMessage !== ''}"
|
||||
@keyup.ctrl.enter="sendPrivateMessage()"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedConversation.key && !user.flags.chatRevoked"
|
||||
class="sub-new-message-row d-flex"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="guidelines flex-fill"
|
||||
v-html="$t('communityGuidelinesIntro')"
|
||||
></div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:class="{'disabled':newMessage === ''}"
|
||||
@click="sendPrivateMessage()"
|
||||
>
|
||||
{{ $t('send') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
$pmHeaderHeight: 56px;
|
||||
|
||||
// Content of Private Message should be always full-size (minus the toolbar/resting banner)
|
||||
|
||||
#private-message {
|
||||
height: calc(100vh - #{$menuToolbarHeight} -
|
||||
var(--banner-gifting-height, 0px) -
|
||||
var(--banner-resting-height, 0px)); // css variable magic :), must be 0px, 0 alone won't work
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: calc(100vh - #{$menuToolbarHeight} - #{$pmHeaderHeight} -
|
||||
var(--banner-gifting-height, 0px) -
|
||||
var(--banner-resting-height, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
.disable-background {
|
||||
.toggle-switch-description {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toggle-switch-outer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 66vw;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 10vh 15vw 0rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1rem 0rem;
|
||||
|
||||
.close {
|
||||
cursor: pointer;
|
||||
margin: 0rem 1.5rem;
|
||||
min-width: 0.75rem;
|
||||
padding: 0rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch-description {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.43;
|
||||
letter-spacing: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
$pmHeaderHeight: 56px;
|
||||
$background: $white;
|
||||
|
||||
.header-bar {
|
||||
height: 56px;
|
||||
background-color: $white;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
align-items: center;
|
||||
|
||||
.mail-icon {
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mail-icon-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.placeholder.svg-icon {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.left-header.w-25 {
|
||||
width: calc(25% - 2rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.input-search {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left 16px;
|
||||
background-size: 16px 16px;
|
||||
background-image: url(~@/assets/svg/for-css/search_gray.svg) !important;
|
||||
padding-left: 40px;
|
||||
|
||||
color: $gray-200 !important;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.selected-conversion {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#private-message {
|
||||
background-color: $background;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.disable-background {
|
||||
height: 44px;
|
||||
background-color: $gray-600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.conversations {
|
||||
max-height: 35rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-messages {
|
||||
h3, h4, p {
|
||||
color: $gray-400;
|
||||
margin: 0rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.envelope {
|
||||
width: 30px;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.envelope {
|
||||
color: $gray-500 !important;
|
||||
margin: 0rem;
|
||||
max-width: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0rem;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
padding: 0.5em;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.messages-column {
|
||||
padding: 0rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.empty-messages, .message-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-scroll {
|
||||
overflow-x: hidden;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.new-message-row {
|
||||
width: 100%;
|
||||
padding-left: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
|
||||
textarea {
|
||||
height: 5.5rem;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
border-radius: 2px;
|
||||
z-index: 5;
|
||||
border: solid 1px $gray-400;
|
||||
opacity: 0.64;
|
||||
background-color: $gray-500;
|
||||
|
||||
&:focus, &.has-content {
|
||||
opacity: 1;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-new-message-row {
|
||||
padding: 1rem 1.5rem 1.5rem;
|
||||
|
||||
.guidelines {
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-200;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
margin-left: 1.5rem;
|
||||
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.64;
|
||||
background-color: $gray-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pm-disabled-caption {
|
||||
padding-top: 1em;
|
||||
background-color: $gray-700;
|
||||
z-index: 2;
|
||||
|
||||
h4, p {
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-700;
|
||||
min-height: 540px;
|
||||
max-width: 330px;
|
||||
padding: 0;
|
||||
border-bottom-left-radius: 8px;
|
||||
|
||||
.search-section {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.to-form input {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.empty-sidebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.floating-message-input {
|
||||
background: $background;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.floating-header-shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
box-shadow: 0 3px 12px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.center-avatar {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import axios from 'axios';
|
||||
import { mapState } from '@/libs/store';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import userLabel from '@/components/userLabel';
|
||||
|
||||
import messageList from '@/components/messages/messageList';
|
||||
import messageIcon from '@/assets/svg/message.svg';
|
||||
import mail from '@/assets/svg/mail.svg';
|
||||
import conversationItem from '@/components/messages/conversationItem';
|
||||
import faceAvatar from '@/components/faceAvatar';
|
||||
import Avatar from '@/components/avatar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
messageList,
|
||||
toggleSwitch,
|
||||
conversationItem,
|
||||
userLabel,
|
||||
faceAvatar,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(new Date(value)).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
messageIcon,
|
||||
mail,
|
||||
}),
|
||||
displayCreate: true,
|
||||
selectedConversation: {},
|
||||
search: '',
|
||||
newMessage: '',
|
||||
showPopover: false,
|
||||
messages: [],
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
loadedConversations: [],
|
||||
loaded: false,
|
||||
messagesLoading: false,
|
||||
initiatedConversation: null,
|
||||
updateConversationsCounter: 0,
|
||||
};
|
||||
},
|
||||
async mounted () {
|
||||
this.$root.$on('pm::refresh', async () => {
|
||||
await this.reload();
|
||||
|
||||
this.selectConversation(this.loadedConversations[0].uuid, true);
|
||||
});
|
||||
|
||||
await this.reload();
|
||||
|
||||
const data = this.$store.state.privateMessageOptions;
|
||||
|
||||
if (data && data.userIdToMessage) {
|
||||
this.initiatedConversation = {
|
||||
uuid: data.userIdToMessage,
|
||||
user: data.displayName,
|
||||
username: data.username,
|
||||
backer: data.backer,
|
||||
contributor: data.contributor,
|
||||
userStyles: data.userStyles,
|
||||
};
|
||||
|
||||
this.$store.state.privateMessageOptions = {};
|
||||
|
||||
this.selectConversation(this.initiatedConversation.uuid);
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica::new-private-message');
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
canLoadMore () {
|
||||
return this.selectedConversation && this.selectedConversation.canLoadMore;
|
||||
},
|
||||
conversations () {
|
||||
const inboxGroup = groupBy(this.loadedConversations, 'uuid');
|
||||
|
||||
// Add placeholder for new conversations
|
||||
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
||||
inboxGroup[this.initiatedConversation.uuid] = [{
|
||||
uuid: this.initiatedConversation.uuid,
|
||||
user: this.initiatedConversation.user,
|
||||
username: this.initiatedConversation.username,
|
||||
contributor: this.initiatedConversation.contributor,
|
||||
backer: this.initiatedConversation.backer,
|
||||
userStyles: this.initiatedConversation.userStyles,
|
||||
id: '',
|
||||
text: '',
|
||||
timestamp: new Date(),
|
||||
}];
|
||||
}
|
||||
// Create conversation objects
|
||||
const convos = [];
|
||||
for (const key in inboxGroup) {
|
||||
if (Object.prototype.hasOwnProperty.call(inboxGroup, key)) {
|
||||
const recentMessage = inboxGroup[key][0];
|
||||
|
||||
const convoModel = {
|
||||
key: recentMessage.uuid,
|
||||
name: recentMessage.user,
|
||||
// Handles case where from user sent the only message
|
||||
// or the to user sent the only message
|
||||
username: recentMessage.username,
|
||||
date: recentMessage.timestamp,
|
||||
lastMessageText: recentMessage.text,
|
||||
contributor: recentMessage.contributor,
|
||||
userStyles: recentMessage.userStyles,
|
||||
backer: recentMessage.backer,
|
||||
canLoadMore: false,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
convos.push(convoModel);
|
||||
}
|
||||
}
|
||||
|
||||
return convos;
|
||||
},
|
||||
// Separate from selectedConversation which is not computed
|
||||
// so messages don't update automatically
|
||||
/* eslint-disable vue/no-side-effects-in-computed-properties */
|
||||
selectedConversationMessages () {
|
||||
// Vue-subscribe to changes
|
||||
const subscribeToUpdate = this.messagesLoading || this.updateConversationsCounter > -1;
|
||||
|
||||
const selectedConversationKey = this.selectedConversation.key;
|
||||
const selectedConversation = this.messagesByConversation[selectedConversationKey];
|
||||
this.messages = selectedConversation || [];
|
||||
|
||||
const ordered = orderBy(this.messages, [m => m.timestamp], ['asc']);
|
||||
|
||||
if (subscribeToUpdate) {
|
||||
return ordered;
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
filtersConversations () {
|
||||
// Vue-subscribe to changes
|
||||
const subscribeToUpdate = this.updateConversationsCounter > -1;
|
||||
|
||||
const filtered = subscribeToUpdate && !this.search
|
||||
? this.conversations
|
||||
|
||||
/* eslint-disable max-len */
|
||||
: filter(this.conversations, conversation => conversation.name.toLowerCase().indexOf(this.search.toLowerCase()) !== -1);
|
||||
|
||||
const ordered = orderBy(filtered, [o => moment(o.date).toDate()], ['desc']);
|
||||
|
||||
return ordered;
|
||||
},
|
||||
currentLength () {
|
||||
return this.newMessage.length;
|
||||
},
|
||||
placeholderTexts () {
|
||||
if (this.user.flags.chatRevoked) {
|
||||
return {
|
||||
title: this.$t('PMPlaceholderTitleRevoked'),
|
||||
description: this.$t('chatPrivilegesRevoked'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: this.$t('PMPlaceholderTitle'),
|
||||
description: this.$t('PMPlaceholderDescription'),
|
||||
};
|
||||
},
|
||||
optTextSet () {
|
||||
if (!this.user.inbox.optOut) {
|
||||
return {
|
||||
switchDescription: this.$t('PMDisabled'),
|
||||
popoverText: this.$t('PMEnabledOptPopoverText'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
switchDescription: this.$t('PMDisabled'),
|
||||
popoverText: this.$t('PMDisabledOptPopoverText'),
|
||||
};
|
||||
},
|
||||
selectedConversationFaceAvatarClass () {
|
||||
if (this.selectedConversation && this.selectedConversation.contributor) {
|
||||
return `tier${this.selectedConversation.contributor.level}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async reload () {
|
||||
this.loaded = false;
|
||||
|
||||
const conversationRes = await axios.get('/api/v4/inbox/conversations');
|
||||
this.loadedConversations = conversationRes.data.data;
|
||||
this.selectedConversation = {};
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
messageRemoved (message) {
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
const messageIndex = messages.findIndex(msg => msg.id === message.id);
|
||||
if (messageIndex !== -1) messages.splice(messageIndex, 1);
|
||||
if (this.selectedConversationMessages.length === 0) {
|
||||
this.initiatedConversation = {
|
||||
uuid: this.selectedConversation.key,
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
backer: this.selectedConversation.backer,
|
||||
contributor: this.selectedConversation.contributor,
|
||||
};
|
||||
}
|
||||
},
|
||||
toggleClick () {
|
||||
this.displayCreate = !this.displayCreate;
|
||||
},
|
||||
toggleOpt () {
|
||||
this.$store.dispatch('user:togglePrivateMessagesOpt');
|
||||
},
|
||||
async selectConversation (key, forceLoadMessage = false) {
|
||||
const convoFound = this.conversations.find(conversation => conversation.key === key);
|
||||
|
||||
this.selectedConversation = convoFound || {};
|
||||
|
||||
if (!this.messagesByConversation[this.selectedConversation.key] || forceLoadMessage) {
|
||||
await this.loadMessages();
|
||||
}
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
},
|
||||
sendPrivateMessage () {
|
||||
if (!this.newMessage) return;
|
||||
|
||||
const messages = this.messagesByConversation[this.selectedConversation.key];
|
||||
|
||||
messages.push({
|
||||
sent: true,
|
||||
text: this.newMessage,
|
||||
timestamp: new Date(),
|
||||
toUser: this.selectedConversation.name,
|
||||
toUserName: this.selectedConversation.username,
|
||||
toUserContributor: this.selectedConversation.contributor,
|
||||
toUserBacker: this.selectedConversation.backer,
|
||||
toUUID: this.selectedConversation.uuid,
|
||||
|
||||
id: '-1', // will be updated once the result is back
|
||||
likes: {},
|
||||
ownerId: this.user._id,
|
||||
uuid: this.user._id,
|
||||
fromUUID: this.user._id,
|
||||
user: this.user.profile.name,
|
||||
username: this.user.auth.local.username,
|
||||
contributor: this.user.contributor,
|
||||
backer: this.user.backer,
|
||||
});
|
||||
|
||||
// Remove the placeholder message
|
||||
if (this.initiatedConversation
|
||||
&& this.initiatedConversation.uuid === this.selectedConversation.key) {
|
||||
this.loadedConversations.unshift(this.initiatedConversation);
|
||||
this.initiatedConversation = null;
|
||||
}
|
||||
|
||||
this.selectedConversation.lastMessageText = this.newMessage;
|
||||
this.selectedConversation.date = new Date();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
|
||||
this.$store.dispatch('members:sendPrivateMessage', {
|
||||
toUserId: this.selectedConversation.key,
|
||||
message: this.newMessage,
|
||||
}).then(response => {
|
||||
const newMessage = response.data.data.message;
|
||||
const messageToReset = messages[messages.length - 1];
|
||||
messageToReset.id = newMessage.id; // just set the id, all other infos already set
|
||||
Object.assign(messages[messages.length - 1], messageToReset);
|
||||
this.updateConversationsCounter += 1;
|
||||
});
|
||||
|
||||
this.newMessage = '';
|
||||
},
|
||||
removeTags (html) {
|
||||
const tmp = document.createElement('DIV');
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || '';
|
||||
},
|
||||
parseMarkdown (text) {
|
||||
if (!text) return null;
|
||||
return habiticaMarkdown.render(String(text));
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
// show loading and wait until the loadMore debounced
|
||||
// or else it would trigger on every scrolling-pixel (while not loading)
|
||||
if (this.canLoadMore) {
|
||||
this.messagesLoading = true;
|
||||
}
|
||||
|
||||
return this.loadMore();
|
||||
},
|
||||
loadMore () {
|
||||
this.selectedConversation.page += 1;
|
||||
return this.loadMessages();
|
||||
},
|
||||
async loadMessages () {
|
||||
this.messagesLoading = true;
|
||||
|
||||
// use local vars if the loading takes longer
|
||||
// and the user switches the conversation while loading
|
||||
const conversationKey = this.selectedConversation.key;
|
||||
|
||||
const requestUrl = `/api/v4/inbox/paged-messages?conversation=${conversationKey}&page=${this.selectedConversation.page}`;
|
||||
const res = await axios.get(requestUrl);
|
||||
const loadedMessages = res.data.data;
|
||||
|
||||
this.messagesByConversation[conversationKey] = this.messagesByConversation[conversationKey] || [];
|
||||
const loadedMessagesToAdd = loadedMessages
|
||||
.filter(m => this.messagesByConversation[conversationKey].findIndex(mI => mI.id === m.id) === -1);
|
||||
this.messagesByConversation[conversationKey].push(...loadedMessagesToAdd);
|
||||
|
||||
// only show the load more Button if the max count was returned
|
||||
this.selectedConversation.canLoadMore = loadedMessages.length === 10;
|
||||
this.messagesLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -75,6 +75,8 @@ const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/com
|
||||
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
|
||||
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages');
|
||||
|
||||
// Challenges
|
||||
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
|
||||
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
|
||||
@@ -193,6 +195,7 @@ const router = new VueRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '/private-messages', name: 'privateMessages', component: MessagesIndex },
|
||||
{
|
||||
name: 'challenges',
|
||||
path: '/challenges',
|
||||
|
||||
@@ -60,7 +60,10 @@ async function buyArmoire (store, params) {
|
||||
|
||||
const isExperience = item.type === 'experience';
|
||||
if (item.type === 'gear') {
|
||||
store.state.user.data.items.gear.owned[item.dropKey] = true;
|
||||
store.state.user.data.items.gear.owned = {
|
||||
...store.state.user.data.items.gear.owned,
|
||||
[item.dropKey]: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === 'food') {
|
||||
|
||||
@@ -160,3 +160,65 @@ export async function userLookup (store, params) {
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export function block (store, params) {
|
||||
store.state.user.data.inbox.blocks.push(params.uuid);
|
||||
return axios.post(`/api/v4/user/block/${params.uuid}`);
|
||||
}
|
||||
|
||||
export function unblock (store, params) {
|
||||
const index = store.state.user.data.inbox.blocks.indexOf(params.uuid);
|
||||
store.state.user.data.inbox.blocks.splice(index, 1);
|
||||
return axios.post(`/api/v4/user/block/${params.uuid}`);
|
||||
}
|
||||
|
||||
export function newPrivateMessageTo (store, params) {
|
||||
const { member } = params;
|
||||
|
||||
const userStyles = {};
|
||||
userStyles.items = { gear: {} };
|
||||
|
||||
if (member.items) {
|
||||
userStyles.items.gear = {};
|
||||
userStyles.items.gear.costume = { ...member.items.gear.costume };
|
||||
userStyles.items.gear.equipped = { ...member.items.gear.equipped };
|
||||
|
||||
userStyles.items.currentMount = member.items.currentMount;
|
||||
userStyles.items.currentPet = member.items.currentPet;
|
||||
}
|
||||
|
||||
if (member.preferences) {
|
||||
userStyles.preferences = {};
|
||||
if (member.preferences.style) userStyles.preferences.style = member.preferences.style;
|
||||
userStyles.preferences.hair = member.preferences.hair;
|
||||
userStyles.preferences.skin = member.preferences.skin;
|
||||
userStyles.preferences.shirt = member.preferences.shirt;
|
||||
userStyles.preferences.chair = member.preferences.chair;
|
||||
userStyles.preferences.size = member.preferences.size;
|
||||
userStyles.preferences.chair = member.preferences.chair;
|
||||
userStyles.preferences.background = member.preferences.background;
|
||||
userStyles.preferences.costume = member.preferences.costume;
|
||||
}
|
||||
|
||||
if (member.stats) {
|
||||
userStyles.stats = {};
|
||||
userStyles.stats.class = member.stats.class;
|
||||
if (member.stats.buffs) {
|
||||
userStyles.stats.buffs = {
|
||||
seafoam: member.stats.buffs.seafoam,
|
||||
shinySeed: member.stats.buffs.shinySeed,
|
||||
spookySparkles: member.stats.buffs.spookySparkles,
|
||||
snowball: member.stats.buffs.snowball,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
store.state.privateMessageOptions = {
|
||||
userIdToMessage: member._id,
|
||||
displayName: member.profile.name,
|
||||
username: member.auth.local.username,
|
||||
backer: member.backer,
|
||||
contributor: member.contributor,
|
||||
userStyles,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +127,13 @@ export default function () {
|
||||
equipmentDrawerOpen: true,
|
||||
groupPlans: [],
|
||||
isRunningYesterdailies: false,
|
||||
privateMessageOptions: {
|
||||
userIdToMessage: '',
|
||||
displayName: '',
|
||||
username: '',
|
||||
backer: {},
|
||||
contributor: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user