Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve @mention handling #10872

Merged
merged 86 commits into from Oct 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
7d29ae4
move the update username route to v3 (#10836)
paglias Nov 14, 2018
39761bd
Add API Call to retrieve auto-complete options for usernames
phillipthelen Nov 15, 2018
aae2fb1
Create links to users profile in chat messages
phillipthelen Nov 15, 2018
e9c8662
Begin adding server-side autocomplete to web client
phillipthelen Nov 26, 2018
0271d98
Add Test to opt out of username being searchable
phillipthelen Nov 27, 2018
f65240c
Fix issue with username highlighting
phillipthelen Nov 27, 2018
ffa2ea4
Correctly update message text when using autocomplete
phillipthelen Nov 27, 2018
30ebdca
remove old autocomplete component
phillipthelen Nov 27, 2018
a450ac3
Improve chat input design
phillipthelen Nov 27, 2018
0b7a60e
rewrite mongoose migration to avoid using recursion
paglias Nov 9, 2018
8d93d6b
fixes
paglias Nov 9, 2018
e8b7dbb
select more fields
paglias Nov 9, 2018
577ff4c
use lean and .update
paglias Nov 9, 2018
dd2c2f6
fix(tests): correct expects
SabreCat Nov 14, 2018
662e642
fix(tests): linting & more expects
SabreCat Nov 14, 2018
e548074
chore(news): Bailey
SabreCat Nov 14, 2018
2598a60
chore(i18n): update locales
SabreCat Nov 14, 2018
793af2a
4.70.0
SabreCat Nov 14, 2018
d031a4c
fix(chat): less intrusive highlight and better margins
SabreCat Nov 14, 2018
d0b72d5
fix(chat): more width tweakage
SabreCat Nov 14, 2018
62bcd87
feat(content): Oddballs Bundle
SabreCat Nov 15, 2018
b49d2e4
chore(sprites): compile
SabreCat Nov 15, 2018
9994770
chore(i18n): update locales
SabreCat Nov 15, 2018
15e2e2a
4.71.0
SabreCat Nov 15, 2018
f5a2cf6
groupChatReceived webhook fix (#10802)
natfarleydev Nov 17, 2018
f5b6291
Set width on .custom-control-label (#10840)
ianoxley Nov 17, 2018
5028b27
Very large Guild member counts overflow the badge #10753 (#10812)
Dimini Nov 17, 2018
e0b78a3
Update superagent to the latest version 🚀 (#10848)
greenkeeper[bot] Nov 18, 2018
42a4bbe
fix(chat): prevent duplicate messages, closes #10823
paglias Nov 18, 2018
1ccb700
Fix for #10814, prevent ParallelSave errors (#10852)
paglias Nov 18, 2018
b2095bd
move computed-props to methods - refactor mountItem to use the states…
negue Nov 20, 2018
d661c72
feat(content): Frost Hatching Potions
SabreCat Nov 20, 2018
d3f964c
chore(sprites): compile
SabreCat Nov 20, 2018
6f86600
chore(i18n): update locales
SabreCat Nov 20, 2018
bfa3f9a
4.72.0
SabreCat Nov 20, 2018
4417787
fix(stable): remove progress number from petItem
paglias Nov 21, 2018
21f14d5
add two slurs - TRIGGER / CONTENT WARNING: assault, slurs, swearwords…
Alys Nov 21, 2018
1562c69
more checks on the item.klass, also added the specialClass checks (#1…
negue Nov 22, 2018
916d930
feat(content): Turkey Day 2018
SabreCat Nov 22, 2018
be92038
chore(sprites): compile
SabreCat Nov 22, 2018
2066f12
chore(i18n): update locales
SabreCat Nov 22, 2018
f9fdab7
4.73.0
SabreCat Nov 22, 2018
46a4948
chore(i18n): update locales
SabreCat Nov 23, 2018
80ef4a3
4.73.1
SabreCat Nov 23, 2018
a0b1732
feat(footer): always show expanded footer (#10862)
paglias Nov 26, 2018
03763f4
Fixes issue #10857 ("Tags have extra space at the bottom when they sh…
natez56 Nov 26, 2018
aac23f3
Attach client to chat messages (#10845)
phillipthelen Nov 26, 2018
8c12080
chore(event): end Thanksgiving tweaks
SabreCat Nov 26, 2018
b355b3d
chore(i18n): update locales
SabreCat Nov 26, 2018
0e262dc
4.73.2
SabreCat Nov 26, 2018
cde279e
Improve chat input design
phillipthelen Nov 27, 2018
024e93e
Fix test errors
phillipthelen Nov 27, 2018
664a457
Merge branch 'develop' into phillip/autocomplete-username
phillipthelen Nov 27, 2018
8641a98
Merge branch 'develop' into phillip/autocomplete-username
phillipthelen Nov 28, 2018
c3d9ac6
Move tier icons import to index
phillipthelen Feb 5, 2019
548c68f
correctly name event variable
phillipthelen Feb 5, 2019
59f08bc
Debounce autocomplete calls
phillipthelen Feb 5, 2019
8618913
Merge branch 'develop' of https://github.com/HabitRPG/habitica into a…
phillipthelen Feb 5, 2019
82863bd
optimize mention highlighting
phillipthelen Feb 6, 2019
e9d5123
fix failing tests
phillipthelen Feb 7, 2019
59dbd12
Fix sending private messages
phillipthelen Feb 8, 2019
bb674d1
Cache username autocomplete requests
phillipthelen Feb 12, 2019
3fec6fc
optimize autocomplete regex
phillipthelen Feb 12, 2019
be5137d
Merge branch 'develop' into phillip/autocomplete-username
phillipthelen Feb 12, 2019
328ecb2
Fix lint error
phillipthelen Feb 12, 2019
718ea92
add optional parameters to limit autocompletion to specific group
phillipthelen Feb 14, 2019
076e1b8
Merge branch 'develop' into phillip/autocomplete-username
phillipthelen Feb 14, 2019
af510ce
Fix non-profile urls not being usable.
phillipthelen Feb 14, 2019
1173a9b
Correctly handle autocomplete for public and private guilds
phillipthelen Feb 15, 2019
071dffe
Add check to make sure users don’t search for parties/guilds they are…
phillipthelen Feb 28, 2019
601811e
fix lint error
phillipthelen Feb 28, 2019
5e50e98
limit autocomplete results to 5
phillipthelen Feb 28, 2019
4a66e2f
fix(mentioning): change default, adapt settings control to checkbox
SabreCat Mar 1, 2019
51f66af
Improve auto completing
phillipthelen Mar 6, 2019
2dca214
improve username autocomplete
phillipthelen Apr 22, 2019
6523b6b
Merge branch 'develop' of https://github.com/HabitRPG/habitica into a…
phillipthelen Sep 19, 2019
a629ec5
Fix merge issue
phillipthelen Sep 19, 2019
ed266ad
remove old code
phillipthelen Sep 19, 2019
0072a39
Send push notifications on mentions
phillipthelen Sep 30, 2019
77b1888
Merge branch 'develop' of https://github.com/HabitRPG/habitica into a…
phillipthelen Oct 1, 2019
2d3f250
Improve handling for sending mention notifications
phillipthelen Oct 1, 2019
e278196
Fix lint error
phillipthelen Oct 1, 2019
3a6c296
Update schema.js
phillipthelen Oct 14, 2019
4cccaf3
Fix failing test
phillipthelen Oct 14, 2019
0d90105
Don't send push notification to users who aren't in the party
phillipthelen Oct 15, 2019
7cd59ce
Remove tributejs from dependencies
phillipthelen Oct 16, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions test/api/unit/libs/highlightMentions.js
@@ -0,0 +1,64 @@
import {
highlightMentions,
} from '../../../../website/server/libs/highlightMentions';
import mongoose from 'mongoose';

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 () => {
let text = 'some chat text';
let result = await highlightMentions(text);
expect(result[0]).to.equal(text);
});
it('highlights existing users', async () => {
let text = '@user: message';
let result = await highlightMentions(text);
expect(result[0]).to.equal('[@user](/profile/111): message');
});
it('highlights special characters', async () => {
let text = '@user-dash: message @user_underscore';
let 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 () => {
let text = '@nouser message';
let result = await highlightMentions(text);
expect(result[0]).to.equal('@nouser message');
});
it('highlights multiple existing users', async () => {
let text = '@user message (@user2) @user3 @user';
let 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 () => {
let text = '@user @user2 @user3 @user4 @user5 @user6';
let result = await highlightMentions(text);
expect(result[0]).to.equal(text);
});
});
23 changes: 16 additions & 7 deletions website/client/components/chat/chatCard.vue
Expand Up @@ -9,7 +9,7 @@ div
span.mr-1(v-if="msg.username") •
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))')
.text(v-html='atHighlight(parseMarkdown(msg.text))', ref='markdownContainer')
.reported(v-if="isMessageReported && (inbox === true)")
span(v-once) {{ $t('reportedMessage')}}
br
Expand Down Expand Up @@ -47,7 +47,6 @@ div

<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/tiers.scss';

.mentioned-icon {
width: 16px;
Expand Down Expand Up @@ -217,6 +216,21 @@ export default {
return 'Message hidden (shadow-muted)';
},
},
mounted () {
const links = this.$refs.markdownContainer.getElementsByTagName('a');
for (let i = 0; i < links.length; i++) {
const link = links[i];
if (links[i].getAttribute('href').startsWith('/profile/')) {
paglias marked this conversation as resolved.
Show resolved Hide resolved
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_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
this.$emit('chat-card-mounted', this.msg.id);
},
methods: {
async like () {
let message = cloneDeep(this.msg);
Expand Down Expand Up @@ -279,10 +293,5 @@ export default {
return habiticaMarkdown.render(String(text));
},
},
mounted () {
this.CHAT_FLAG_LIMIT_FOR_HIDING = CHAT_FLAG_LIMIT_FOR_HIDING;
this.CHAT_FLAG_FROM_SHADOW_MUTE = CHAT_FLAG_FROM_SHADOW_MUTE;
this.$emit('chat-card-mounted', this.msg.id);
},
};
</script>
1 change: 0 additions & 1 deletion website/client/components/userMenu/inbox.vue
Expand Up @@ -76,7 +76,6 @@

<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/tiers.scss';

.header-wrap {
padding: 0.5em;
Expand Down
8 changes: 6 additions & 2 deletions website/common/locales/en/settings.json
Expand Up @@ -205,6 +205,10 @@
"goToSettings": "Go to Settings",
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
"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.",
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!"
"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!",
"mentioning": "Mentioning",
"suggestMyUsername": "Suggest my username",
"everywhere": "Everywhere",
"onlyPrivateSpaces": "Only in private spaces"
}
15 changes: 11 additions & 4 deletions website/server/controllers/api-v3/chat.js
Expand Up @@ -19,6 +19,7 @@ import guildsAllowingBannedWords from '../../libs/guildsAllowingBannedWords';
import { getMatchesByWordArray } from '../../libs/stringUtils';
import bannedSlurs from '../../libs/bannedSlurs';
import apiError from '../../libs/apiError';
import {highlightMentions} from '../../libs/highlightMentions';

const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
return { email, canSend: true };
Expand Down Expand Up @@ -90,7 +91,6 @@ function getBannedWordsFromText (message) {
}


const mentionRegex = new RegExp('\\B@[-\\w]+', 'g');
/**
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
* @apiName PostChat
Expand Down Expand Up @@ -183,6 +183,7 @@ api.postChat = {
throw new NotAuthorized(res.t('messageGroupChatSpam'));
}

const [message, mentions, mentionedMembers] = await highlightMentions(req.body.message);
let client = req.headers['x-client'] || '3rd Party';
if (client) {
client = client.replace('habitica-', '');
Expand All @@ -191,7 +192,6 @@ api.postChat = {
let flagCount = 0;
if (group.privacy === 'public' && user.flags.chatShadowMuted) {
flagCount = common.constants.CHAT_FLAG_FROM_SHADOW_MUTE;
let message = req.body.message;

// Email the mods
let authorEmail = getUserInfo(user, ['email']).email;
Expand Down Expand Up @@ -223,14 +223,22 @@ api.postChat = {
});
}

const newChatMessage = group.sendChat({message: req.body.message, user, flagCount, metaData: null, client, translate: res.t});
const newChatMessage = group.sendChat({message: req.body.message,
user,
flagCount,
metaData: null,
client,
translate: res.t,
mentions,
mentionedMembers});
let toSave = [newChatMessage.save()];

if (group.type === 'party') {
user.party.lastMessageSeen = newChatMessage.id;
toSave.push(user.save());
}


await Promise.all(toSave);

let analyticsObject = {
Expand All @@ -242,7 +250,6 @@ api.postChat = {
headers: req.headers,
};

const mentions = req.body.message.match(mentionRegex);
if (mentions) {
analyticsObject.mentionsCount = mentions.length;
} else {
Expand Down
3 changes: 2 additions & 1 deletion website/server/controllers/api-v3/members.js
Expand Up @@ -21,6 +21,7 @@ import {
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
import { achievements } from '../../../../website/common/';
import {sentMessage} from '../../libs/inbox';
import {highlightMentions} from '../../libs/highlightMentions';

let api = {};

Expand Down Expand Up @@ -633,7 +634,7 @@ api.sendPrivateMessage = {
if (validationErrors) throw validationErrors;

const sender = res.locals.user;
const message = req.body.message;
const message = (await highlightMentions(req.body.message))[0];

const receiver = await User.findById(req.body.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userNotFound'));
Expand Down
7 changes: 5 additions & 2 deletions website/server/libs/chat.js
Expand Up @@ -18,15 +18,18 @@ export async function getAuthorEmailFromMessage (message) {
}
}

export async function sendChatPushNotifications (user, group, message, translate) {
export async function sendChatPushNotifications (user, group, message, mentions, translate) {
let members = await User.find({
'party._id': group._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();
members.forEach(member => {
if (member.preferences.pushNotifications.partyActivity !== false) {
if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) {
return;
}
sendPushNotification(
member,
{
Expand Down
23 changes: 23 additions & 0 deletions website/server/libs/highlightMentions.js
@@ -0,0 +1,23 @@
import {model as User} from '../models/user';

const mentionRegex = new RegExp('\\B@[-\\w]+', 'g');

export async function highlightMentions (text) {
const mentions = text.match(mentionRegex);
phillipthelen marked this conversation as resolved.
Show resolved Hide resolved
let members = [];
if (mentions !== null && mentions.length <= 5) {
const usernames = mentions.map((mention) => {
return 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.username;
text = text.replace(new RegExp(`@${username}(?![\\-\\w])`, 'g'), `[@${username}](/profile/${member._id})`);
});
}
return [text, mentions, members];
}
28 changes: 25 additions & 3 deletions website/server/models/group.js
Expand Up @@ -513,7 +513,7 @@ schema.methods.getMemberCount = async function getMemberCount () {
};

schema.methods.sendChat = function sendChat (options = {}) {
const {message, user, metaData, client, flagCount = 0, info = {}, translate} = options;
const {message, user, metaData, client, flagCount = 0, info = {}, translate, mentions, mentionedMembers} = options;
let newMessage = messageDefaults(message, user, client, flagCount, info);
let newChatMessage = new Chat();
newChatMessage = Object.assign(newChatMessage, newMessage);
Expand Down Expand Up @@ -576,9 +576,31 @@ schema.methods.sendChat = function sendChat (options = {}) {
});

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

Expand Down
3 changes: 3 additions & 0 deletions website/server/models/user/schema.js
Expand Up @@ -504,6 +504,9 @@ let schema = new Schema({
questStarted: {$type: Boolean, default: true},
invitedQuest: {$type: Boolean, default: true},
majorUpdates: {$type: Boolean, default: true},
mentionParty: {$type: Boolean, default: true},
mentionJoinedGuild: {$type: Boolean, default: true},
mentionUnjoinedGuild: {$type: Boolean, default: true},
partyActivity: {$type: Boolean, default: true},
},
suppressModals: {
Expand Down