Merge branch 'develop' into release

This commit is contained in:
Sabe Jones 2019-03-07 13:28:34 -06:00
commit e989503cfa
39 changed files with 811 additions and 218 deletions

View file

@ -32,6 +32,7 @@ describe('slack', () => {
},
message: {
id: 'chat-id',
username: 'author',
user: 'Author',
uuid: 'author-id',
text: 'some text',
@ -50,11 +51,11 @@ describe('slack', () => {
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
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: [{
fallback: 'Flag Message',
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_link: undefined,
text: 'some text',

View file

@ -63,11 +63,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */
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: [{
fallback: 'Flag Message',
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_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
@ -98,11 +98,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */
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: [{
fallback: 'Flag Message',
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_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,

View file

@ -257,7 +257,7 @@ describe('POST /chat', () => {
attachments: [{
fallback: 'Slur Message',
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_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
@ -310,7 +310,7 @@ describe('POST /chat', () => {
attachments: [{
fallback: 'Slur Message',
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_link: undefined,
text: testSlurMessage,

View 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'),
});
});
});

View file

@ -43,6 +43,8 @@
li(v-html='$t("communityForum")')
li
a(href='https://www.facebook.com/Habitica', target='_blank') {{ $t('communityFacebook') }}
li
a(href='https://www.instagram.com/habitica', target='_blank') {{ $t('communityInstagram') }}
li
a(href='https://www.reddit.com/r/habitrpg/', target='_blank') {{ $t('communityReddit') }}
.col-12.col-md-6
@ -56,23 +58,22 @@
a(:href="getDataDisplayToolUrl", target='_blank') {{ $t('dataDisplayTool') }}
li
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
h3 {{ $t('footerSocial') }}
a.social-circle(href='https://twitter.com/habitica', target='_blank')
.social-icon.svg-icon(v-html='icons.twitter')
// TODO: Not ready yet. a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
.social-icon.svg-icon.instagram(v-html='icons.instagram')
a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
.social-icon.facebook.svg-icon(v-html='icons.facebook')
.icons
a.social-circle(href='https://twitter.com/habitica', target='_blank')
.social-icon.svg-icon(v-html='icons.twitter')
a.social-circle(href='https://www.instagram.com/habitica/', target='_blank')
.social-icon.svg-icon.instagram(v-html='icons.instagram')
a.social-circle(href='https://www.facebook.com/Habitica', target='_blank')
.social-icon.facebook.svg-icon(v-html='icons.facebook')
.row
.col-12.col-md-8 {{ $t('donateText3') }}
.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")
.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')
.svg-icon.heart(v-html="icons.heart")
.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 {
width: 40px;
height: 40px;
@ -152,17 +169,20 @@
background-color: #c3c0c7;
display: flex;
margin-left: 1em;
float: right;
&:first-child {
margin-left: 0;
}
&:hover {
background-color: #a5a1ac;
}
.social-icon {
color: #e1e0e3;
width: 16px;
margin: auto;
}
.instagram {
margin-top: .85em;
}
}
.logo {
@ -185,6 +205,14 @@
box-shadow: none;
border-radius: 4px;
&:hover {
background: #a5a1ac;
.text {
color: white;
}
}
a {
display: flex;
}

View file

@ -1,8 +1,8 @@
<template lang="pug">
div
.mentioned-icon(v-if='isUserMentioned')
.message-hidden(v-if='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 flagged once, not hidden
.message-hidden(v-if='!inbox && msg.flagCount > 1 && user.contributor.admin') Message hidden
.card-body
user-link(:userId="msg.uuid", :name="msg.user", :backer="msg.backer", :contributor="msg.contributor")
p.time
@ -11,25 +11,28 @@ div
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}&nbsp;
span(v-if="msg.client && user.contributor.level >= 4") ({{ msg.client }})
.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
.d-flex(v-if='msg.id')
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
div {{$t('copyAsTodo')}}
.action.d-flex.align-items-center(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
div {{$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='(inbox || (user.flags.communityGuidelinesAccepted && msg.uuid !== "system")) && !isMessageReported', @click='report(msg)')
.svg-icon(v-html="icons.report", v-once)
div(v-once) {{$t('report')}}
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
.svg-icon(v-html="icons.delete", v-once)
div(v-once) {{$t('delete')}}
.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]}')
.svg-icon(v-html="icons.liked", :title='$t("liked")')
| +{{ likeCount }}
.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")')
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='!msg.likes[user._id] && !inbox') {{ $t('like') }}
</template>
<style lang="scss">
@ -111,6 +114,11 @@ div
color: $purple-400;
}
}
.reported {
margin-top: 18px;
color: $red-50;
}
</style>
<script>
@ -132,8 +140,15 @@ import reportIcon from 'assets/svg/report.svg';
import {highlightUsers} from '../../libs/highlightUsers';
export default {
props: ['msg', 'inbox', 'groupId'],
components: {userLink},
props: {
msg: {},
inbox: {
type: Boolean,
default: false,
},
groupId: {},
},
data () {
return {
icons: Object.freeze({
@ -143,6 +158,7 @@ export default {
delete: deleteIcon,
liked: likedIcon,
}),
reported: false,
};
},
filters: {
@ -191,6 +207,9 @@ export default {
}
return likeCount;
},
isMessageReported () {
return this.msg.flags && this.msg.flags[this.user.id] || this.reported;
},
},
methods: {
async like () {
@ -216,10 +235,18 @@ export default {
copyAsTodo (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', {
message: this.msg,
groupId: this.groupId,
groupId: this.groupId || 'privateMessage',
});
},
async remove () {

View file

@ -3,7 +3,6 @@
.row
.col-12
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}')
.d-flex(v-if='user._id !== msg.uuid', :class='{"flex-grow-1": inbox}')
avatar.avatar-left(
@ -105,14 +104,21 @@ import findIndex from 'lodash/findIndex';
import Avatar from '../avatar';
import copyAsTodoModal from './copyAsTodoModal';
import reportFlagModal from './reportFlagModal';
import chatCard from './chatCard';
export default {
props: ['chat', 'groupType', 'groupId', 'groupName', 'inbox'],
props: {
chat: {},
inbox: {
type: Boolean,
default: false,
},
groupType: {},
groupId: {},
groupName: {},
},
components: {
copyAsTodoModal,
reportFlagModal,
chatCard,
Avatar,
},

View file

@ -106,14 +106,16 @@ export default {
this.$root.$emit('bv::hide::modal', 'report-flag');
},
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,
chatId: this.abuseObject.id,
comment: this.reportComment,
});
this.$root.$emit('habitica:report-result', result);
this.close();
},
async clearFlagCount () {

View file

@ -3,6 +3,7 @@ div
inbox-modal
creator-intro
profileModal
report-flag-modal
send-gems-modal
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
b-navbar-brand.brand
@ -351,12 +352,15 @@ import profileModal from '../userMenu/profileModal';
import sendGemsModal from 'client/components/payments/sendGemsModal';
import userDropdown from './userDropdown';
import reportFlagModal from '../chat/reportFlagModal';
export default {
components: {
creatorIntro,
InboxModal,
notificationMenu,
profileModal,
reportFlagModal,
sendGemsModal,
userDropdown,
},

View file

@ -82,7 +82,7 @@
)
div(
v-for="group in itemsGroups",
v-if="viewOptions[group.key].selected",
v-if="!anyFilterSelected || viewOptions[group.key].selected",
:key="group.key",
:class='group.key',
)
@ -383,10 +383,13 @@ export default {
items () {
return this.groupBy === 'type' ? this.gearItemsByType : this.gearItemsByClass;
},
anyFilterSelected () {
return Object.values(this.viewOptions).some(g => g.selected);
},
itemsGroups () {
return map(this.groups, (label, group) => {
this.$set(this.viewOptions, group, {
selected: true,
selected: false,
open: false,
itemsInFirstPosition: [],
firstRender: true,

View file

@ -25,7 +25,7 @@
b-dropdown-item(@click="sortBy = 'AZ'", :active="sortBy === 'AZ'") {{ $t('AZ') }}
div(
v-for="group in groups",
v-if="group.selected",
v-if="!anyFilterSelected || group.selected",
:key="group.key",
)
h2.mb-3
@ -225,7 +225,7 @@ const groups = [
return {
key: group,
quantity: 0,
selected: true,
selected: false,
classPrefix,
allowedItems,
};
@ -340,6 +340,10 @@ export default {
return itemsByType;
},
anyFilterSelected () {
return this.groups.some(g => g.selected);
},
},
methods: {
userHasPet (potionKey, eggKey) {
@ -465,8 +469,6 @@ export default {
let openedItem = result.data.data;
let text = this.content.gear.flat[openedItem.key].text();
this.drop(this.$t('messageDropMysteryItem', {dropText: text}), openedItem);
item.quantity--;
this.$forceUpdate();
} else {
this.$root.$emit('selectMembersModal::showItem', item);
}

View file

@ -69,7 +69,7 @@
span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
div(v-for="(petGroup, index) in petGroups",
v-if="viewOptions[petGroup.key].selected",
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected",
:key="petGroup.key")
h4(v-if="viewOptions[petGroup.key].animalCount !== 0") {{ petGroup.label }}
@ -92,7 +92,11 @@
@click="petClicked(item)"
)
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"')
| {{ $_openedItemRows_isToggled(petGroup.key) ? $t('showLess') : $t('showMore') }}
@ -103,7 +107,7 @@
span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
div(v-for="mountGroup in mountGroups",
v-if="viewOptions[mountGroup.key].selected",
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected",
:key="mountGroup.key")
h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
@ -469,7 +473,7 @@
petGroups.map((petGroup) => {
this.$set(this.viewOptions, petGroup.key, {
selected: true,
selected: false,
animalCount: 0,
});
});
@ -514,7 +518,7 @@
mountGroups.map((mountGroup) => {
this.$set(this.viewOptions, mountGroup.key, {
selected: true,
selected: false,
animalCount: 0,
});
});
@ -538,6 +542,9 @@
},
];
},
anyFilterSelected () {
return Object.values(this.viewOptions).some(g => g.selected);
},
},
methods: {
setShowMore (key) {

View file

@ -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")
span {{ characterLevel }}
.progress-container(v-b-tooltip.hover.bottom="$t('health')")
.svg-icon(v-html="icons.health")
.progress
.progress-bar.bg-health(:style="{width: `${percent(member.stats.hp, MAX_HEALTH)}%`}")
span.small-text {{member.stats.hp | statFloor}} / {{MAX_HEALTH}}
.progress-container(v-b-tooltip.hover.bottom="$t('experience')")
.svg-icon(v-html="icons.experience")
.progress
.progress-bar.bg-experience(:style="{width: `${percent(member.stats.exp, toNextLevel)}%`}")
span.small-text {{member.stats.exp | statFloor}} / {{toNextLevel}}
.progress-container(v-if="hasClass", v-b-tooltip.hover.bottom="$t('mana')")
.svg-icon(v-html="icons.mana")
.progress
.progress-bar.bg-mana(:style="{width: `${percent(member.stats.mp, maxMP)}%`}")
span.small-text {{member.stats.mp | statFloor}} / {{maxMP}}
stats-bar(
:icon="icons.health",
:value="member.stats.hp",
:maxValue="MAX_HEALTH",
:tooltip="$t('health')",
progressClass="bg-health",
:condensed="condensed"
)
stats-bar(
:icon="icons.experience",
:value="member.stats.exp",
:maxValue="toNextLevel",
:tooltip="$t('experience')",
progressClass="bg-experience",
: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>
<style lang="scss" scoped>
@ -99,44 +109,6 @@
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
.member-details.condensed.expanded {
background: $header-dark-background;
@ -163,25 +135,6 @@
border-bottom-left-radius: 4px;
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>
@ -190,6 +143,7 @@ import Avatar from './avatar';
import ClassBadge from './members/classBadge';
import { mapState } from 'client/libs/store';
import Profile from './userMenu/profile';
import StatsBar from './ui/statsbar';
import { toNextLevel } from '../../common/script/statHelpers';
import statsComputed from '../../common/script/libs/statsComputed';
@ -205,9 +159,7 @@ export default {
Avatar,
Profile,
ClassBadge,
},
directives: {
// bTooltip,
StatsBar,
},
props: {
member: {

View file

@ -1,41 +1,57 @@
<template lang="pug">
.row(:class="{'small-version': smallVersion}")
dl
template(v-if="quest.collect")
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('collect') + ':' }}
dd.col-8
.table-row(v-if="quest.collect")
dt {{ $t('collect') + ':' }}
dd
div(v-for="(collect, key) of quest.collect")
span {{ collect.count }} {{ getCollectText(collect) }}
template(v-if="quest.boss")
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('bossHP') + ':' }}
dd.col-8 {{ quest.boss.hp }}
.table-row(v-if="quest.boss")
dt {{ $t('bossHP') + ':' }}
dd {{ quest.boss.hp }}
dt(:class="smallVersion ? 'col-3' : 'col-4'") {{ $t('difficulty') + ':' }}
dd.col-8
.svg-icon.inline(
v-for="star of stars()", v-html="icons[star]",
:class="smallVersion ? 'icon-12' : 'icon-16'",
)
.table-row
dt {{ $t('difficulty') + ':' }}
dd
.svg-icon.inline(
v-for="star of stars()", v-html="icons[star]",
:class="smallVersion ? 'icon-12' : 'icon-16'",
)
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.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 {
font-size: 1.3em;
line-height: 1.2;
color: $gray-50;
}
.col-8 {
text-align: left;
}
.col-8:not(:last-child) {
margin-bottom: 4px;
}
.svg-icon {
margin-right: 4px;
}

View file

@ -37,8 +37,9 @@
draggable.sortable-tasks(
ref="tasksList",
@update='taskSorted',
@start="isDragging(true)",
@end="isDragging(false)",
:options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}',
class="sortable-tasks"
)
task(
v-for="task in taskList",
@ -49,12 +50,11 @@
:group='group',
)
template(v-if="hasRewardsList")
draggable(
draggable.reward-items(
ref="rewardsList",
@update="rewardSorted",
@start="rewardDragStart",
@end="rewardDragEnd",
class="reward-items",
)
shopItem(
v-for="reward in inAppRewards",
@ -76,6 +76,9 @@
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
/deep/ .draggable-cursor {
cursor: grabbing;
}
.tasks-column {
min-height: 556px;
@ -89,7 +92,6 @@
margin-top: 16px;
}
.reward-items {
@supports (display: grid) {
display: grid;
@ -335,6 +337,7 @@ export default {
showPopovers: true,
selectedItemToBuy: {},
dragging: false,
};
},
created () {
@ -521,9 +524,11 @@ export default {
rewardDragStart () {
// We need to stop popovers from interfering with our dragging
this.showPopovers = false;
this.isDragging(true);
},
rewardDragEnd () {
this.showPopovers = true;
this.isDragging(false);
},
quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
@ -678,6 +683,14 @@ export default {
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>

View file

@ -20,7 +20,7 @@
v-if="isUser && !isRunningYesterdailies",
:right="task.type === 'reward'",
ref="taskDropdown",
v-b-tooltip.hover.top="$t('showMore')"
v-b-tooltip.hover.top="$t('options')"
)
div(slot="dropdown-toggle", draggable=false)
.svg-icon.dropdown-icon(v-html="icons.menu")
@ -141,6 +141,11 @@
min-width: 0px;
overflow-wrap: break-word;
// markdown p-tag, can't find without /deep/
/deep/ p {
margin-bottom: 0;
}
&.has-notes {
padding-bottom: 4px;
}
@ -229,7 +234,7 @@
overflow-wrap: break-word;
&.has-checklist {
padding-bottom: 8px;
padding-bottom: 2px;
}
}
@ -285,7 +290,7 @@
margin-bottom: -3px;
min-height: 0px;
width: 100%;
margin-left: 8px;
margin-left: 0;
padding-right: 20px;
overflow-wrap: break-word;
@ -427,7 +432,7 @@
border-left: none;
}
.task-control, .reward-control {
.task-control:not(.task-disabled-habit-control-inner), .reward-control {
cursor: pointer;
}

View file

@ -12,7 +12,7 @@
.col-12.col-md-4.offset-md-4
.d-flex
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",
@click="toggleFilterPanel()",
:class="{active: selectedTags.length > 0}",
@ -110,6 +110,10 @@
padding-top: 16px;
}
.input-search, .search-button {
height: 40px;
}
.tasks-navigation {
margin-bottom: 20px;
}

View 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>

View file

@ -324,6 +324,7 @@
}
.progress-container > .progress {
border-radius: 1px;
background-color: $gray-500;
}
}
@ -371,8 +372,10 @@
.progress {
height: 8px;
border-radius: 1px;
.progress-bar {
border-radius: 1px;
background-color: $green-10 !important;
}
}
@ -495,6 +498,9 @@ export default {
async userId () {
this.loadUser();
},
userLoggedIn () {
this.loadUser();
},
},
methods: {
async loadUser () {

View file

@ -48,10 +48,18 @@ export async function like (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, {
comment: payload.comment,
});
return response.data.data;
}

View file

@ -5,6 +5,7 @@ import axios from 'axios';
import { togglePinnedItem as togglePinnedItemOp } from 'common/script/ops/pinnedGearUtils';
import changeClassOp from 'common/script/ops/changeClass';
import disableClassesOp from 'common/script/ops/disableClasses';
import openMysteryItemOp from 'common/script/ops/openMysteryItem';
export function fetch (store, options = {}) { // eslint-disable-line no-shadow
return loadAsyncResource({
@ -127,7 +128,9 @@ export function castSpell (store, params) {
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');
}

View file

@ -8,11 +8,14 @@ module.exports = {
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.',
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
groupIdRequired: 'req.params.groupId must contain a groupId.',
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.',
eventRequired: '"req.params.event" is required.',
@ -22,6 +25,4 @@ module.exports = {
missingCustomerId: 'Missing "req.query.customerId"',
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
missingSubKey: 'Missing "req.query.sub"',
messageIdRequired: '\"messageId\" must be a valid UUID.",',
};

View file

@ -23,6 +23,7 @@
"communityBug": "Submit Bug",
"communityExtensions": "<a href='http://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations' target='_blank'>Add-ons & Extensions</a>",
"communityFacebook": "Facebook",
"communityInstagram": "Instagram",
"communityFeature": "Request Feature",
"communityForum": "<a target='_blank' href='http://habitica.fandom.com/wiki/Special:Forum'>Forum</a>",
"communityKickstarter": "Kickstarter",

View file

@ -34,6 +34,7 @@
"saveEdits": "Save Edits",
"showMore": "Show More",
"showLess": "Show Less",
"options": "Options",
"expandToolbar": "Expand Toolbar",
"collapseToolbar": "Collapse Toolbar",

View file

@ -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.",
"abuseFlagModalButton": "Report Violation",
"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.",
"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.",
@ -230,9 +231,9 @@
"memberCannotRemoveYourself": "You cannot remove yourself!",
"groupMemberNotFound": "User not found among group's members",
"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.",
"inviteMissingUuid": "Missing user id in invite",
"inviteMissingUuid": "Missing User ID in invite",
"inviteMustNotBeEmpty": "Invite must not be empty.",
"partyMustbePrivate": "Parties must be private",
"userAlreadyInGroup": "UserID: <%= userId %>, User \"<%= username %>\" already in that group.",

View file

@ -45,7 +45,7 @@
"messageAuthEmailTaken": "Email already taken",
"messageAuthNoUserFound": "No user found.",
"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.",
"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!",
"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."
}

View file

@ -49,7 +49,7 @@ let api = {};
* @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 {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.name Display Name of leader.
* @apiSuccess {String} challenge.updatedAt Timestamp of last update.

View file

@ -30,12 +30,17 @@ const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email)
/**
* @apiDefine GroupIdRequired
* @apiError (404) {badRequest} groupIdRequired A group ID is required
* @apiError (400) {badRequest} groupIdRequired A group ID is required
*/
/**
* @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 = {};
@ -246,7 +251,7 @@ api.likeChat = {
let groupId = req.params.groupId;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
@ -285,7 +290,7 @@ api.likeChat = {
* @apiSuccess {Object} data.likes The likes of the message
* @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
* @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
*
* @apiUse GroupNotFound
@ -334,7 +339,7 @@ api.clearChatFlags = {
let chatId = req.params.chatId;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
@ -470,7 +475,7 @@ api.deleteChat = {
let chatId = req.params.chatId;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
req.checkParams('chatId', res.t('chatIdRequired')).notEmpty();
req.checkParams('chatId', apiError('chatIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;

View file

@ -941,11 +941,11 @@ api.removeGroupMember = {
* {"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"]
* }
* @apiParamExample {json} User Ids and Emails
* @apiParamExample {json} User IDs and Emails
* {
* "emails": [
* {"email": "user-1@example.com"},
@ -955,7 +955,7 @@ api.removeGroupMember = {
* }
*
* @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.
*
* @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": [
* { 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": [
* "user-1@example.com",
@ -987,9 +987,9 @@ api.removeGroupMember = {
* param `Array`.
* @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.
* @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} 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.
* @apiError (400) {BadRequest} ExceedsMembersLimit A max of 30 members can join a party.
*

View file

@ -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")
* @apiParam (Path) {UUID} heroId user ID
* @apiParam (Path) {UUID} heroId User ID
* @apiName UpdateHero
* @apiGroup Hall
* @apiPermission Admin

View 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;

View file

@ -1,6 +1,4 @@
import {
} from '../errors';
import { getUserInfo } from '../email';
import { getGroupUrl, getUserInfo } from '../email';
import { getAuthorEmailFromMessage } from '../chat';
export default class ChatReporter {
@ -11,25 +9,51 @@ export default class ChatReporter {
async validate () {}
async notify (group, message) {
const reporterEmailContent = getUserInfo(this.user, ['email']).email;
this.authorEmail = await getAuthorEmailFromMessage(message);
this.emailVariables = [
async getMessageVariables (group, message) {
const reporterEmail = getUserInfo(this.user, ['email']).email;
const authorVariables = await this.getAuthorVariables(message);
const groupUrl = getGroupUrl(group);
return [
{name: 'MESSAGE_TIME', content: (new Date(message.timestamp)).toString()},
{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_EMAIL', content: reporterEmailContent},
{name: 'REPORTER_EMAIL', content: reporterEmail},
{name: 'REPORTER_MODAL_URL', content: `/static/front/#?memberId=${this.user._id}`},
{name: 'AUTHOR_USERNAME', content: message.user},
{name: 'AUTHOR_UUID', content: message.uuid},
{name: 'AUTHOR_EMAIL', content: this.authorEmail},
{name: 'AUTHOR_MODAL_URL', content: `/static/front/#?memberId=${message.uuid}`},
...authorVariables,
{name: 'GROUP_NAME', content: group.name},
{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 () {
throw new Error('Flag must be implemented');
}

View file

@ -1,11 +1,10 @@
import GroupChatReporter from './groupChatReporter';
// import InboxChatReporter from './inboxChatReporter';
import InboxChatReporter from './inboxChatReporter';
export function chatReporterFactory (type, req, res) {
if (type === 'Group') {
return new GroupChatReporter(req, res);
} else if (type === 'Inbox') {
return new InboxChatReporter(req, res);
}
// else if (type === 'Inbox') {
// return new InboxChatReporter(req, res);
// }
}

View file

@ -6,7 +6,7 @@ import {
BadRequest,
NotFound,
} from '../errors';
import { getGroupUrl, sendTxn } from '../email';
import { sendTxn } from '../email';
import slack from '../slack';
import { model as Group } from '../../models/group';
import { chatModel as Chat } from '../../models/message';
@ -28,7 +28,7 @@ export default class GroupChatReporter extends ChatReporter {
async validate () {
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();
if (validationErrors) throw validationErrors;
@ -50,16 +50,12 @@ export default class GroupChatReporter extends ChatReporter {
}
async notify (group, message, userComment, automatedComment = '') {
await super.notify(group, message);
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},
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.sendFlagNotification({
authorEmail: this.authorEmail,

View 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;
}
}

View file

@ -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) {
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
if (!message) return false;

View file

@ -6,7 +6,7 @@ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// 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
// have a database of user records, the complete Facebook profile is serialized
// and deserialized.

View file

@ -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_SUBSCRIPTIONS_URL = nconf.get('SLACK_SUBSCRIPTIONS_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 subscriptionSlack;
@ -18,6 +22,26 @@ try {
subscriptionSlack = new IncomingWebhook(SLACK_SUBSCRIPTIONS_URL);
} catch (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 ({
@ -28,13 +52,13 @@ function sendFlagNotification ({
userComment,
automatedComment,
}) {
if (!SLACK_FLAGGING_URL) {
if (SKIP_FLAG_METHODS) {
return;
}
let titleLink;
let authorName;
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.>`;
if (userComment) {
@ -55,7 +79,12 @@ function sendFlagNotification ({
if (!message.user && message.uuid === 'system') {
authorName = 'System Message';
} 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`;
@ -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 ({
buyer,
recipient,
@ -84,7 +176,7 @@ function sendSubscriptionNotification ({
months,
groupId,
}) {
if (!SLACK_SUBSCRIPTIONS_URL) {
if (SKIP_SUB_METHOD) {
return;
}
let text;
@ -108,7 +200,7 @@ function sendSlurNotification ({
group,
message,
}) {
if (!SLACK_FLAGGING_URL) {
if (SKIP_FLAG_METHODS) {
return;
}
let titleLink;
@ -124,7 +216,12 @@ function sendSlurNotification ({
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({
text,
@ -143,5 +240,9 @@ function sendSlurNotification ({
}
module.exports = {
sendFlagNotification, sendSubscriptionNotification, sendSlurNotification,
sendFlagNotification,
sendInboxFlagNotification,
sendSubscriptionNotification,
sendSlurNotification,
formatUser,
};

View file

@ -408,7 +408,7 @@ function getInviteCount (uuids, emails) {
/**
* 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 res Express res object for use with translations
* @throws BadRequest An error describing the issue with the invitations