mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 05:37:22 +01:00
Merge branch 'develop' into client-monorepo
This commit is contained in:
60
test/api/unit/libs/highlightMentions.js
Normal file
60
test/api/unit/libs/highlightMentions.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import {
|
||||||
|
highlightMentions,
|
||||||
|
} from '../../../../website/server/libs/highlightMentions';
|
||||||
|
|
||||||
|
describe('highlightMentions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockFind = {
|
||||||
|
select () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
lean () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
exec () {
|
||||||
|
return Promise.resolve([{
|
||||||
|
auth: { local: { username: 'user' } }, _id: '111',
|
||||||
|
}, { auth: { local: { username: 'user2' } }, _id: '222' }, { auth: { local: { username: 'user3' } }, _id: '333' }, { auth: { local: { username: 'user-dash' } }, _id: '444' }, { auth: { local: { username: 'user_underscore' } }, _id: '555' },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sinon.stub(mongoose.Model, 'find').returns(mockFind);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesn\'t change text without mentions', async () => {
|
||||||
|
const text = 'some chat text';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal(text);
|
||||||
|
});
|
||||||
|
it('highlights existing users', async () => {
|
||||||
|
const text = '@user: message';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal('[@user](/profile/111): message');
|
||||||
|
});
|
||||||
|
it('highlights special characters', async () => {
|
||||||
|
const text = '@user-dash: message @user_underscore';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||||
|
});
|
||||||
|
it('doesn\'t highlight nonexisting users', async () => {
|
||||||
|
const text = '@nouser message';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal('@nouser message');
|
||||||
|
});
|
||||||
|
it('highlights multiple existing users', async () => {
|
||||||
|
const text = '@user message (@user2) @user3 @user';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal('[@user](/profile/111) message ([@user2](/profile/222)) [@user3](/profile/333) [@user](/profile/111)');
|
||||||
|
});
|
||||||
|
it('doesn\'t highlight more than 5 users', async () => {
|
||||||
|
const text = '@user @user2 @user3 @user4 @user5 @user6';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal(text);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -386,6 +386,7 @@ describe('User Model', () => {
|
|||||||
user = await user.save();
|
user = await user.save();
|
||||||
// verify that it's been awarded
|
// verify that it's been awarded
|
||||||
expect(user.achievements.beastMaster).to.equal(true);
|
expect(user.achievements.beastMaster).to.equal(true);
|
||||||
|
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
|
||||||
|
|
||||||
// reset the user
|
// reset the user
|
||||||
user.achievements.beastMasterCount = 0;
|
user.achievements.beastMasterCount = 0;
|
||||||
@@ -417,6 +418,28 @@ describe('User Model', () => {
|
|||||||
expect(user.achievements.beastMaster).to.not.equal(true);
|
expect(user.achievements.beastMaster).to.not.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds achievements to notification list', async () => {
|
||||||
|
let user = new User();
|
||||||
|
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||||
|
|
||||||
|
// Create conditions for achievements to be awarded
|
||||||
|
user.achievements.beastMasterCount = 3;
|
||||||
|
user.achievements.mountMasterCount = 3;
|
||||||
|
user.achievements.triadBingoCount = 3;
|
||||||
|
// verify that it was not awarded initially
|
||||||
|
expect(user.achievements.beastMaster).to.not.equal(true);
|
||||||
|
// verify that it was not awarded initially
|
||||||
|
expect(user.achievements.mountMaster).to.not.equal(true);
|
||||||
|
// verify that it was not awarded initially
|
||||||
|
expect(user.achievements.triadBingo).to.not.equal(true);
|
||||||
|
|
||||||
|
user = await user.save();
|
||||||
|
// verify that it's been awarded
|
||||||
|
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
|
||||||
|
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_MOUNT_MASTER')).to.exist;
|
||||||
|
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_TRIAD_BINGO')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
context('manage unallocated stats points notifications', () => {
|
context('manage unallocated stats points notifications', () => {
|
||||||
it('doesn\'t add a notification if there are no points to allocate', async () => {
|
it('doesn\'t add a notification if there are no points to allocate', async () => {
|
||||||
let user = new User();
|
let user = new User();
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ describe('POST /group/:groupId/join', () => {
|
|||||||
name: 'Testing Party',
|
name: 'Testing Party',
|
||||||
type: 'party',
|
type: 'party',
|
||||||
});
|
});
|
||||||
|
|
||||||
await leader.post(`/groups/${party._id}/invite`, {
|
await leader.post(`/groups/${party._id}/invite`, {
|
||||||
uuids: [member._id],
|
uuids: [member._id],
|
||||||
});
|
});
|
||||||
@@ -329,7 +330,9 @@ describe('POST /group/:groupId/join', () => {
|
|||||||
await leader.sync();
|
await leader.sync();
|
||||||
|
|
||||||
expect(member).to.have.nested.property('achievements.partyUp', true);
|
expect(member).to.have.nested.property('achievements.partyUp', true);
|
||||||
|
expect(member.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_UP')).to.exist;
|
||||||
expect(leader).to.have.nested.property('achievements.partyUp', true);
|
expect(leader).to.have.nested.property('achievements.partyUp', true);
|
||||||
|
expect(leader.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_UP')).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not award Party On achievement to party of size 2', async () => {
|
it('does not award Party On achievement to party of size 2', async () => {
|
||||||
@@ -353,7 +356,9 @@ describe('POST /group/:groupId/join', () => {
|
|||||||
await leader.sync();
|
await leader.sync();
|
||||||
|
|
||||||
expect(member).to.have.nested.property('achievements.partyOn', true);
|
expect(member).to.have.nested.property('achievements.partyOn', true);
|
||||||
|
expect(member.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_ON')).to.exist;
|
||||||
expect(leader).to.have.nested.property('achievements.partyOn', true);
|
expect(leader).to.have.nested.property('achievements.partyOn', true);
|
||||||
|
expect(leader.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_ON')).to.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<span v-if="msg.client && user.contributor.level >= 4">({{ msg.client }})</span>
|
<span v-if="msg.client && user.contributor.level >= 4">({{ msg.client }})</span>
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
|
ref="markdownContainer"
|
||||||
class="text"
|
class="text"
|
||||||
v-html="atHighlight(parseMarkdown(msg.text))"
|
v-html="atHighlight(parseMarkdown(msg.text))"
|
||||||
></div>
|
></div>
|
||||||
@@ -139,7 +140,6 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '~@/assets/scss/colors.scss';
|
||||||
@import '~@/assets/scss/tiers.scss';
|
|
||||||
|
|
||||||
.mentioned-icon {
|
.mentioned-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -313,6 +313,16 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||||
|
for (let i = 0; i < links.length; i += 1) {
|
||||||
|
const link = links[i];
|
||||||
|
if (links[i].getAttribute('href').startsWith('/profile/')) {
|
||||||
|
links[i].onclick = ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.$router.push({ path: link.getAttribute('href') });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
|
||||||
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||||
this.$emit('chat-card-mounted', this.msg.id);
|
this.$emit('chat-card-mounted', this.msg.id);
|
||||||
|
|||||||
@@ -563,7 +563,6 @@ export default {
|
|||||||
if (this.isParty) {
|
if (this.isParty) {
|
||||||
await this.$store.dispatch('party:getParty', true);
|
await this.$store.dispatch('party:getParty', true);
|
||||||
this.group = this.$store.state.party.data;
|
this.group = this.$store.state.party.data;
|
||||||
this.checkForAchievements();
|
|
||||||
} else {
|
} else {
|
||||||
const group = await this.$store.dispatch('guilds:getGroup', { groupId: this.searchId });
|
const group = await this.$store.dispatch('guilds:getGroup', { groupId: this.searchId });
|
||||||
this.$set(this, 'group', group);
|
this.$set(this, 'group', group);
|
||||||
@@ -584,21 +583,6 @@ export default {
|
|||||||
|
|
||||||
return this.user.notifications.some(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId);
|
return this.user.notifications.some(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId);
|
||||||
},
|
},
|
||||||
checkForAchievements () {
|
|
||||||
// Checks if user's party has reached 2 players for the first time.
|
|
||||||
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {
|
|
||||||
// @TODO
|
|
||||||
// User.set({'achievements.partyUp':true});
|
|
||||||
// Achievement.displayAchievement('partyUp');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if user's party has reached 4 players for the first time.
|
|
||||||
if (!this.user.achievements.partyOn && this.group.memberCount >= 4) {
|
|
||||||
// @TODO
|
|
||||||
// User.set({'achievements.partyOn':true});
|
|
||||||
// Achievement.displayAchievement('partyOn');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async join () {
|
async join () {
|
||||||
if (this.group.cancelledPlan && !window.confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
if (this.group.cancelledPlan && !window.confirm(this.$t('aboutToJoinCancelledGroupPlan'))) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -175,6 +175,43 @@ const NOTIFICATIONS = {
|
|||||||
label: $t => `${$t('achievement')}: ${$t('achievementAridAuthority')}`,
|
label: $t => `${$t('achievement')}: ${$t('achievementAridAuthority')}`,
|
||||||
modalId: 'generic-achievement',
|
modalId: 'generic-achievement',
|
||||||
},
|
},
|
||||||
|
ACHIEVEMENT_PARTY_UP: {
|
||||||
|
achievement: true,
|
||||||
|
label: $t => `${$t('achievement')}: ${$t('achievementPartyUp')}`,
|
||||||
|
modalId: 'generic-achievement',
|
||||||
|
},
|
||||||
|
ACHIEVEMENT_PARTY_ON: {
|
||||||
|
achievement: true,
|
||||||
|
label: $t => `${$t('achievement')}: ${$t('achievementPartyOn')}`,
|
||||||
|
modalId: 'generic-achievement',
|
||||||
|
},
|
||||||
|
ACHIEVEMENT_BEAST_MASTER: {
|
||||||
|
achievement: true,
|
||||||
|
label: $t => `${$t('achievement')}: ${$t('beastAchievement')}`,
|
||||||
|
modalId: 'generic-achievement',
|
||||||
|
data: {
|
||||||
|
message: $t => $t('achievement'),
|
||||||
|
modalText: $t => $t('mountAchievement'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ACHIEVEMENT_MOUNT_MASTER: {
|
||||||
|
achievement: true,
|
||||||
|
label: $t => `${$t('achievement')}: ${$t('mountAchievement')}`,
|
||||||
|
modalId: 'generic-achievement',
|
||||||
|
data: {
|
||||||
|
message: $t => $t('achievement'),
|
||||||
|
modalText: $t => $t('mountAchievement'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ACHIEVEMENT_TRIAD_BINGO: {
|
||||||
|
achievement: true,
|
||||||
|
label: $t => `${$t('achievement')}: ${$t('triadBingoAchievement')}`,
|
||||||
|
modalId: 'generic-achievement',
|
||||||
|
data: {
|
||||||
|
message: $t => $t('achievement'),
|
||||||
|
modalText: $t => $t('triadBingoAchievement'),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -229,7 +266,8 @@ export default {
|
|||||||
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
||||||
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
||||||
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||||
'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', 'GENERIC_ACHIEVEMENT',
|
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
|
||||||
|
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
|
||||||
].forEach(type => {
|
].forEach(type => {
|
||||||
handledNotifications[type] = true;
|
handledNotifications[type] = true;
|
||||||
});
|
});
|
||||||
@@ -380,21 +418,30 @@ export default {
|
|||||||
if (!config) {
|
if (!config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.achievement) {
|
if (config.achievement) {
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
} else if (config.sound) {
|
} else if (config.sound) {
|
||||||
this.playSound(config.sound);
|
this.playSound(config.sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data = {};
|
||||||
if (notification.data) {
|
if (notification.data) {
|
||||||
this.notificationData = notification.data;
|
data = notification.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.modalText && config.data.modalText) {
|
||||||
|
data.modalText = config.data.modalText(this.$t);
|
||||||
|
}
|
||||||
|
if (!data.message && config.data.message) {
|
||||||
|
data.message = config.data.message(this.$t);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notificationData = data;
|
||||||
if (forceToModal) {
|
if (forceToModal) {
|
||||||
this.$root.$emit('bv::show::modal', config.modalId);
|
this.$root.$emit('bv::show::modal', config.modalId);
|
||||||
} else {
|
} else {
|
||||||
this.text(config.label(this.$t), () => {
|
this.text(config.label(this.$t), () => {
|
||||||
|
this.notificationData = data;
|
||||||
this.$root.$emit('bv::show::modal', config.modalId);
|
this.$root.$emit('bv::show::modal', config.modalId);
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
@@ -619,6 +666,11 @@ export default {
|
|||||||
case 'ACHIEVEMENT_BACK_TO_BASICS':
|
case 'ACHIEVEMENT_BACK_TO_BASICS':
|
||||||
case 'ACHIEVEMENT_DUST_DEVIL':
|
case 'ACHIEVEMENT_DUST_DEVIL':
|
||||||
case 'ACHIEVEMENT_ARID_AUTHORITY':
|
case 'ACHIEVEMENT_ARID_AUTHORITY':
|
||||||
|
case 'ACHIEVEMENT_PARTY_UP':
|
||||||
|
case 'ACHIEVEMENT_PARTY_ON':
|
||||||
|
case 'ACHIEVEMENT_BEAST_MASTER':
|
||||||
|
case 'ACHIEVEMENT_MOUNT_MASTER':
|
||||||
|
case 'ACHIEVEMENT_TRIAD_BINGO':
|
||||||
case 'GENERIC_ACHIEVEMENT':
|
case 'GENERIC_ACHIEVEMENT':
|
||||||
this.showNotificationWithModal(notification);
|
this.showNotificationWithModal(notification);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -178,7 +178,6 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '~@/assets/scss/colors.scss';
|
||||||
@import '~@/assets/scss/tiers.scss';
|
|
||||||
|
|
||||||
.header-wrap {
|
.header-wrap {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
"achievementDustDevil": "Dust Devil",
|
"achievementDustDevil": "Dust Devil",
|
||||||
"achievementDustDevilText": "Has collected all Desert Pets.",
|
"achievementDustDevilText": "Has collected all Desert Pets.",
|
||||||
"achievementDustDevilModalText": "You collected all the Desert Pets!",
|
"achievementDustDevilModalText": "You collected all the Desert Pets!",
|
||||||
|
"achievementPartyUp": "You teamed up with a party member!",
|
||||||
"achievementAridAuthority": "Arid Authority",
|
"achievementAridAuthority": "Arid Authority",
|
||||||
"achievementAridAuthorityText": "Has tamed all Desert Mounts.",
|
"achievementAridAuthorityText": "Has tamed all Desert Mounts.",
|
||||||
"achievementAridAuthorityModalText": "You tamed all the Desert Mounts!",
|
"achievementAridAuthorityModalText": "You tamed all the Desert Mounts!",
|
||||||
"achievementKickstarter2019": "Pin Kickstarter Backer",
|
"achievementKickstarter2019": "Pin Kickstarter Backer",
|
||||||
"achievementKickstarter2019Text": "Backed the 2019 Pin Kickstarter Project"
|
"achievementKickstarter2019Text": "Backed the 2019 Pin Kickstarter Project",
|
||||||
|
"achievementPartyOn": "Your party grew to 4 members!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,10 @@
|
|||||||
"goToSettings": "Go to Settings",
|
"goToSettings": "Go to Settings",
|
||||||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||||
"usernameNotVerified": "Please confirm your username.",
|
"usernameNotVerified": "Please confirm your username.",
|
||||||
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging.",
|
"changeUsernameDisclaimer": "This username will be used for invitations, @mentions in chat, and messaging.",
|
||||||
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!"
|
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!",
|
||||||
|
"mentioning": "Mentioning",
|
||||||
|
"suggestMyUsername": "Suggest my username",
|
||||||
|
"everywhere": "Everywhere",
|
||||||
|
"onlyPrivateSpaces": "Only in private spaces"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import guildsAllowingBannedWords from '../../libs/guildsAllowingBannedWords';
|
|||||||
import { getMatchesByWordArray } from '../../libs/stringUtils';
|
import { getMatchesByWordArray } from '../../libs/stringUtils';
|
||||||
import bannedSlurs from '../../libs/bannedSlurs';
|
import bannedSlurs from '../../libs/bannedSlurs';
|
||||||
import apiError from '../../libs/apiError';
|
import apiError from '../../libs/apiError';
|
||||||
|
import { highlightMentions } from '../../libs/highlightMentions';
|
||||||
|
|
||||||
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true }));
|
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true }));
|
||||||
|
|
||||||
@@ -89,7 +90,6 @@ function getBannedWordsFromText (message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const mentionRegex = new RegExp('\\B@[-\\w]+', 'g');
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
|
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
|
||||||
* @apiName PostChat
|
* @apiName PostChat
|
||||||
@@ -187,6 +187,7 @@ api.postChat = {
|
|||||||
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [message, mentions, mentionedMembers] = await highlightMentions(req.body.message);
|
||||||
let client = req.headers['x-client'] || '3rd Party';
|
let client = req.headers['x-client'] || '3rd Party';
|
||||||
if (client) {
|
if (client) {
|
||||||
client = client.replace('habitica-', '');
|
client = client.replace('habitica-', '');
|
||||||
@@ -195,7 +196,6 @@ api.postChat = {
|
|||||||
let flagCount = 0;
|
let flagCount = 0;
|
||||||
if (group.privacy === 'public' && user.flags.chatShadowMuted) {
|
if (group.privacy === 'public' && user.flags.chatShadowMuted) {
|
||||||
flagCount = common.constants.CHAT_FLAG_FROM_SHADOW_MUTE;
|
flagCount = common.constants.CHAT_FLAG_FROM_SHADOW_MUTE;
|
||||||
const { message } = req.body;
|
|
||||||
|
|
||||||
// Email the mods
|
// Email the mods
|
||||||
const authorEmail = getUserInfo(user, ['email']).email;
|
const authorEmail = getUserInfo(user, ['email']).email;
|
||||||
@@ -228,7 +228,14 @@ api.postChat = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newChatMessage = group.sendChat({
|
const newChatMessage = group.sendChat({
|
||||||
message: req.body.message, user, flagCount, metaData: null, client, translate: res.t,
|
message: req.body.message,
|
||||||
|
user,
|
||||||
|
flagCount,
|
||||||
|
metaData: null,
|
||||||
|
client,
|
||||||
|
translate: res.t,
|
||||||
|
mentions,
|
||||||
|
mentionedMembers,
|
||||||
});
|
});
|
||||||
const toSave = [newChatMessage.save()];
|
const toSave = [newChatMessage.save()];
|
||||||
|
|
||||||
@@ -237,6 +244,7 @@ api.postChat = {
|
|||||||
toSave.push(user.save());
|
toSave.push(user.save());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await Promise.all(toSave);
|
await Promise.all(toSave);
|
||||||
|
|
||||||
const analyticsObject = {
|
const analyticsObject = {
|
||||||
@@ -248,7 +256,6 @@ api.postChat = {
|
|||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mentions = req.body.message.match(mentionRegex);
|
|
||||||
if (mentions) {
|
if (mentions) {
|
||||||
analyticsObject.mentionsCount = mentions.length;
|
analyticsObject.mentionsCount = mentions.length;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -630,16 +630,47 @@ api.joinGroup = {
|
|||||||
|
|
||||||
if (group.type === 'party' && inviter) {
|
if (group.type === 'party' && inviter) {
|
||||||
if (group.memberCount > 1) {
|
if (group.memberCount > 1) {
|
||||||
promises.push(User.update({
|
promises.push(User.update(
|
||||||
$or: [{ 'party._id': group._id }, { _id: user._id }],
|
{
|
||||||
'achievements.partyUp': { $ne: true },
|
$or: [{ 'party._id': group._id }, { _id: user._id }],
|
||||||
}, { $set: { 'achievements.partyUp': true } }, { multi: true }).exec());
|
'achievements.partyUp': { $ne: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: { 'achievements.partyUp': true },
|
||||||
|
$push: { notifications: { type: 'ACHIEVEMENT_PARTY_UP' } },
|
||||||
|
},
|
||||||
|
{ multi: true },
|
||||||
|
).exec());
|
||||||
|
|
||||||
|
if (inviter) {
|
||||||
|
if (inviter.achievements.partyUp !== true) {
|
||||||
|
// Since the notification list of the inviter is already
|
||||||
|
// updated in this save we need to add the notification here
|
||||||
|
inviter.addNotification('ACHIEVEMENT_PARTY_UP');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.memberCount > 3) {
|
if (group.memberCount > 3) {
|
||||||
promises.push(User.update({
|
promises.push(User.update(
|
||||||
$or: [{ 'party._id': group._id }, { _id: user._id }],
|
{
|
||||||
'achievements.partyOn': { $ne: true },
|
$or: [{ 'party._id': group._id }, { _id: user._id }],
|
||||||
}, { $set: { 'achievements.partyOn': true } }, { multi: true }).exec());
|
'achievements.partyOn': { $ne: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: { 'achievements.partyOn': true },
|
||||||
|
$push: { notifications: { type: 'ACHIEVEMENT_PARTY_ON' } },
|
||||||
|
},
|
||||||
|
{ multi: true },
|
||||||
|
).exec());
|
||||||
|
|
||||||
|
if (inviter) {
|
||||||
|
if (inviter.achievements.partyOn !== true) {
|
||||||
|
// Since the notification list of the inviter is already
|
||||||
|
// updated in this save we need to add the notification here
|
||||||
|
inviter.addNotification('ACHIEVEMENT_PARTY_ON');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||||
import common from '../../../common';
|
import common from '../../../common';
|
||||||
import { sentMessage } from '../../libs/inbox';
|
import { sentMessage } from '../../libs/inbox';
|
||||||
|
import { highlightMentions } from '../../libs/highlightMentions';
|
||||||
|
|
||||||
const { achievements } = common;
|
const { achievements } = common;
|
||||||
|
|
||||||
@@ -676,7 +677,7 @@ api.sendPrivateMessage = {
|
|||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
const sender = res.locals.user;
|
const sender = res.locals.user;
|
||||||
const { message } = req.body;
|
const message = (await highlightMentions(req.body.message))[0];
|
||||||
|
|
||||||
const receiver = await User.findById(req.body.toUserId).exec();
|
const receiver = await User.findById(req.body.toUserId).exec();
|
||||||
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
||||||
|
|||||||
@@ -17,15 +17,19 @@ export async function getAuthorEmailFromMessage (message) {
|
|||||||
return 'Author Account Deleted';
|
return 'Author Account Deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendChatPushNotifications (user, group, message, translate) {
|
export async function sendChatPushNotifications (user, group, message, mentions, translate) {
|
||||||
const members = await User.find({
|
const members = await User.find({
|
||||||
'party._id': group._id,
|
'party._id': group._id,
|
||||||
_id: { $ne: user._id },
|
_id: { $ne: user._id },
|
||||||
})
|
})
|
||||||
.select('preferences.pushNotifications preferences.language profile.name pushDevices')
|
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
members.forEach(member => {
|
members.forEach(member => {
|
||||||
if (member.preferences.pushNotifications.partyActivity !== false) {
|
if (member.preferences.pushNotifications.partyActivity !== false) {
|
||||||
|
if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
sendPushNotification(
|
sendPushNotification(
|
||||||
member,
|
member,
|
||||||
{
|
{
|
||||||
|
|||||||
23
website/server/libs/highlightMentions.js
Normal file
23
website/server/libs/highlightMentions.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { model as User } from '../models/user';
|
||||||
|
|
||||||
|
const mentionRegex = new RegExp('\\B@[-\\w]+', 'g');
|
||||||
|
|
||||||
|
export async function highlightMentions (text) { // eslint-disable-line import/prefer-default-export
|
||||||
|
const mentions = text.match(mentionRegex);
|
||||||
|
let members = [];
|
||||||
|
|
||||||
|
if (mentions !== null && mentions.length <= 5) {
|
||||||
|
const usernames = mentions.map(mention => mention.substr(1));
|
||||||
|
members = await User
|
||||||
|
.find({ 'auth.local.username': { $in: usernames }, 'flags.verifiedUsername': true })
|
||||||
|
.select(['auth.local.username', '_id', 'preferences.pushNotifications', 'pushDevices'])
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
members.forEach(member => {
|
||||||
|
const { username } = member.auth.local;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
text = text.replace(new RegExp(`@${username}(?![\\-\\w])`, 'g'), `[@${username}](/profile/${member._id})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [text, mentions, members];
|
||||||
|
}
|
||||||
@@ -541,9 +541,12 @@ schema.methods.getMemberCount = async function getMemberCount () {
|
|||||||
|
|
||||||
schema.methods.sendChat = function sendChat (options = {}) {
|
schema.methods.sendChat = function sendChat (options = {}) {
|
||||||
const {
|
const {
|
||||||
message, user, metaData, client, flagCount = 0, info = {}, translate,
|
message, user, metaData,
|
||||||
|
client, flagCount = 0, info = {},
|
||||||
|
translate, mentions, mentionedMembers,
|
||||||
} = options;
|
} = options;
|
||||||
const newMessage = messageDefaults(message, user, client, flagCount, info);
|
const newMessage = messageDefaults(message, user, client, flagCount, info);
|
||||||
|
|
||||||
let newChatMessage = new Chat();
|
let newChatMessage = new Chat();
|
||||||
newChatMessage = Object.assign(newChatMessage, newMessage);
|
newChatMessage = Object.assign(newChatMessage, newMessage);
|
||||||
newChatMessage.groupId = this._id;
|
newChatMessage.groupId = this._id;
|
||||||
@@ -609,9 +612,31 @@ schema.methods.sendChat = function sendChat (options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.type === 'party' && user) {
|
if (this.type === 'party' && user) {
|
||||||
sendChatPushNotifications(user, this, newChatMessage, translate);
|
sendChatPushNotifications(user, this, newChatMessage, mentions, translate);
|
||||||
|
}
|
||||||
|
if (mentionedMembers) {
|
||||||
|
mentionedMembers.forEach(member => {
|
||||||
|
if (member._id === user._id) return;
|
||||||
|
const pushNotifPrefs = member.preferences.pushNotifications;
|
||||||
|
if (this.type === 'party') {
|
||||||
|
if (pushNotifPrefs.mentionParty !== true || !this.isMember(member)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (this.isMember(member)) {
|
||||||
|
if (pushNotifPrefs.mentionJoinedGuild !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.privacy !== 'public') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pushNotifPrefs.mentionUnjoinedGuild !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendPushNotification(member, { identifier: 'chatMention', title: `${user.profile.name} mentioned you in ${this.name}`, message });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newChatMessage;
|
return newChatMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -190,23 +190,35 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
|||||||
// Determines if Beast Master should be awarded
|
// Determines if Beast Master should be awarded
|
||||||
const beastMasterProgress = common.count.beastMasterProgress(this.items.pets);
|
const beastMasterProgress = common.count.beastMasterProgress(this.items.pets);
|
||||||
|
|
||||||
if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) {
|
if (
|
||||||
|
(beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0)
|
||||||
|
&& this.achievements.beastMaster !== true
|
||||||
|
) {
|
||||||
this.achievements.beastMaster = true;
|
this.achievements.beastMaster = true;
|
||||||
|
this.addNotification('ACHIEVEMENT_BEAST_MASTER');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if Mount Master should be awarded
|
// Determines if Mount Master should be awarded
|
||||||
const mountMasterProgress = common.count.mountMasterProgress(this.items.mounts);
|
const mountMasterProgress = common.count.mountMasterProgress(this.items.mounts);
|
||||||
|
|
||||||
if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) {
|
if (
|
||||||
|
(mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0)
|
||||||
|
&& this.achievements.mountMaster !== true
|
||||||
|
) {
|
||||||
this.achievements.mountMaster = true;
|
this.achievements.mountMaster = true;
|
||||||
|
this.addNotification('ACHIEVEMENT_MOUNT_MASTER');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if Triad Bingo should be awarded
|
// Determines if Triad Bingo should be awarded
|
||||||
const dropPetCount = common.count.dropPetsCurrentlyOwned(this.items.pets);
|
const dropPetCount = common.count.dropPetsCurrentlyOwned(this.items.pets);
|
||||||
const qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90;
|
const qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90;
|
||||||
|
|
||||||
if (qualifiesForTriad || this.achievements.triadBingoCount > 0) {
|
if (
|
||||||
|
(qualifiesForTriad || this.achievements.triadBingoCount > 0)
|
||||||
|
&& this.achievements.triadBingo !== true
|
||||||
|
) {
|
||||||
this.achievements.triadBingo = true;
|
this.achievements.triadBingo = true;
|
||||||
|
this.addNotification('ACHIEVEMENT_TRIAD_BINGO');
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXAMPLE CODE for allowing all existing and new players to be
|
// EXAMPLE CODE for allowing all existing and new players to be
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ const NOTIFICATION_TYPES = [
|
|||||||
'ACHIEVEMENT_MIND_OVER_MATTER',
|
'ACHIEVEMENT_MIND_OVER_MATTER',
|
||||||
'ACHIEVEMENT_DUST_DEVIL',
|
'ACHIEVEMENT_DUST_DEVIL',
|
||||||
'ACHIEVEMENT_ARID_AUTHORITY',
|
'ACHIEVEMENT_ARID_AUTHORITY',
|
||||||
|
'ACHIEVEMENT_PARTY_UP',
|
||||||
|
'ACHIEVEMENT_PARTY_ON',
|
||||||
|
'ACHIEVEMENT_BEAST_MASTER',
|
||||||
|
'ACHIEVEMENT_MOUNT_MASTER',
|
||||||
|
'ACHIEVEMENT_TRIAD_BINGO',
|
||||||
];
|
];
|
||||||
|
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|||||||
Reference in New Issue
Block a user