mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-08-04 04:59:40 +00:00
Merge branch 'develop' into release
This commit is contained in:
commit
e989503cfa
39 changed files with 811 additions and 218 deletions
|
|
@ -32,6 +32,7 @@ describe('slack', () => {
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
id: 'chat-id',
|
id: 'chat-id',
|
||||||
|
username: 'author',
|
||||||
user: 'Author',
|
user: 'Author',
|
||||||
uuid: 'author-id',
|
uuid: 'author-id',
|
||||||
text: 'some text',
|
text: 'some text',
|
||||||
|
|
@ -50,11 +51,11 @@ describe('slack', () => {
|
||||||
|
|
||||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||||
text: 'flagger (flagger-id; language: flagger-lang) flagged a message',
|
text: 'flagger (flagger-id; language: flagger-lang) flagged a group message',
|
||||||
attachments: [{
|
attachments: [{
|
||||||
fallback: 'Flag Message',
|
fallback: 'Flag Message',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
author_name: `Author - author@example.com - author-id\n${timestamp}`,
|
author_name: `@author Author (author@example.com; author-id)\n${timestamp}`,
|
||||||
title: 'Flag in Some group - (private guild)',
|
title: 'Flag in Some group - (private guild)',
|
||||||
title_link: undefined,
|
title_link: undefined,
|
||||||
text: 'some text',
|
text: 'some text',
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,11 @@ describe('POST /chat/:chatId/flag', () => {
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||||
text: `${user.profile.name} (${user.id}; language: en) flagged a message`,
|
text: `${user.profile.name} (${user.id}; language: en) flagged a group message`,
|
||||||
attachments: [{
|
attachments: [{
|
||||||
fallback: 'Flag Message',
|
fallback: 'Flag Message',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`,
|
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
|
||||||
title: 'Flag in Test Guild',
|
title: 'Flag in Test Guild',
|
||||||
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
||||||
text: TEST_MESSAGE,
|
text: TEST_MESSAGE,
|
||||||
|
|
@ -98,11 +98,11 @@ describe('POST /chat/:chatId/flag', () => {
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||||
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`,
|
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a group message`,
|
||||||
attachments: [{
|
attachments: [{
|
||||||
fallback: 'Flag Message',
|
fallback: 'Flag Message',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`,
|
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
|
||||||
title: 'Flag in Test Guild',
|
title: 'Flag in Test Guild',
|
||||||
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
title_link: `${BASE_URL}/groups/guild/${group._id}`,
|
||||||
text: TEST_MESSAGE,
|
text: TEST_MESSAGE,
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ describe('POST /chat', () => {
|
||||||
attachments: [{
|
attachments: [{
|
||||||
fallback: 'Slur Message',
|
fallback: 'Slur Message',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
author_name: `${user.profile.name} - ${user.auth.local.email} - ${user._id}`,
|
author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
|
||||||
title: 'Slur in Test Guild',
|
title: 'Slur in Test Guild',
|
||||||
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
|
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
|
||||||
text: testSlurMessage,
|
text: testSlurMessage,
|
||||||
|
|
@ -310,7 +310,7 @@ describe('POST /chat', () => {
|
||||||
attachments: [{
|
attachments: [{
|
||||||
fallback: 'Slur Message',
|
fallback: 'Slur Message',
|
||||||
color: 'danger',
|
color: 'danger',
|
||||||
author_name: `${members[0].profile.name} - ${members[0].auth.local.email} - ${members[0]._id}`,
|
author_name: `@${members[0].auth.local.username} ${members[0].profile.name} (${members[0].auth.local.email}; ${members[0]._id})`,
|
||||||
title: 'Slur in Party - (private party)',
|
title: 'Slur in Party - (private party)',
|
||||||
title_link: undefined,
|
title_link: undefined,
|
||||||
text: testSlurMessage,
|
text: testSlurMessage,
|
||||||
|
|
|
||||||
74
test/api/v4/members/POST-flag_private_message.test.js
Normal file
74
test/api/v4/members/POST-flag_private_message.test.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('POST /members/flag-private-message/:messageId', () => {
|
||||||
|
let userToSendMessage;
|
||||||
|
let messageToSend = 'Test Private Message';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
userToSendMessage = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Allows players to flag their own private message', async () => {
|
||||||
|
let receiver = await generateUser();
|
||||||
|
|
||||||
|
await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: messageToSend,
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
|
||||||
|
let sendersMessageInSendersInbox = _.find(senderMessages, (message) => {
|
||||||
|
return message.uuid === receiver._id && message.text === messageToSend;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendersMessageInSendersInbox).to.exist;
|
||||||
|
await expect(userToSendMessage.post(`/members/flag-private-message/${sendersMessageInSendersInbox.id}`)).to.eventually.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Flags a private message', async () => {
|
||||||
|
let receiver = await generateUser();
|
||||||
|
|
||||||
|
await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: messageToSend,
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
|
||||||
|
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendersMessageInReceiversInbox).to.exist;
|
||||||
|
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Returns an error when user tries to flag a private message that is already flagged', async () => {
|
||||||
|
let receiver = await generateUser();
|
||||||
|
|
||||||
|
await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: messageToSend,
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
|
||||||
|
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendersMessageInReceiversInbox).to.exist;
|
||||||
|
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
|
||||||
|
|
||||||
|
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('messageGroupChatFlagAlreadyReported'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -43,6 +43,8 @@
|
||||||
li(v-html='$t("communityForum")')
|
li(v-html='$t("communityForum")')
|
||||||
li
|
li
|
||||||
a(href='https://www.facebook.com/Habitica', target='_blank') {{ $t('communityFacebook') }}
|
a(href='https://www.facebook.com/Habitica', target='_blank') {{ $t('communityFacebook') }}
|
||||||
|
li
|
||||||
|
a(href='https://www.instagram.com/habitica', target='_blank') {{ $t('communityInstagram') }}
|
||||||
li
|
li
|
||||||
a(href='https://www.reddit.com/r/habitrpg/', target='_blank') {{ $t('communityReddit') }}
|
a(href='https://www.reddit.com/r/habitrpg/', target='_blank') {{ $t('communityReddit') }}
|
||||||
.col-12.col-md-6
|
.col-12.col-md-6
|
||||||
|
|
@ -56,23 +58,22 @@
|
||||||
a(:href="getDataDisplayToolUrl", target='_blank') {{ $t('dataDisplayTool') }}
|
a(:href="getDataDisplayToolUrl", target='_blank') {{ $t('dataDisplayTool') }}
|
||||||
li
|
li
|
||||||
a(href='http://habitica.fandom.com/wiki/Guidance_for_Blacksmiths', target='_blank') {{ $t('guidanceForBlacksmiths') }}
|
a(href='http://habitica.fandom.com/wiki/Guidance_for_Blacksmiths', target='_blank') {{ $t('guidanceForBlacksmiths') }}
|
||||||
li
|
|
||||||
a(href='http://devs.habitica.com/', target='_blank') {{ $t('devBlog') }}
|
|
||||||
.col-6.social
|
.col-6.social
|
||||||
h3 {{ $t('footerSocial') }}
|
h3 {{ $t('footerSocial') }}
|
||||||
a.social-circle(href='https://twitter.com/habitica', target='_blank')
|
.icons
|
||||||
.social-icon.svg-icon(v-html='icons.twitter')
|
a.social-circle(href='https://twitter.com/habitica', target='_blank')
|
||||||
// TODO: Not ready yet. a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
|
.social-icon.svg-icon(v-html='icons.twitter')
|
||||||
.social-icon.svg-icon.instagram(v-html='icons.instagram')
|
a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
|
||||||
a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
|
.social-icon.svg-icon.instagram(v-html='icons.instagram')
|
||||||
.social-icon.facebook.svg-icon(v-html='icons.facebook')
|
a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
|
||||||
|
.social-icon.facebook.svg-icon(v-html='icons.facebook')
|
||||||
.row
|
.row
|
||||||
.col-12.col-md-8 {{ $t('donateText3') }}
|
.col-12.col-md-8 {{ $t('donateText3') }}
|
||||||
.col-12.col-md-4
|
.col-12.col-md-4
|
||||||
button.btn.btn-contribute(@click="donate()", v-if="user")
|
button.btn.btn-contribute.btn-flat(@click="donate()", v-if="user")
|
||||||
.svg-icon.heart(v-html="icons.heart")
|
.svg-icon.heart(v-html="icons.heart")
|
||||||
.text {{ $t('companyDonate') }}
|
.text {{ $t('companyDonate') }}
|
||||||
.btn.btn-contribute(v-else)
|
.btn.btn-contribute.btn-flat(v-else)
|
||||||
a(href='http://habitica.fandom.com/wiki/Contributing_to_Habitica', target='_blank')
|
a(href='http://habitica.fandom.com/wiki/Contributing_to_Habitica', target='_blank')
|
||||||
.svg-icon.heart(v-html="icons.heart")
|
.svg-icon.heart(v-html="icons.heart")
|
||||||
.text {{ $t('companyContribute') }}
|
.text {{ $t('companyContribute') }}
|
||||||
|
|
@ -145,6 +146,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// smaller than desktop
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
.social-circle {
|
||||||
|
height: 32px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
|
||||||
|
margin-left: 0.75em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.social-circle {
|
.social-circle {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
@ -152,17 +169,20 @@
|
||||||
background-color: #c3c0c7;
|
background-color: #c3c0c7;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
float: right;
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #a5a1ac;
|
||||||
|
}
|
||||||
|
|
||||||
.social-icon {
|
.social-icon {
|
||||||
color: #e1e0e3;
|
color: #e1e0e3;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.instagram {
|
|
||||||
margin-top: .85em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
|
@ -185,6 +205,14 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a5a1ac;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
div
|
div
|
||||||
.mentioned-icon(v-if='isUserMentioned')
|
.mentioned-icon(v-if='isUserMentioned')
|
||||||
.message-hidden(v-if='msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
|
.message-hidden(v-if='!inbox && msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
|
||||||
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
|
.message-hidden(v-if='!inbox && msg.flagCount > 1 && user.contributor.admin') Message hidden
|
||||||
.card-body
|
.card-body
|
||||||
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
|
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
|
||||||
p.time
|
p.time
|
||||||
|
|
@ -11,25 +11,28 @@ div
|
||||||
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}
|
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}
|
||||||
span(v-if="msg.client && user.contributor.level >= 4") ({{ msg.client }})
|
span(v-if="msg.client && user.contributor.level >= 4") ({{ msg.client }})
|
||||||
.text(v-html='atHighlight(parseMarkdown(msg.text))')
|
.text(v-html='atHighlight(parseMarkdown(msg.text))')
|
||||||
|
.reported(v-if="isMessageReported && (inbox === true)")
|
||||||
|
span(v-once) {{ $t('reportedMessage')}}
|
||||||
|
br
|
||||||
|
span(v-once) {{ $t('canDeleteNow') }}
|
||||||
hr
|
hr
|
||||||
.d-flex(v-if='msg.id')
|
.d-flex(v-if='msg.id')
|
||||||
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
|
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
|
||||||
.svg-icon(v-html="icons.copy")
|
.svg-icon(v-html="icons.copy")
|
||||||
div {{$t('copyAsTodo')}}
|
div {{$t('copyAsTodo')}}
|
||||||
.action.d-flex.align-items-center(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
|
.action.d-flex.align-items-center(v-if='(inbox || (user.flags.communityGuidelinesAccepted && msg.uuid !== "system")) && !isMessageReported', @click='report(msg)')
|
||||||
.svg-icon(v-html="icons.report")
|
.svg-icon(v-html="icons.report", v-once)
|
||||||
div {{$t('report')}}
|
div(v-once) {{$t('report')}}
|
||||||
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
|
|
||||||
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||||
.svg-icon(v-html="icons.delete")
|
.svg-icon(v-html="icons.delete", v-once)
|
||||||
| {{$t('delete')}}
|
div(v-once) {{$t('delete')}}
|
||||||
.ml-auto.d-flex(v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}", v-if='!inbox')
|
.ml-auto.d-flex(v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}", v-if='!inbox')
|
||||||
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount > 0', :class='{active: msg.likes[user._id]}')
|
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount > 0', :class='{active: msg.likes[user._id]}')
|
||||||
.svg-icon(v-html="icons.liked", :title='$t("liked")')
|
.svg-icon(v-html="icons.liked", :title='$t("liked")')
|
||||||
| +{{ likeCount }}
|
| +{{ likeCount }}
|
||||||
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount === 0', :class='{active: msg.likes[user._id]}')
|
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount === 0', :class='{active: msg.likes[user._id]}')
|
||||||
.svg-icon(v-html="icons.like", :title='$t("like")')
|
.svg-icon(v-html="icons.like", :title='$t("like")')
|
||||||
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
|
span(v-if='!msg.likes[user._id] && !inbox') {{ $t('like') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -111,6 +114,11 @@ div
|
||||||
color: $purple-400;
|
color: $purple-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reported {
|
||||||
|
margin-top: 18px;
|
||||||
|
color: $red-50;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -132,8 +140,15 @@ import reportIcon from 'assets/svg/report.svg';
|
||||||
import {highlightUsers} from '../../libs/highlightUsers';
|
import {highlightUsers} from '../../libs/highlightUsers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['msg', 'inbox', 'groupId'],
|
|
||||||
components: {userLink},
|
components: {userLink},
|
||||||
|
props: {
|
||||||
|
msg: {},
|
||||||
|
inbox: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
groupId: {},
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
|
|
@ -143,6 +158,7 @@ export default {
|
||||||
delete: deleteIcon,
|
delete: deleteIcon,
|
||||||
liked: likedIcon,
|
liked: likedIcon,
|
||||||
}),
|
}),
|
||||||
|
reported: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -191,6 +207,9 @@ export default {
|
||||||
}
|
}
|
||||||
return likeCount;
|
return likeCount;
|
||||||
},
|
},
|
||||||
|
isMessageReported () {
|
||||||
|
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async like () {
|
async like () {
|
||||||
|
|
@ -216,10 +235,18 @@ export default {
|
||||||
copyAsTodo (message) {
|
copyAsTodo (message) {
|
||||||
this.$root.$emit('habitica::copy-as-todo', message);
|
this.$root.$emit('habitica::copy-as-todo', message);
|
||||||
},
|
},
|
||||||
async report () {
|
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', {
|
this.$root.$emit('habitica::report-chat', {
|
||||||
message: this.msg,
|
message: this.msg,
|
||||||
groupId: this.groupId,
|
groupId: this.groupId || 'privateMessage',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async remove () {
|
async remove () {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
.row
|
.row
|
||||||
.col-12
|
.col-12
|
||||||
copy-as-todo-modal(:group-type='groupType', :group-name='groupName', :group-id='groupId')
|
copy-as-todo-modal(:group-type='groupType', :group-name='groupName', :group-id='groupId')
|
||||||
report-flag-modal
|
|
||||||
div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)', :class='{row: inbox}')
|
div(v-for="(msg, index) in messages", v-if='chat && canViewFlag(msg)', :class='{row: inbox}')
|
||||||
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
|
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
|
||||||
avatar.avatar-left(
|
avatar.avatar-left(
|
||||||
|
|
@ -105,14 +104,21 @@ import findIndex from 'lodash/findIndex';
|
||||||
|
|
||||||
import Avatar from '../avatar';
|
import Avatar from '../avatar';
|
||||||
import copyAsTodoModal from './copyAsTodoModal';
|
import copyAsTodoModal from './copyAsTodoModal';
|
||||||
import reportFlagModal from './reportFlagModal';
|
|
||||||
import chatCard from './chatCard';
|
import chatCard from './chatCard';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['chat', 'groupType', 'groupId', 'groupName', 'inbox'],
|
props: {
|
||||||
|
chat: {},
|
||||||
|
inbox: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
groupType: {},
|
||||||
|
groupId: {},
|
||||||
|
groupName: {},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
copyAsTodoModal,
|
copyAsTodoModal,
|
||||||
reportFlagModal,
|
|
||||||
chatCard,
|
chatCard,
|
||||||
Avatar,
|
Avatar,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,16 @@ export default {
|
||||||
this.$root.$emit('bv::hide::modal', 'report-flag');
|
this.$root.$emit('bv::hide::modal', 'report-flag');
|
||||||
},
|
},
|
||||||
async reportAbuse () {
|
async reportAbuse () {
|
||||||
this.notify('Thank you for reporting this violation. The moderators have been notified.');
|
this.text(this.$t(this.groupId === 'privateMessage' ? 'pmReported' : 'abuseReported'));
|
||||||
|
|
||||||
await this.$store.dispatch('chat:flag', {
|
let result = await this.$store.dispatch('chat:flag', {
|
||||||
groupId: this.groupId,
|
groupId: this.groupId,
|
||||||
chatId: this.abuseObject.id,
|
chatId: this.abuseObject.id,
|
||||||
comment: this.reportComment,
|
comment: this.reportComment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$root.$emit('habitica:report-result', result);
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
async clearFlagCount () {
|
async clearFlagCount () {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ div
|
||||||
inbox-modal
|
inbox-modal
|
||||||
creator-intro
|
creator-intro
|
||||||
profileModal
|
profileModal
|
||||||
|
report-flag-modal
|
||||||
send-gems-modal
|
send-gems-modal
|
||||||
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
|
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
|
||||||
b-navbar-brand.brand
|
b-navbar-brand.brand
|
||||||
|
|
@ -351,12 +352,15 @@ import profileModal from '../userMenu/profileModal';
|
||||||
import sendGemsModal from 'client/components/payments/sendGemsModal';
|
import sendGemsModal from 'client/components/payments/sendGemsModal';
|
||||||
import userDropdown from './userDropdown';
|
import userDropdown from './userDropdown';
|
||||||
|
|
||||||
|
import reportFlagModal from '../chat/reportFlagModal';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
creatorIntro,
|
creatorIntro,
|
||||||
InboxModal,
|
InboxModal,
|
||||||
notificationMenu,
|
notificationMenu,
|
||||||
profileModal,
|
profileModal,
|
||||||
|
reportFlagModal,
|
||||||
sendGemsModal,
|
sendGemsModal,
|
||||||
userDropdown,
|
userDropdown,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
)
|
)
|
||||||
div(
|
div(
|
||||||
v-for="group in itemsGroups",
|
v-for="group in itemsGroups",
|
||||||
v-if="viewOptions[group.key].selected",
|
v-if="!anyFilterSelected || viewOptions[group.key].selected",
|
||||||
:key="group.key",
|
:key="group.key",
|
||||||
:class='group.key',
|
:class='group.key',
|
||||||
)
|
)
|
||||||
|
|
@ -383,10 +383,13 @@ export default {
|
||||||
items () {
|
items () {
|
||||||
return this.groupBy === 'type' ? this.gearItemsByType : this.gearItemsByClass;
|
return this.groupBy === 'type' ? this.gearItemsByType : this.gearItemsByClass;
|
||||||
},
|
},
|
||||||
|
anyFilterSelected () {
|
||||||
|
return Object.values(this.viewOptions).some(g => g.selected);
|
||||||
|
},
|
||||||
itemsGroups () {
|
itemsGroups () {
|
||||||
return map(this.groups, (label, group) => {
|
return map(this.groups, (label, group) => {
|
||||||
this.$set(this.viewOptions, group, {
|
this.$set(this.viewOptions, group, {
|
||||||
selected: true,
|
selected: false,
|
||||||
open: false,
|
open: false,
|
||||||
itemsInFirstPosition: [],
|
itemsInFirstPosition: [],
|
||||||
firstRender: true,
|
firstRender: true,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
b-dropdown-item(@click="sortBy = 'AZ'", :active="sortBy === 'AZ'") {{ $t('AZ') }}
|
b-dropdown-item(@click="sortBy = 'AZ'", :active="sortBy === 'AZ'") {{ $t('AZ') }}
|
||||||
div(
|
div(
|
||||||
v-for="group in groups",
|
v-for="group in groups",
|
||||||
v-if="group.selected",
|
v-if="!anyFilterSelected || group.selected",
|
||||||
:key="group.key",
|
:key="group.key",
|
||||||
)
|
)
|
||||||
h2.mb-3
|
h2.mb-3
|
||||||
|
|
@ -225,7 +225,7 @@ const groups = [
|
||||||
return {
|
return {
|
||||||
key: group,
|
key: group,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
selected: true,
|
selected: false,
|
||||||
classPrefix,
|
classPrefix,
|
||||||
allowedItems,
|
allowedItems,
|
||||||
};
|
};
|
||||||
|
|
@ -340,6 +340,10 @@ export default {
|
||||||
|
|
||||||
return itemsByType;
|
return itemsByType;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
anyFilterSelected () {
|
||||||
|
return this.groups.some(g => g.selected);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
userHasPet (potionKey, eggKey) {
|
userHasPet (potionKey, eggKey) {
|
||||||
|
|
@ -465,8 +469,6 @@ export default {
|
||||||
let openedItem = result.data.data;
|
let openedItem = result.data.data;
|
||||||
let text = this.content.gear.flat[openedItem.key].text();
|
let text = this.content.gear.flat[openedItem.key].text();
|
||||||
this.drop(this.$t('messageDropMysteryItem', {dropText: text}), openedItem);
|
this.drop(this.$t('messageDropMysteryItem', {dropText: text}), openedItem);
|
||||||
item.quantity--;
|
|
||||||
this.$forceUpdate();
|
|
||||||
} else {
|
} else {
|
||||||
this.$root.$emit('selectMembersModal::showItem', item);
|
this.$root.$emit('selectMembersModal::showItem', item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
|
||||||
|
|
||||||
div(v-for="(petGroup, index) in petGroups",
|
div(v-for="(petGroup, index) in petGroups",
|
||||||
v-if="viewOptions[petGroup.key].selected",
|
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected",
|
||||||
:key="petGroup.key")
|
:key="petGroup.key")
|
||||||
h4(v-if="viewOptions[petGroup.key].animalCount !== 0") {{ petGroup.label }}
|
h4(v-if="viewOptions[petGroup.key].animalCount !== 0") {{ petGroup.label }}
|
||||||
|
|
||||||
|
|
@ -92,7 +92,11 @@
|
||||||
@click="petClicked(item)"
|
@click="petClicked(item)"
|
||||||
)
|
)
|
||||||
template(slot="itemBadge", slot-scope="context")
|
template(slot="itemBadge", slot-scope="context")
|
||||||
starBadge(:selected="item.key === currentPet", :show="isOwned('pet', item)", @click="selectPet(item)")
|
starBadge(
|
||||||
|
:selected="context.item.key === currentPet",
|
||||||
|
:show="isOwned('pet', context.item)",
|
||||||
|
@click="selectPet(context.item)"
|
||||||
|
)
|
||||||
|
|
||||||
.btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
|
.btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
|
||||||
| {{ $_openedItemRows_isToggled(petGroup.key) ? $t('showLess') : $t('showMore') }}
|
| {{ $_openedItemRows_isToggled(petGroup.key) ? $t('showLess') : $t('showMore') }}
|
||||||
|
|
@ -103,7 +107,7 @@
|
||||||
span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
|
||||||
|
|
||||||
div(v-for="mountGroup in mountGroups",
|
div(v-for="mountGroup in mountGroups",
|
||||||
v-if="viewOptions[mountGroup.key].selected",
|
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected",
|
||||||
:key="mountGroup.key")
|
:key="mountGroup.key")
|
||||||
h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
|
h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
|
||||||
|
|
||||||
|
|
@ -469,7 +473,7 @@
|
||||||
|
|
||||||
petGroups.map((petGroup) => {
|
petGroups.map((petGroup) => {
|
||||||
this.$set(this.viewOptions, petGroup.key, {
|
this.$set(this.viewOptions, petGroup.key, {
|
||||||
selected: true,
|
selected: false,
|
||||||
animalCount: 0,
|
animalCount: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -514,7 +518,7 @@
|
||||||
|
|
||||||
mountGroups.map((mountGroup) => {
|
mountGroups.map((mountGroup) => {
|
||||||
this.$set(this.viewOptions, mountGroup.key, {
|
this.$set(this.viewOptions, mountGroup.key, {
|
||||||
selected: true,
|
selected: false,
|
||||||
animalCount: 0,
|
animalCount: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -538,6 +542,9 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
anyFilterSelected () {
|
||||||
|
return Object.values(this.viewOptions).some(g => g.selected);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setShowMore (key) {
|
setShowMore (key) {
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,31 @@
|
||||||
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") @{{ member.auth.local.username }}
|
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") @{{ member.auth.local.username }}
|
||||||
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") •
|
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") •
|
||||||
span {{ characterLevel }}
|
span {{ characterLevel }}
|
||||||
.progress-container(v-b-tooltip.hover.bottom="$t('health')")
|
stats-bar(
|
||||||
.svg-icon(v-html="icons.health")
|
:icon="icons.health",
|
||||||
.progress
|
:value="member.stats.hp",
|
||||||
.progress-bar.bg-health(:style="{width: `${percent(member.stats.hp, MAX_HEALTH)}%`}")
|
:maxValue="MAX_HEALTH",
|
||||||
span.small-text {{member.stats.hp | statFloor}} / {{MAX_HEALTH}}
|
:tooltip="$t('health')",
|
||||||
.progress-container(v-b-tooltip.hover.bottom="$t('experience')")
|
progressClass="bg-health",
|
||||||
.svg-icon(v-html="icons.experience")
|
:condensed="condensed"
|
||||||
.progress
|
)
|
||||||
.progress-bar.bg-experience(:style="{width: `${percent(member.stats.exp, toNextLevel)}%`}")
|
stats-bar(
|
||||||
span.small-text {{member.stats.exp | statFloor}} / {{toNextLevel}}
|
:icon="icons.experience",
|
||||||
.progress-container(v-if="hasClass", v-b-tooltip.hover.bottom="$t('mana')")
|
:value="member.stats.exp",
|
||||||
.svg-icon(v-html="icons.mana")
|
:maxValue="toNextLevel",
|
||||||
.progress
|
:tooltip="$t('experience')",
|
||||||
.progress-bar.bg-mana(:style="{width: `${percent(member.stats.mp, maxMP)}%`}")
|
progressClass="bg-experience",
|
||||||
span.small-text {{member.stats.mp | statFloor}} / {{maxMP}}
|
:condensed="condensed"
|
||||||
|
)
|
||||||
|
stats-bar(
|
||||||
|
v-if="hasClass",
|
||||||
|
:icon="icons.mana",
|
||||||
|
:value="member.stats.mp",
|
||||||
|
:maxValue="maxMP",
|
||||||
|
:tooltip="$t('mana')",
|
||||||
|
progressClass="bg-mana",
|
||||||
|
:condensed="condensed"
|
||||||
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
@ -99,44 +109,6 @@
|
||||||
margin-bottom: .5em
|
margin-bottom: .5em
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > span {
|
|
||||||
color: $header-color;
|
|
||||||
margin-left: 8px;
|
|
||||||
font-style: normal;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > .svg-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > .progress {
|
|
||||||
min-width: 200px;
|
|
||||||
margin: 0px;
|
|
||||||
border-radius: 2px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: $header-dark-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > .progress > .progress-bar {
|
|
||||||
border-radius: 2px;
|
|
||||||
height: 12px;
|
|
||||||
min-width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container .svg-icon, .progress-container .progress, .progress-container .small-text {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condensed version
|
// Condensed version
|
||||||
.member-details.condensed.expanded {
|
.member-details.condensed.expanded {
|
||||||
background: $header-dark-background;
|
background: $header-dark-background;
|
||||||
|
|
@ -163,25 +135,6 @@
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container > .svg-icon {
|
|
||||||
width: 19px;
|
|
||||||
height: 19px;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > .progress {
|
|
||||||
width: 152px;
|
|
||||||
border-radius: 0px;
|
|
||||||
height: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
background: $purple-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > .progress > .progress-bar {
|
|
||||||
border-radius: 0px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
@ -190,6 +143,7 @@ import Avatar from './avatar';
|
||||||
import ClassBadge from './members/classBadge';
|
import ClassBadge from './members/classBadge';
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import Profile from './userMenu/profile';
|
import Profile from './userMenu/profile';
|
||||||
|
import StatsBar from './ui/statsbar';
|
||||||
|
|
||||||
import { toNextLevel } from '../../common/script/statHelpers';
|
import { toNextLevel } from '../../common/script/statHelpers';
|
||||||
import statsComputed from '../../common/script/libs/statsComputed';
|
import statsComputed from '../../common/script/libs/statsComputed';
|
||||||
|
|
@ -205,9 +159,7 @@ export default {
|
||||||
Avatar,
|
Avatar,
|
||||||
Profile,
|
Profile,
|
||||||
ClassBadge,
|
ClassBadge,
|
||||||
},
|
StatsBar,
|
||||||
directives: {
|
|
||||||
// bTooltip,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
member: {
|
member: {
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,57 @@
|
||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.row(:class="{'small-version': smallVersion}")
|
.row(:class="{'small-version': smallVersion}")
|
||||||
dl
|
dl
|
||||||
template(v-if="quest.collect")
|
.table-row(v-if="quest.collect")
|
||||||
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('collect') + ':' }}
|
dt {{ $t('collect') + ':' }}
|
||||||
dd.col-8
|
dd
|
||||||
div(v-for="(collect, key) of quest.collect")
|
div(v-for="(collect, key) of quest.collect")
|
||||||
span {{ collect.count }} {{ getCollectText(collect) }}
|
span {{ collect.count }} {{ getCollectText(collect) }}
|
||||||
|
|
||||||
template(v-if="quest.boss")
|
.table-row(v-if="quest.boss")
|
||||||
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('bossHP') + ':' }}
|
dt {{ $t('bossHP') + ':' }}
|
||||||
dd.col-8 {{ quest.boss.hp }}
|
dd {{ quest.boss.hp }}
|
||||||
|
|
||||||
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('difficulty') + ':' }}
|
.table-row
|
||||||
dd.col-8
|
dt {{ $t('difficulty') + ':' }}
|
||||||
.svg-icon.inline(
|
dd
|
||||||
v-for="star of stars()", v-html="icons[star]",
|
.svg-icon.inline(
|
||||||
:class="smallVersion ? 'icon-12' : 'icon-16'",
|
v-for="star of stars()", v-html="icons[star]",
|
||||||
)
|
:class="smallVersion ? 'icon-12' : 'icon-16'",
|
||||||
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~client/assets/scss/colors.scss';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: table;
|
||||||
|
color: #E1E0E3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: table-row;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, dd {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, dd, dd > * {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-8 {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-8:not(:last-child) {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@
|
||||||
draggable.sortable-tasks(
|
draggable.sortable-tasks(
|
||||||
ref="tasksList",
|
ref="tasksList",
|
||||||
@update='taskSorted',
|
@update='taskSorted',
|
||||||
|
@start="isDragging(true)",
|
||||||
|
@end="isDragging(false)",
|
||||||
:options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}',
|
:options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}',
|
||||||
class="sortable-tasks"
|
|
||||||
)
|
)
|
||||||
task(
|
task(
|
||||||
v-for="task in taskList",
|
v-for="task in taskList",
|
||||||
|
|
@ -49,12 +50,11 @@
|
||||||
:group='group',
|
:group='group',
|
||||||
)
|
)
|
||||||
template(v-if="hasRewardsList")
|
template(v-if="hasRewardsList")
|
||||||
draggable(
|
draggable.reward-items(
|
||||||
ref="rewardsList",
|
ref="rewardsList",
|
||||||
@update="rewardSorted",
|
@update="rewardSorted",
|
||||||
@start="rewardDragStart",
|
@start="rewardDragStart",
|
||||||
@end="rewardDragEnd",
|
@end="rewardDragEnd",
|
||||||
class="reward-items",
|
|
||||||
)
|
)
|
||||||
shopItem(
|
shopItem(
|
||||||
v-for="reward in inAppRewards",
|
v-for="reward in inAppRewards",
|
||||||
|
|
@ -76,6 +76,9 @@
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~client/assets/scss/colors.scss';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
/deep/ .draggable-cursor {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.tasks-column {
|
.tasks-column {
|
||||||
min-height: 556px;
|
min-height: 556px;
|
||||||
|
|
@ -89,7 +92,6 @@
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.reward-items {
|
.reward-items {
|
||||||
@supports (display: grid) {
|
@supports (display: grid) {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -335,6 +337,7 @@ export default {
|
||||||
showPopovers: true,
|
showPopovers: true,
|
||||||
|
|
||||||
selectedItemToBuy: {},
|
selectedItemToBuy: {},
|
||||||
|
dragging: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|
@ -521,9 +524,11 @@ export default {
|
||||||
rewardDragStart () {
|
rewardDragStart () {
|
||||||
// We need to stop popovers from interfering with our dragging
|
// We need to stop popovers from interfering with our dragging
|
||||||
this.showPopovers = false;
|
this.showPopovers = false;
|
||||||
|
this.isDragging(true);
|
||||||
},
|
},
|
||||||
rewardDragEnd () {
|
rewardDragEnd () {
|
||||||
this.showPopovers = true;
|
this.showPopovers = true;
|
||||||
|
this.isDragging(false);
|
||||||
},
|
},
|
||||||
quickAdd (ev) {
|
quickAdd (ev) {
|
||||||
// Add a new line if Shift+Enter Pressed
|
// Add a new line if Shift+Enter Pressed
|
||||||
|
|
@ -678,6 +683,14 @@ export default {
|
||||||
this.error(e.message);
|
this.error(e.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isDragging (dragging) {
|
||||||
|
this.dragging = dragging;
|
||||||
|
if (dragging) {
|
||||||
|
document.documentElement.classList.add('draggable-cursor');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('draggable-cursor');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
v-if="isUser && !isRunningYesterdailies",
|
v-if="isUser && !isRunningYesterdailies",
|
||||||
:right="task.type === 'reward'",
|
:right="task.type === 'reward'",
|
||||||
ref="taskDropdown",
|
ref="taskDropdown",
|
||||||
v-b-tooltip.hover.top="$t('showMore')"
|
v-b-tooltip.hover.top="$t('options')"
|
||||||
)
|
)
|
||||||
div(slot="dropdown-toggle", draggable=false)
|
div(slot="dropdown-toggle", draggable=false)
|
||||||
.svg-icon.dropdown-icon(v-html="icons.menu")
|
.svg-icon.dropdown-icon(v-html="icons.menu")
|
||||||
|
|
@ -141,6 +141,11 @@
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
// markdown p-tag, can't find without /deep/
|
||||||
|
/deep/ p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.has-notes {
|
&.has-notes {
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +234,7 @@
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
&.has-checklist {
|
&.has-checklist {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,7 +290,7 @@
|
||||||
margin-bottom: -3px;
|
margin-bottom: -3px;
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 8px;
|
margin-left: 0;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
|
@ -427,7 +432,7 @@
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-control, .reward-control {
|
.task-control:not(.task-disabled-habit-control-inner), .reward-control {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
.col-12.col-md-4.offset-md-4
|
.col-12.col-md-4.offset-md-4
|
||||||
.d-flex
|
.d-flex
|
||||||
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
|
input.form-control.input-search(type="text", :placeholder="$t('search')", v-model="searchText")
|
||||||
button.btn.btn-secondary.dropdown-toggle.ml-2.d-flex.align-items-center(
|
button.btn.btn-secondary.dropdown-toggle.ml-2.d-flex.align-items-center.search-button(
|
||||||
type="button",
|
type="button",
|
||||||
@click="toggleFilterPanel()",
|
@click="toggleFilterPanel()",
|
||||||
:class="{active: selectedTags.length > 0}",
|
:class="{active: selectedTags.length > 0}",
|
||||||
|
|
@ -110,6 +110,10 @@
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-search, .search-button {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.tasks-navigation {
|
.tasks-navigation {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
126
website/client/components/ui/statsbar.vue
Normal file
126
website/client/components/ui/statsbar.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template lang="pug">
|
||||||
|
.progress-container(ref="container", :id="elementId", :class="{condensed}")
|
||||||
|
.svg-icon(v-html="icon")
|
||||||
|
.progress
|
||||||
|
.progress-bar(:class="progressClass", :style="{width: `${percent(value, maxValue)}%`}")
|
||||||
|
span.small-text {{value | statFloor}} / {{maxValue}}
|
||||||
|
b-tooltip.myClass(:target="() => $refs.container", :container="elementId", :title="tooltip", triggers="hover", placement="bottom")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-bottom: .5em;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
/deep/ .bs-tooltip-bottom {
|
||||||
|
top: -16px !important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container > span {
|
||||||
|
color: $header-color;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container > .svg-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container > .progress {
|
||||||
|
min-width: 200px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: $header-dark-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container > .progress > .progress-bar {
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 12px;
|
||||||
|
min-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container .svg-icon, .progress-container .progress, .progress-container .small-text {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container.condensed {
|
||||||
|
> .svg-icon {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .progress {
|
||||||
|
width: 152px;
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
background: $purple-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .progress > .progress-bar {
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import percent from '../../../common/script/libs/percent';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
maxValue: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
progressClass: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
condensed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
elementId: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
statFloor (value) {
|
||||||
|
if (value < 1 && value > 0) {
|
||||||
|
return Math.ceil(value * 10) / 10;
|
||||||
|
} else {
|
||||||
|
return Math.floor(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
percent,
|
||||||
|
click () {
|
||||||
|
this.$emit('click');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.elementId = `container_${this._uid}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -324,6 +324,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container > .progress {
|
.progress-container > .progress {
|
||||||
|
border-radius: 1px;
|
||||||
background-color: $gray-500;
|
background-color: $gray-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,8 +372,10 @@
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
border-radius: 1px;
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
|
border-radius: 1px;
|
||||||
background-color: $green-10 !important;
|
background-color: $green-10 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -495,6 +498,9 @@ export default {
|
||||||
async userId () {
|
async userId () {
|
||||||
this.loadUser();
|
this.loadUser();
|
||||||
},
|
},
|
||||||
|
userLoggedIn () {
|
||||||
|
this.loadUser();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadUser () {
|
async loadUser () {
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,18 @@ export async function like (store, payload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function flag (store, payload) {
|
export async function flag (store, payload) {
|
||||||
const url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatId}/flag`;
|
let url = '';
|
||||||
|
|
||||||
|
if (payload.groupId === 'privateMessage') {
|
||||||
|
url = `/api/v4/members/flag-private-message/${payload.chatId}`;
|
||||||
|
} else {
|
||||||
|
url = `/api/v4/groups/${payload.groupId}/chat/${payload.chatId}/flag`;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.post(url, {
|
const response = await axios.post(url, {
|
||||||
comment: payload.comment,
|
comment: payload.comment,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import axios from 'axios';
|
||||||
import { togglePinnedItem as togglePinnedItemOp } from 'common/script/ops/pinnedGearUtils';
|
import { togglePinnedItem as togglePinnedItemOp } from 'common/script/ops/pinnedGearUtils';
|
||||||
import changeClassOp from 'common/script/ops/changeClass';
|
import changeClassOp from 'common/script/ops/changeClass';
|
||||||
import disableClassesOp from 'common/script/ops/disableClasses';
|
import disableClassesOp from 'common/script/ops/disableClasses';
|
||||||
|
import openMysteryItemOp from 'common/script/ops/openMysteryItem';
|
||||||
|
|
||||||
export function fetch (store, options = {}) { // eslint-disable-line no-shadow
|
export function fetch (store, options = {}) { // eslint-disable-line no-shadow
|
||||||
return loadAsyncResource({
|
return loadAsyncResource({
|
||||||
|
|
@ -127,7 +128,9 @@ export function castSpell (store, params) {
|
||||||
return axios.post(spellUrl, data);
|
return axios.post(spellUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openMysteryItem () {
|
export async function openMysteryItem (store) {
|
||||||
|
let user = store.state.user.data;
|
||||||
|
openMysteryItemOp(user);
|
||||||
return axios.post('/api/v4/user/open-mystery-item');
|
return axios.post('/api/v4/user/open-mystery-item');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ module.exports = {
|
||||||
|
|
||||||
missingTypeKeyEquip: '"key" and "type" are required parameters.',
|
missingTypeKeyEquip: '"key" and "type" are required parameters.',
|
||||||
|
|
||||||
|
chatIdRequired: 'req.params.chatId must contain a chatId.',
|
||||||
|
messageIdRequired: 'req.params.messageId must contain a message ID.',
|
||||||
|
|
||||||
guildsOnlyPaginate: 'Only public guilds support pagination.',
|
guildsOnlyPaginate: 'Only public guilds support pagination.',
|
||||||
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
|
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
|
||||||
groupIdRequired: 'req.params.groupId must contain a groupId.',
|
groupIdRequired: 'req.params.groupId must contain a groupId.',
|
||||||
groupRemainOrLeaveChallenges: 'req.query.keep must be either "remain-in-challenges" or "leave-challenges"',
|
groupRemainOrLeaveChallenges: 'req.query.keep must be either "remain-in-challenges" or "leave-challenges"',
|
||||||
managerIdRequired: 'req.body.managerId must contain a user ID.',
|
managerIdRequired: 'req.body.managerId must contain a User ID.',
|
||||||
noSudoAccess: 'You don\'t have sudo access.',
|
noSudoAccess: 'You don\'t have sudo access.',
|
||||||
|
|
||||||
eventRequired: '"req.params.event" is required.',
|
eventRequired: '"req.params.event" is required.',
|
||||||
|
|
@ -22,6 +25,4 @@ module.exports = {
|
||||||
missingCustomerId: 'Missing "req.query.customerId"',
|
missingCustomerId: 'Missing "req.query.customerId"',
|
||||||
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
|
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
|
||||||
missingSubKey: 'Missing "req.query.sub"',
|
missingSubKey: 'Missing "req.query.sub"',
|
||||||
|
|
||||||
messageIdRequired: '\"messageId\" must be a valid UUID.",',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"communityBug": "Submit Bug",
|
"communityBug": "Submit Bug",
|
||||||
"communityExtensions": "<a href='http://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations' target='_blank'>Add-ons & Extensions</a>",
|
"communityExtensions": "<a href='http://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations' target='_blank'>Add-ons & Extensions</a>",
|
||||||
"communityFacebook": "Facebook",
|
"communityFacebook": "Facebook",
|
||||||
|
"communityInstagram": "Instagram",
|
||||||
"communityFeature": "Request Feature",
|
"communityFeature": "Request Feature",
|
||||||
"communityForum": "<a target='_blank' href='http://habitica.fandom.com/wiki/Special:Forum'>Forum</a>",
|
"communityForum": "<a target='_blank' href='http://habitica.fandom.com/wiki/Special:Forum'>Forum</a>",
|
||||||
"communityKickstarter": "Kickstarter",
|
"communityKickstarter": "Kickstarter",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"saveEdits": "Save Edits",
|
"saveEdits": "Save Edits",
|
||||||
"showMore": "Show More",
|
"showMore": "Show More",
|
||||||
"showLess": "Show Less",
|
"showLess": "Show Less",
|
||||||
|
"options": "Options",
|
||||||
|
|
||||||
"expandToolbar": "Expand Toolbar",
|
"expandToolbar": "Expand Toolbar",
|
||||||
"collapseToolbar": "Collapse Toolbar",
|
"collapseToolbar": "Collapse Toolbar",
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@
|
||||||
"abuseFlagModalBody": "Are you sure you want to report this post? You should <strong>only</strong> report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction.",
|
"abuseFlagModalBody": "Are you sure you want to report this post? You should <strong>only</strong> report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction.",
|
||||||
"abuseFlagModalButton": "Report Violation",
|
"abuseFlagModalButton": "Report Violation",
|
||||||
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
|
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
|
||||||
|
"pmReported": "Thank you for reporting this message.",
|
||||||
"abuseAlreadyReported": "You have already reported this message.",
|
"abuseAlreadyReported": "You have already reported this message.",
|
||||||
"whyReportingPost": "Why are you reporting this post?",
|
"whyReportingPost": "Why are you reporting this post?",
|
||||||
"whyReportingPostPlaceholder": "Please help our moderators by letting us know why you are reporting this post for a violation, e.g., spam, swearing, religious oaths, bigotry, slurs, adult topics, violence.",
|
"whyReportingPostPlaceholder": "Please help our moderators by letting us know why you are reporting this post for a violation, e.g., spam, swearing, religious oaths, bigotry, slurs, adult topics, violence.",
|
||||||
|
|
@ -230,9 +231,9 @@
|
||||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||||
"groupMemberNotFound": "User not found among group's members",
|
"groupMemberNotFound": "User not found among group's members",
|
||||||
"mustBeGroupMember": "Must be member of the group.",
|
"mustBeGroupMember": "Must be member of the group.",
|
||||||
"canOnlyInviteEmailUuid": "Can only invite using user IDs, emails, or usernames.",
|
"canOnlyInviteEmailUuid": "Can only invite using User IDs, emails, or usernames.",
|
||||||
"inviteMissingEmail": "Missing email address in invite.",
|
"inviteMissingEmail": "Missing email address in invite.",
|
||||||
"inviteMissingUuid": "Missing user id in invite",
|
"inviteMissingUuid": "Missing User ID in invite",
|
||||||
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
||||||
"partyMustbePrivate": "Parties must be private",
|
"partyMustbePrivate": "Parties must be private",
|
||||||
"userAlreadyInGroup": "UserID: <%= userId %>, User \"<%= username %>\" already in that group.",
|
"userAlreadyInGroup": "UserID: <%= userId %>, User \"<%= username %>\" already in that group.",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"messageAuthEmailTaken": "Email already taken",
|
"messageAuthEmailTaken": "Email already taken",
|
||||||
"messageAuthNoUserFound": "No user found.",
|
"messageAuthNoUserFound": "No user found.",
|
||||||
"messageAuthMustBeLoggedIn": "You must be logged in.",
|
"messageAuthMustBeLoggedIn": "You must be logged in.",
|
||||||
"messageAuthMustIncludeTokens": "You must include a token and uid (user id) in your request",
|
"messageAuthMustIncludeTokens": "You must include a token and uid (User ID) in your request",
|
||||||
|
|
||||||
"messageGroupAlreadyInParty": "Already in a party, try refreshing.",
|
"messageGroupAlreadyInParty": "Already in a party, try refreshing.",
|
||||||
"messageGroupOnlyLeaderCanUpdate": "Only the group leader can update the group!",
|
"messageGroupOnlyLeaderCanUpdate": "Only the group leader can update the group!",
|
||||||
|
|
@ -71,6 +71,7 @@
|
||||||
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!",
|
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!",
|
||||||
|
|
||||||
"messageDeletedUser": "Sorry, this user has deleted their account.",
|
"messageDeletedUser": "Sorry, this user has deleted their account.",
|
||||||
|
"messageMissingDisplayName": "Missing display name.",
|
||||||
"messageMissingDisplayName": "Missing display name."
|
"reportedMessage": "You have reported this message to moderators.",
|
||||||
|
"canDeleteNow": "You can now delete the message if you wish."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ let api = {};
|
||||||
* @apiSuccess {String} challenge.name Full name of challenge.
|
* @apiSuccess {String} challenge.name Full name of challenge.
|
||||||
* @apiSuccess {String} challenge.shortName A shortened name for the challenge, to be used as a tag.
|
* @apiSuccess {String} challenge.shortName A shortened name for the challenge, to be used as a tag.
|
||||||
* @apiSuccess {Object} challenge.leader User details of challenge leader.
|
* @apiSuccess {Object} challenge.leader User details of challenge leader.
|
||||||
* @apiSuccess {UUID} challenge.leader._id User id of challenge leader.
|
* @apiSuccess {UUID} challenge.leader._id User ID of challenge leader.
|
||||||
* @apiSuccess {Object} challenge.leader.profile Profile information of leader.
|
* @apiSuccess {Object} challenge.leader.profile Profile information of leader.
|
||||||
* @apiSuccess {Object} challenge.leader.profile.name Display Name of leader.
|
* @apiSuccess {Object} challenge.leader.profile.name Display Name of leader.
|
||||||
* @apiSuccess {String} challenge.updatedAt Timestamp of last update.
|
* @apiSuccess {String} challenge.updatedAt Timestamp of last update.
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,17 @@ const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine GroupIdRequired
|
* @apiDefine GroupIdRequired
|
||||||
* @apiError (404) {badRequest} groupIdRequired A group ID is required
|
* @apiError (400) {badRequest} groupIdRequired A group ID is required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine ChatIdRequired
|
* @apiDefine ChatIdRequired
|
||||||
* @apiError (404) {badRequest} chatIdRequired A chat ID is required
|
* @apiError (400) {badRequest} chatIdRequired A chat ID is required
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine MessageIdRequired
|
||||||
|
* @apiError (400) {badRequest} messageIdRequired A message ID is required
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
@ -246,7 +251,7 @@ api.likeChat = {
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
|
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||||
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
@ -285,7 +290,7 @@ api.likeChat = {
|
||||||
* @apiSuccess {Object} data.likes The likes of the message
|
* @apiSuccess {Object} data.likes The likes of the message
|
||||||
* @apiSuccess {Object} data.flags The flags of the message
|
* @apiSuccess {Object} data.flags The flags of the message
|
||||||
* @apiSuccess {Number} data.flagCount The number of flags the message has
|
* @apiSuccess {Number} data.flagCount The number of flags the message has
|
||||||
* @apiSuccess {UUID} data.uuid The user id of the author of the message
|
* @apiSuccess {UUID} data.uuid The User ID of the author of the message
|
||||||
* @apiSuccess {String} data.user The username of the author of the message
|
* @apiSuccess {String} data.user The username of the author of the message
|
||||||
*
|
*
|
||||||
* @apiUse GroupNotFound
|
* @apiUse GroupNotFound
|
||||||
|
|
@ -334,7 +339,7 @@ api.clearChatFlags = {
|
||||||
let chatId = req.params.chatId;
|
let chatId = req.params.chatId;
|
||||||
|
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||||
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
@ -470,7 +475,7 @@ api.deleteChat = {
|
||||||
let chatId = req.params.chatId;
|
let chatId = req.params.chatId;
|
||||||
|
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||||
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
|
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
|
||||||
|
|
@ -941,11 +941,11 @@ api.removeGroupMember = {
|
||||||
* {"name": "User2", "email": "user-2@example.com"}
|
* {"name": "User2", "email": "user-2@example.com"}
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
* @apiParamExample {json} User Ids
|
* @apiParamExample {json} User IDs
|
||||||
* {
|
* {
|
||||||
* "uuids": ["user-id-of-existing-user", "user-id-of-another-existing-user"]
|
* "uuids": ["user-id-of-existing-user", "user-id-of-another-existing-user"]
|
||||||
* }
|
* }
|
||||||
* @apiParamExample {json} User Ids and Emails
|
* @apiParamExample {json} User IDs and Emails
|
||||||
* {
|
* {
|
||||||
* "emails": [
|
* "emails": [
|
||||||
* {"email": "user-1@example.com"},
|
* {"email": "user-1@example.com"},
|
||||||
|
|
@ -955,7 +955,7 @@ api.removeGroupMember = {
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @apiSuccess {Array} data The invites
|
* @apiSuccess {Array} data The invites
|
||||||
* @apiSuccess {Object} data[0] If the invitation was a user id, you'll receive back an object. You'll receive one Object for each succesful user id invite.
|
* @apiSuccess {Object} data[0] If the invitation was a User ID, you'll receive back an object. You'll receive one Object for each succesful User ID invite.
|
||||||
* @apiSuccess {String} data[1] If the invitation was an email, you'll receive back the email. You'll receive one String for each successful email invite.
|
* @apiSuccess {String} data[1] If the invitation was an email, you'll receive back the email. You'll receive one String for each successful email invite.
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Successful Response with Emails
|
* @apiSuccessExample {json} Successful Response with Emails
|
||||||
|
|
@ -966,13 +966,13 @@ api.removeGroupMember = {
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json} Successful Response with User Id
|
* @apiSuccessExample {json} Successful Response with User ID
|
||||||
* {
|
* {
|
||||||
* "data": [
|
* "data": [
|
||||||
* { id: 'the-id-of-the-invited-user', name: 'The group name', inviter: 'your-user-id' }
|
* { id: 'the-id-of-the-invited-user', name: 'The group name', inviter: 'your-user-id' }
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
* @apiSuccessExample {json} Successful Response with User Ids and Emails
|
* @apiSuccessExample {json} Successful Response with User IDs and Emails
|
||||||
* {
|
* {
|
||||||
* "data": [
|
* "data": [
|
||||||
* "user-1@example.com",
|
* "user-1@example.com",
|
||||||
|
|
@ -987,9 +987,9 @@ api.removeGroupMember = {
|
||||||
* param `Array`.
|
* param `Array`.
|
||||||
* @apiError (400) {BadRequest} UuidOrEmailOnly The `emails` and `uuids` params were both missing and/or a
|
* @apiError (400) {BadRequest} UuidOrEmailOnly The `emails` and `uuids` params were both missing and/or a
|
||||||
* key other than `emails` or `uuids` was provided in the body param.
|
* key other than `emails` or `uuids` was provided in the body param.
|
||||||
* @apiError (400) {BadRequest} CannotInviteSelf User id or email of invitee matches that of the inviter.
|
* @apiError (400) {BadRequest} CannotInviteSelf User ID or email of invitee matches that of the inviter.
|
||||||
* @apiError (400) {BadRequest} MustBeArray The `uuids` or `emails` body param was not an array.
|
* @apiError (400) {BadRequest} MustBeArray The `uuids` or `emails` body param was not an array.
|
||||||
* @apiError (400) {BadRequest} TooManyInvites A max of 100 invites (combined emails and user ids) can
|
* @apiError (400) {BadRequest} TooManyInvites A max of 100 invites (combined emails and User IDs) can
|
||||||
* be sent out at a time.
|
* be sent out at a time.
|
||||||
* @apiError (400) {BadRequest} ExceedsMembersLimit A max of 30 members can join a party.
|
* @apiError (400) {BadRequest} ExceedsMembersLimit A max of 30 members can join a party.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {put} /api/v3/hall/heroes/:heroId Update any user ("hero")
|
* @api {put} /api/v3/hall/heroes/:heroId Update any user ("hero")
|
||||||
* @apiParam (Path) {UUID} heroId user ID
|
* @apiParam (Path) {UUID} heroId User ID
|
||||||
* @apiName UpdateHero
|
* @apiName UpdateHero
|
||||||
* @apiGroup Hall
|
* @apiGroup Hall
|
||||||
* @apiPermission Admin
|
* @apiPermission Admin
|
||||||
|
|
|
||||||
43
website/server/controllers/api-v4/members.js
Normal file
43
website/server/controllers/api-v4/members.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
|
||||||
|
|
||||||
|
let api = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/members/flag-private-message/:messageId Flag a private message
|
||||||
|
* @apiDescription An email and slack message are sent to the moderators about every flagged message.
|
||||||
|
* @apiName FlagPrivateMessage
|
||||||
|
* @apiGroup Member
|
||||||
|
*
|
||||||
|
* @apiParam (Path) {UUID} messageId The private message id
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The flagged private message
|
||||||
|
* @apiSuccess {UUID} data.id The id of the message
|
||||||
|
* @apiSuccess {String} data.text The text of the message
|
||||||
|
* @apiSuccess {Number} data.timestamp The timestamp of the message in milliseconds
|
||||||
|
* @apiSuccess {Object} data.likes The likes of the message (always an empty object)
|
||||||
|
* @apiSuccess {Object} data.flags The flags of the message
|
||||||
|
* @apiSuccess {Number} data.flagCount The number of flags the message has
|
||||||
|
* @apiSuccess {UUID} data.uuid The User ID of the author of the message, or of the recipient if `sent` is true
|
||||||
|
* @apiSuccess {String} data.user The Display Name of the author of the message, or of the recipient if `sent` is true
|
||||||
|
* @apiSuccess {String} data.username The Username of the author of the message, or of the recipient if `sent` is true
|
||||||
|
*
|
||||||
|
* @apiUse MessageNotFound
|
||||||
|
* @apiUse MessageIdRequired
|
||||||
|
* @apiError (400) {BadRequest} messageGroupChatFlagAlreadyReported You have already reported this message
|
||||||
|
*/
|
||||||
|
api.flagPrivateMessage = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/members/flag-private-message/:messageId',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
const chatReporter = chatReporterFactory('Inbox', req, res);
|
||||||
|
const message = await chatReporter.flag();
|
||||||
|
res.respond(200, {
|
||||||
|
ok: true,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import {
|
import { getGroupUrl, getUserInfo } from '../email';
|
||||||
} from '../errors';
|
|
||||||
import { getUserInfo } from '../email';
|
|
||||||
import { getAuthorEmailFromMessage } from '../chat';
|
import { getAuthorEmailFromMessage } from '../chat';
|
||||||
|
|
||||||
export default class ChatReporter {
|
export default class ChatReporter {
|
||||||
|
|
@ -11,25 +9,51 @@ export default class ChatReporter {
|
||||||
|
|
||||||
async validate () {}
|
async validate () {}
|
||||||
|
|
||||||
async notify (group, message) {
|
async getMessageVariables (group, message) {
|
||||||
const reporterEmailContent = getUserInfo(this.user, ['email']).email;
|
const reporterEmail = getUserInfo(this.user, ['email']).email;
|
||||||
this.authorEmail = await getAuthorEmailFromMessage(message);
|
|
||||||
this.emailVariables = [
|
const authorVariables = await this.getAuthorVariables(message);
|
||||||
|
const groupUrl = getGroupUrl(group);
|
||||||
|
|
||||||
|
return [
|
||||||
{name: 'MESSAGE_TIME', content: (new Date(message.timestamp)).toString()},
|
{name: 'MESSAGE_TIME', content: (new Date(message.timestamp)).toString()},
|
||||||
{name: 'MESSAGE_TEXT', content: message.text},
|
{name: 'MESSAGE_TEXT', content: message.text},
|
||||||
|
|
||||||
{name: 'REPORTER_USERNAME', content: this.user.profile.name},
|
{name: 'REPORTER_DISPLAY_NAME', content: this.user.profile.name},
|
||||||
|
{name: 'REPORTER_USERNAME', content: this.user.auth.local.username},
|
||||||
{name: 'REPORTER_UUID', content: this.user._id},
|
{name: 'REPORTER_UUID', content: this.user._id},
|
||||||
{name: 'REPORTER_EMAIL', content: reporterEmailContent},
|
{name: 'REPORTER_EMAIL', content: reporterEmail},
|
||||||
{name: 'REPORTER_MODAL_URL', content: `/static/front/#?memberId=${this.user._id}`},
|
{name: 'REPORTER_MODAL_URL', content: `/static/front/#?memberId=${this.user._id}`},
|
||||||
|
|
||||||
{name: 'AUTHOR_USERNAME', content: message.user},
|
...authorVariables,
|
||||||
{name: 'AUTHOR_UUID', content: message.uuid},
|
|
||||||
{name: 'AUTHOR_EMAIL', content: this.authorEmail},
|
{name: 'GROUP_NAME', content: group.name},
|
||||||
{name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId=${message.uuid}`},
|
{name: 'GROUP_TYPE', content: group.type},
|
||||||
|
{name: 'GROUP_ID', content: group._id},
|
||||||
|
{name: 'GROUP_URL', content: groupUrl || 'N/A'},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createGenericAuthorVariables (prefix, {user, username, uuid, email}) {
|
||||||
|
return [
|
||||||
|
{name: `${prefix}_DISPLAY_NAME`, content: user},
|
||||||
|
{name: `${prefix}_USERNAME`, content: username},
|
||||||
|
{name: `${prefix}_UUID`, content: uuid},
|
||||||
|
{name: `${prefix}_EMAIL`, content: email},
|
||||||
|
{name: `${prefix}_MODAL_URL`, content: `/static/front/#?memberId=${uuid}`},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthorVariables (message) {
|
||||||
|
this.authorEmail = await getAuthorEmailFromMessage(message);
|
||||||
|
return this.createGenericAuthorVariables('AUTHOR', {
|
||||||
|
user: message.user,
|
||||||
|
username: message.username,
|
||||||
|
uuid: message.uuid,
|
||||||
|
email: this.authorEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async flag () {
|
async flag () {
|
||||||
throw new Error('Flag must be implemented');
|
throw new Error('Flag must be implemented');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import GroupChatReporter from './groupChatReporter';
|
import GroupChatReporter from './groupChatReporter';
|
||||||
// import InboxChatReporter from './inboxChatReporter';
|
import InboxChatReporter from './inboxChatReporter';
|
||||||
|
|
||||||
export function chatReporterFactory (type, req, res) {
|
export function chatReporterFactory (type, req, res) {
|
||||||
if (type === 'Group') {
|
if (type === 'Group') {
|
||||||
return new GroupChatReporter(req, res);
|
return new GroupChatReporter(req, res);
|
||||||
|
} else if (type === 'Inbox') {
|
||||||
|
return new InboxChatReporter(req, res);
|
||||||
}
|
}
|
||||||
// else if (type === 'Inbox') {
|
|
||||||
// return new InboxChatReporter(req, res);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotFound,
|
NotFound,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { getGroupUrl, sendTxn } from '../email';
|
import { sendTxn } from '../email';
|
||||||
import slack from '../slack';
|
import slack from '../slack';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { chatModel as Chat } from '../../models/message';
|
import { chatModel as Chat } from '../../models/message';
|
||||||
|
|
@ -28,7 +28,7 @@ export default class GroupChatReporter extends ChatReporter {
|
||||||
|
|
||||||
async validate () {
|
async validate () {
|
||||||
this.req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
this.req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||||
this.req.checkParams('chatId', this.res.t('chatIdRequired')).notEmpty();
|
this.req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
|
||||||
|
|
||||||
let validationErrors = this.req.validationErrors();
|
let validationErrors = this.req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
@ -50,16 +50,12 @@ export default class GroupChatReporter extends ChatReporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async notify (group, message, userComment, automatedComment = '') {
|
async notify (group, message, userComment, automatedComment = '') {
|
||||||
await super.notify(group, message);
|
let emailVariables = await this.getMessageVariables(group, message);
|
||||||
|
emailVariables = emailVariables.concat([
|
||||||
const groupUrl = getGroupUrl(group);
|
|
||||||
sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', this.emailVariables.concat([
|
|
||||||
{name: 'GROUP_NAME', content: group.name},
|
|
||||||
{name: 'GROUP_TYPE', content: group.type},
|
|
||||||
{name: 'GROUP_ID', content: group._id},
|
|
||||||
{name: 'GROUP_URL', content: groupUrl},
|
|
||||||
{name: 'REPORTER_COMMENT', content: userComment || ''},
|
{name: 'REPORTER_COMMENT', content: userComment || ''},
|
||||||
]));
|
]);
|
||||||
|
|
||||||
|
sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', emailVariables);
|
||||||
|
|
||||||
slack.sendFlagNotification({
|
slack.sendFlagNotification({
|
||||||
authorEmail: this.authorEmail,
|
authorEmail: this.authorEmail,
|
||||||
|
|
|
||||||
129
website/server/libs/chatReporting/inboxChatReporter.js
Normal file
129
website/server/libs/chatReporting/inboxChatReporter.js
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import { model as User } from '../../models/user';
|
||||||
|
|
||||||
|
import ChatReporter from './chatReporter';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
} from '../errors';
|
||||||
|
import { getUserInfo, sendTxn} from '../email';
|
||||||
|
import slack from '../slack';
|
||||||
|
import apiError from '../apiError';
|
||||||
|
|
||||||
|
import * as inboxLib from '../inbox';
|
||||||
|
import {getAuthorEmailFromMessage} from '../chat';
|
||||||
|
|
||||||
|
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
|
||||||
|
return { email, canSend: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class InboxChatReporter extends ChatReporter {
|
||||||
|
constructor (req, res) {
|
||||||
|
super(req, res);
|
||||||
|
|
||||||
|
this.user = res.locals.user;
|
||||||
|
this.inboxUser = res.locals.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate () {
|
||||||
|
this.req.checkParams('messageId', apiError('messageIdRequired')).notEmpty();
|
||||||
|
|
||||||
|
let validationErrors = this.req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
if (this.user.contributor.admin && this.req.query.userId) {
|
||||||
|
this.inboxUser = await User.findOne({_id: this.req.query.userId});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await inboxLib.getUserInboxMessage(this.inboxUser, this.req.params.messageId);
|
||||||
|
if (!message) throw new BadRequest(this.res.t('messageGroupChatNotFound'));
|
||||||
|
|
||||||
|
const userComment = this.req.body.comment;
|
||||||
|
|
||||||
|
return {message, userComment};
|
||||||
|
}
|
||||||
|
|
||||||
|
async notify (message, userComment) {
|
||||||
|
const group = {
|
||||||
|
type: 'private messages',
|
||||||
|
name: 'N/A',
|
||||||
|
_id: 'N/A',
|
||||||
|
};
|
||||||
|
|
||||||
|
let emailVariables = await this.getMessageVariables(group, message);
|
||||||
|
emailVariables = emailVariables.concat([
|
||||||
|
{name: 'REPORTER_COMMENT', content: userComment || ''},
|
||||||
|
]);
|
||||||
|
|
||||||
|
sendTxn(FLAG_REPORT_EMAILS, 'flag-report-to-mods-with-comments', emailVariables);
|
||||||
|
|
||||||
|
slack.sendInboxFlagNotification({
|
||||||
|
authorEmail: this.authorEmail,
|
||||||
|
flagger: this.user,
|
||||||
|
message,
|
||||||
|
userComment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthorVariables (message) {
|
||||||
|
const messageUser = {
|
||||||
|
user: message.user,
|
||||||
|
username: message.username,
|
||||||
|
uuid: message.uuid,
|
||||||
|
email: await getAuthorEmailFromMessage(message),
|
||||||
|
};
|
||||||
|
|
||||||
|
const reporter = {
|
||||||
|
user: this.user.profile.name,
|
||||||
|
username: this.user.auth.local.username,
|
||||||
|
uuid: this.user._id,
|
||||||
|
email: getUserInfo(this.user, ['email']).email,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if message.sent, the reporter is the author of this message
|
||||||
|
const sendingUser = message.sent ? reporter : messageUser;
|
||||||
|
const recipient = message.sent ? messageUser : reporter;
|
||||||
|
|
||||||
|
this.authorEmail = sendingUser.email;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...this.createGenericAuthorVariables('AUTHOR', sendingUser),
|
||||||
|
...this.createGenericAuthorVariables('RECIPIENT', recipient),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessageAndSave (message, ...changedFields) {
|
||||||
|
for (const changedField of changedFields) {
|
||||||
|
message.markModified(changedField);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
flagInboxMessage (message) {
|
||||||
|
// Log user ids that have flagged the message
|
||||||
|
if (!message.flags) message.flags = {};
|
||||||
|
// TODO fix error type
|
||||||
|
if (message.flags[this.user._id] && !this.user.contributor.admin) {
|
||||||
|
throw new BadRequest(this.res.t('messageGroupChatFlagAlreadyReported'));
|
||||||
|
}
|
||||||
|
|
||||||
|
message.flags[this.user._id] = true;
|
||||||
|
message.flagCount = 1;
|
||||||
|
|
||||||
|
return this.updateMessageAndSave(message, 'flags', 'flagCount');
|
||||||
|
}
|
||||||
|
|
||||||
|
async markMessageAsReported (message) {
|
||||||
|
message.reported = true;
|
||||||
|
|
||||||
|
return this.updateMessageAndSave(message, 'reported');
|
||||||
|
}
|
||||||
|
|
||||||
|
async flag () {
|
||||||
|
let {message, userComment} = await this.validate();
|
||||||
|
await this.flagInboxMessage(message);
|
||||||
|
await this.notify(message, userComment);
|
||||||
|
await this.markMessageAsReported(message);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,10 @@ export async function getUserInbox (user, asArray = true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserInboxMessage (user, messageId) {
|
||||||
|
return Inbox.findOne({ownerId: user._id, _id: messageId}).exec();
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteMessage (user, messageId) {
|
export async function deleteMessage (user, messageId) {
|
||||||
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
|
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
|
||||||
if (!message) return false;
|
if (!message) return false;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||||
// Passport session setup.
|
// Passport session setup.
|
||||||
// To support persistent login sessions, Passport needs to be able to
|
// To support persistent login sessions, Passport needs to be able to
|
||||||
// serialize users into and deserialize users out of the session. Typically,
|
// serialize users into and deserialize users out of the session. Typically,
|
||||||
// this will be as simple as storing the user ID when serializing, and finding
|
// this will be as simple as storing the User ID when serializing, and finding
|
||||||
// the user by ID when deserializing. However, since this example does not
|
// the user by ID when deserializing. However, since this example does not
|
||||||
// have a database of user records, the complete Facebook profile is serialized
|
// have a database of user records, the complete Facebook profile is serialized
|
||||||
// and deserialized.
|
// and deserialized.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ const SLACK_FLAGGING_URL = nconf.get('SLACK_FLAGGING_URL');
|
||||||
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK');
|
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK');
|
||||||
const SLACK_SUBSCRIPTIONS_URL = nconf.get('SLACK_SUBSCRIPTIONS_URL');
|
const SLACK_SUBSCRIPTIONS_URL = nconf.get('SLACK_SUBSCRIPTIONS_URL');
|
||||||
const BASE_URL = nconf.get('BASE_URL');
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
const IS_PRODUCTION = nconf.get('IS_PROD');
|
||||||
|
|
||||||
|
const SKIP_FLAG_METHODS = IS_PRODUCTION && !SLACK_FLAGGING_URL;
|
||||||
|
const SKIP_SUB_METHOD = IS_PRODUCTION && !SLACK_SUBSCRIPTIONS_URL;
|
||||||
|
|
||||||
let flagSlack;
|
let flagSlack;
|
||||||
let subscriptionSlack;
|
let subscriptionSlack;
|
||||||
|
|
@ -18,6 +22,26 @@ try {
|
||||||
subscriptionSlack = new IncomingWebhook(SLACK_SUBSCRIPTIONS_URL);
|
subscriptionSlack = new IncomingWebhook(SLACK_SUBSCRIPTIONS_URL);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
|
if (!IS_PRODUCTION) {
|
||||||
|
flagSlack = subscriptionSlack = {
|
||||||
|
send (data) {
|
||||||
|
logger.info('Data sent to slack', data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param formatObj.name userName
|
||||||
|
* @param formatObj.displayName displayName
|
||||||
|
* @param formatObj.email email
|
||||||
|
* @param formatObj.uuid uuid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatUser (formatObj) {
|
||||||
|
return `@${formatObj.name} ${formatObj.displayName} (${formatObj.email}; ${formatObj.uuid})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendFlagNotification ({
|
function sendFlagNotification ({
|
||||||
|
|
@ -28,13 +52,13 @@ function sendFlagNotification ({
|
||||||
userComment,
|
userComment,
|
||||||
automatedComment,
|
automatedComment,
|
||||||
}) {
|
}) {
|
||||||
if (!SLACK_FLAGGING_URL) {
|
if (SKIP_FLAG_METHODS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let titleLink;
|
let titleLink;
|
||||||
let authorName;
|
let authorName;
|
||||||
let title = `Flag in ${group.name}`;
|
let title = `Flag in ${group.name}`;
|
||||||
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a message`;
|
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a group message`;
|
||||||
let footer = `<${SLACK_FLAGGING_FOOTER_LINK}?groupId=${group.id}&chatId=${message.id}|Flag this message.>`;
|
let footer = `<${SLACK_FLAGGING_FOOTER_LINK}?groupId=${group.id}&chatId=${message.id}|Flag this message.>`;
|
||||||
|
|
||||||
if (userComment) {
|
if (userComment) {
|
||||||
|
|
@ -55,7 +79,12 @@ function sendFlagNotification ({
|
||||||
if (!message.user && message.uuid === 'system') {
|
if (!message.user && message.uuid === 'system') {
|
||||||
authorName = 'System Message';
|
authorName = 'System Message';
|
||||||
} else {
|
} else {
|
||||||
authorName = `${message.user} - ${authorEmail} - ${message.uuid}`;
|
authorName = formatUser({
|
||||||
|
name: message.username,
|
||||||
|
displayName: message.user,
|
||||||
|
email: authorEmail,
|
||||||
|
uuid: message.uuid,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||||
|
|
@ -77,6 +106,69 @@ function sendFlagNotification ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendInboxFlagNotification ({
|
||||||
|
authorEmail,
|
||||||
|
flagger,
|
||||||
|
message,
|
||||||
|
userComment,
|
||||||
|
}) {
|
||||||
|
if (SKIP_FLAG_METHODS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let titleLink = '';
|
||||||
|
let authorName;
|
||||||
|
let title = `Flag in ${flagger.profile.name}'s Inbox`;
|
||||||
|
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a PM`;
|
||||||
|
let footer = '';
|
||||||
|
|
||||||
|
if (userComment) {
|
||||||
|
text += ` and commented: ${userComment}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageText = message.text;
|
||||||
|
let sender = '';
|
||||||
|
let recipient = '';
|
||||||
|
|
||||||
|
const flaggerFormat = formatUser({
|
||||||
|
displayName: flagger.profile.name,
|
||||||
|
name: flagger.auth.local.username,
|
||||||
|
email: flagger.auth.local.email,
|
||||||
|
uuid: flagger._id,
|
||||||
|
});
|
||||||
|
const messageUserFormat = formatUser({
|
||||||
|
displayName: message.user,
|
||||||
|
name: message.username,
|
||||||
|
email: authorEmail,
|
||||||
|
uuid: message.uuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.sent) {
|
||||||
|
sender = flaggerFormat;
|
||||||
|
recipient = messageUserFormat;
|
||||||
|
} else {
|
||||||
|
sender = messageUserFormat;
|
||||||
|
recipient = flaggerFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
authorName = `${sender} wrote this message to ${recipient}.`;
|
||||||
|
|
||||||
|
flagSlack.send({
|
||||||
|
text,
|
||||||
|
attachments: [{
|
||||||
|
fallback: 'Flag Message',
|
||||||
|
color: 'danger',
|
||||||
|
author_name: authorName,
|
||||||
|
title,
|
||||||
|
title_link: titleLink,
|
||||||
|
text: messageText,
|
||||||
|
footer,
|
||||||
|
mrkdwn_in: [
|
||||||
|
'text',
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sendSubscriptionNotification ({
|
function sendSubscriptionNotification ({
|
||||||
buyer,
|
buyer,
|
||||||
recipient,
|
recipient,
|
||||||
|
|
@ -84,7 +176,7 @@ function sendSubscriptionNotification ({
|
||||||
months,
|
months,
|
||||||
groupId,
|
groupId,
|
||||||
}) {
|
}) {
|
||||||
if (!SLACK_SUBSCRIPTIONS_URL) {
|
if (SKIP_SUB_METHOD) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let text;
|
let text;
|
||||||
|
|
@ -108,7 +200,7 @@ function sendSlurNotification ({
|
||||||
group,
|
group,
|
||||||
message,
|
message,
|
||||||
}) {
|
}) {
|
||||||
if (!SLACK_FLAGGING_URL) {
|
if (SKIP_FLAG_METHODS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let titleLink;
|
let titleLink;
|
||||||
|
|
@ -124,7 +216,12 @@ function sendSlurNotification ({
|
||||||
title += ` - (${group.privacy} ${group.type})`;
|
title += ` - (${group.privacy} ${group.type})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
authorName = `${author.profile.name} - ${authorEmail} - ${author.id}`;
|
authorName = formatUser({
|
||||||
|
name: author.auth.local.username,
|
||||||
|
displayName: author.profile.name,
|
||||||
|
email: authorEmail,
|
||||||
|
uuid: author.id,
|
||||||
|
});
|
||||||
|
|
||||||
flagSlack.send({
|
flagSlack.send({
|
||||||
text,
|
text,
|
||||||
|
|
@ -143,5 +240,9 @@ function sendSlurNotification ({
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendFlagNotification, sendSubscriptionNotification, sendSlurNotification,
|
sendFlagNotification,
|
||||||
|
sendInboxFlagNotification,
|
||||||
|
sendSubscriptionNotification,
|
||||||
|
sendSlurNotification,
|
||||||
|
formatUser,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,7 @@ function getInviteCount (uuids, emails) {
|
||||||
/**
|
/**
|
||||||
* Checks invitation uuids and emails for possible errors.
|
* Checks invitation uuids and emails for possible errors.
|
||||||
*
|
*
|
||||||
* @param uuids An array of user ids
|
* @param uuids An array of User IDs
|
||||||
* @param emails An array of emails
|
* @param emails An array of emails
|
||||||
* @param res Express res object for use with translations
|
* @param res Express res object for use with translations
|
||||||
* @throws BadRequest An error describing the issue with the invitations
|
* @throws BadRequest An error describing the issue with the invitations
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue