mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-07-18 09:52:20 +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: {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
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
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
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 () {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
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 {
|
||||
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 () {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"saveEdits": "Save Edits",
|
||||
"showMore": "Show More",
|
||||
"showLess": "Show Less",
|
||||
"options": "Options",
|
||||
|
||||
"expandToolbar": "Expand 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.",
|
||||
"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.",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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 {
|
||||
} 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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
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) {
|
||||
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
|
||||
if (!message) return false;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue