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

More improvements on send notifications logic #10736

Merged
merged 9 commits into from
May 11, 2018
7 changes: 7 additions & 0 deletions packages/rocketchat-api/server/v1/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,13 @@ RocketChat.API.v1.addRoute('users.setPreferences', { authRequired: true }, {
preferences = _.extend({ _id: userId, settings: { preferences: this.bodyParams.data } });
}

// Keep compatibility with old values
if (preferences.emailNotificationMode === 'all') {
Copy link

@i4x i4x Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But sometimes we do need 'all', not just 'mentions'. People should be able to get notifications when they are not on-line, even when they are not mentioned. For example, people who don't have RocketChat client, and we want to use e-mail as notifications, for important channels

Copy link

@davemecha davemecha Nov 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be so great to have email notifications for all messages due to push-notifications issues on some rocket.chat instances, like on wechange.de. With this I could create a mail-filter and forward all mails to IFTTT, that triggers a push notification on the phone. - It would be even greater to have an option to change the mail content for email-notifications to minimal, where mail body holds just the deep link to the rocket.chat conversation and the subject holds the channel title (with some prefix, like the rocket.chat domain or something like that).
With this it would be quite easy to set up a IFTTT rule for getting push-notifications per user via email notifications without having the rocket.chat host to set up its own relay for push notifications. And users could get up to 750 notifications for free with the free tier of IFTTT.

preferences.emailNotificationMode = 'mentions';
} else if (preferences.emailNotificationMode === 'disabled') {
preferences.emailNotificationMode = 'nothing';
}

Meteor.runAsUser(this.userId, () => RocketChat.saveUser(this.userId, preferences));

return RocketChat.API.v1.success({ user: RocketChat.models.Users.findOneById(this.bodyParams.userId, { fields: preferences }) });
Expand Down
2 changes: 1 addition & 1 deletion packages/rocketchat-emoji-emojione/server/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* globals emojione */
Meteor.startup(function() {
RocketChat.callbacks.add('beforeNotifyUser', (message) => {
RocketChat.callbacks.add('beforeSendMessageNotifications', (message) => {
return emojione.shortnameToUnicode(message);
});
});
3 changes: 2 additions & 1 deletion packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,7 @@
"Government": "Government",
"Group_by_Type": "Group by Type",
"Group_favorites": "Group favorites",
"Group_mentions_disabled_x_members": "Group mentions `@all` and `@here` have been disabled for rooms with more than __total__ members.",
"Group_mentions_only": "Group mentions only",
"Guest_Pool": "Guest Pool",
"Hash": "Hash",
Expand Down Expand Up @@ -1398,7 +1399,7 @@
"Message_Ignored": "This message was ignored",
"Message_info": "Message info",
"Message_KeepHistory": "Keep Per Message Editing History",
"Message_MaxAll": "Maximum Channel Size for ALL Message",
"Message_MaxAll": "Maximum Channel Size for Group Mentions (@all and @here)",
"Message_MaxAllowedSize": "Maximum Allowed Characters Per Message",
"Message_pinning": "Message pinning",
"Message_QuoteChainLimit": "Maximum Number of Chained Quotes",
Expand Down
44 changes: 12 additions & 32 deletions packages/rocketchat-lib/server/functions/notifications/desktop.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,31 @@
import { parseMessageText } from './index';

/**
* Replaces @username with full name
*
* @param {string} message The message to replace
* @param {object[]} mentions Array of mentions used to make replacements
*
* @returns {string}
*/
function replaceMentionedUsernamesWithFullNames(message, mentions) {
if (!mentions || !mentions.length) {
return message;
}
mentions.forEach((mention) => {
const user = RocketChat.models.Users.findOneById(mention._id);
if (user && user.name) {
message = message.replace(`@${ mention.username }`, user.name);
}
});
return message;
}

/**
* Send notification to user
*
* @param {string} userId The user to notify
* @param {object} user The sender
* @param {object} room The room send from
* @param {object} message The message object
* @param {number} duration Duration of notification
* @param {string} notificationMessage The message text to send on notification body
*/
export function notifyDesktopUser(userId, user, message, room, duration) {

export function notifyDesktopUser({
userId,
user,
message,
room,
duration,
notificationMessage
}) {
const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true;
message.msg = parseMessageText(message, userId);

if (UI_Use_Real_Name) {
message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions);
}

let title = '';
let text = '';
if (room.t === 'd') {
title = UI_Use_Real_Name ? user.name : `@${ user.username }`;
text = message.msg;
text = notificationMessage;
} else if (room.name) {
title = `#${ room.name }`;
text = `${ UI_Use_Real_Name ? user.name : user.username }: ${ message.msg }`;
text = `${ UI_Use_Real_Name ? user.name : user.username }: ${ notificationMessage }`;
} else {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function shouldNotifyEmail({
}

// default server preference is disabled
if (RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'disabled') {
if (RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'nothing') {
return false;
}
}
Expand Down
32 changes: 25 additions & 7 deletions packages/rocketchat-lib/server/functions/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,34 @@ import s from 'underscore.string';
*
* @param {object} message the message to be parsed
*/
export function parseMessageText(message, userId) {
const user = RocketChat.models.Users.findOneById(userId);
const lng = user && user.language || RocketChat.settings.get('language') || 'en';

export function parseMessageTextPerUser(message, receiver) {
if (!message.msg && message.attachments && message.attachments[0]) {
message.msg = message.attachments[0].image_type ? TAPi18n.__('User_uploaded_image', {lng}) : TAPi18n.__('User_uploaded_file', {lng});
const lng = receiver.language || RocketChat.settings.get('language') || 'en';

return message.attachments[0].image_type ? TAPi18n.__('User_uploaded_image', {lng}) : TAPi18n.__('User_uploaded_file', {lng});
}
message.msg = RocketChat.callbacks.run('beforeNotifyUser', message.msg);

return message.msg;
return message;
}

/**
* Replaces @username with full name
*
* @param {string} message The message to replace
* @param {object[]} mentions Array of mentions used to make replacements
*
* @returns {string}
*/
export function replaceMentionedUsernamesWithFullNames(message, mentions) {
if (!mentions || !mentions.length) {
return message;
}
mentions.forEach((mention) => {
if (mention.name) {
message = message.replace(new RegExp(s.escapeRegExp(`@${ mention.username }`), 'g'), mention.name);
}
});
return message;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { parseMessageText } from './index';

const CATEGORY_MESSAGE = 'MESSAGE';
const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY';

Expand All @@ -20,7 +18,7 @@ function canSendMessageToRoom(room, username) {
return !((room.muted || []).includes(username));
}

export function sendSinglePush({ room, message, userId, receiverUsername, senderUsername }) {
export function sendSinglePush({ room, message, userId, receiverUsername, senderUsername, notificationMessage }) {
RocketChat.PushNotification.send({
roomId: message.rid,
payload: {
Expand All @@ -32,7 +30,7 @@ export function sendSinglePush({ room, message, userId, receiverUsername, sender
},
roomName: RocketChat.settings.get('Push_show_username_room') ? `#${ RocketChat.roomTypes.getRoomName(room.t, room) }` : '',
username: RocketChat.settings.get('Push_show_username_room') ? senderUsername : '',
message: RocketChat.settings.get('Push_show_message') ? parseMessageText(message, userId) : ' ',
message: RocketChat.settings.get('Push_show_message') ? notificationMessage : ' ',
// badge: getBadgeCount(userIdToNotify),
usersTo: {
userId
Expand Down
14 changes: 6 additions & 8 deletions packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ function notifyUsersOnMessage(message, room) {
let toHere = false;
const mentionIds = [];
const highlightsIds = [];
const highlights = RocketChat.models.Users.findUsersByUsernamesWithHighlights(room.usernames, { fields: { '_id': 1, 'settings.preferences.highlights': 1 }}).fetch();

const highlights = RocketChat.models.Subscriptions.findByRoomWithUserHighlights(room._id, { fields: {'userHighlights': 1, 'u._id': 1 }}).fetch();
if (message.mentions != null) {
message.mentions.forEach(function(mention) {
if (!toAll && mention._id === 'all') {
Expand All @@ -61,11 +60,10 @@ function notifyUsersOnMessage(message, room) {
});
}

highlights.forEach(function(user) {
const userHighlights = RocketChat.getUserPreference(user, 'highlights');
if (userHighlights && messageContainsHighlight(message, userHighlights)) {
if (user._id !== message.u._id) {
highlightsIds.push(user._id);
highlights.forEach(function(subscription) {
if (subscription.userHighlights && messageContainsHighlight(message, subscription.userHighlights)) {
if (subscription.u._id !== message.u._id) {
highlightsIds.push(subscription.u._id);
}
}
});
Expand Down Expand Up @@ -103,8 +101,8 @@ function notifyUsersOnMessage(message, room) {
}

// Update all the room activity tracker fields
// This method take so long to execute on gient rooms cuz it will trugger the cache rebuild for the releations of that room
RocketChat.models.Rooms.incMsgCountAndSetLastMessageById(message.rid, 1, message.ts, RocketChat.settings.get('Store_Last_Message') && message);

// Update all other subscriptions to alert their owners but witout incrementing
// the unread counter, as it is only for mentions and direct messages
// We now set alert and open properties in two separate update commands. This proved to be more efficient on MongoDB - because it uses a more efficient index.
Expand Down
88 changes: 64 additions & 24 deletions packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import moment from 'moment';

import { callJoinRoom, messageContainsHighlight } from '../functions/notifications/';
import { callJoinRoom, messageContainsHighlight, parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../functions/notifications/';
import { sendEmail, shouldNotifyEmail } from '../functions/notifications/email';
import { sendSinglePush, shouldNotifyMobile } from '../functions/notifications/mobile';
import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop';
Expand All @@ -12,6 +12,7 @@ const sendNotification = ({
hasMentionToAll,
hasMentionToHere,
message,
notificationMessage,
room,
mentionIds,
disableAllMessageNotifications
Expand Down Expand Up @@ -45,7 +46,9 @@ const sendNotification = ({
return;
}

const isHighlighted = messageContainsHighlight(message, receiver.settings && receiver.settings.preferences && receiver.settings.preferences.highlights);
notificationMessage = parseMessageTextPerUser(notificationMessage, receiver);

const isHighlighted = messageContainsHighlight(message, subscription.userHighlights);

const {
audioNotifications,
Expand Down Expand Up @@ -80,7 +83,14 @@ const sendNotification = ({
hasMentionToUser
})) {
notificationSent = true;
notifyDesktopUser(subscription.u._id, sender, message, room, subscription.desktopNotificationDuration);
notifyDesktopUser({
notificationMessage,
userId: subscription.u._id,
user: sender,
message,
room,
duration: subscription.desktopNotificationDuration
});
}

if (shouldNotifyMobile({
Expand All @@ -94,6 +104,7 @@ const sendNotification = ({
notificationSent = true;

sendSinglePush({
notificationMessage,
room,
message,
userId: subscription.u._id,
Expand Down Expand Up @@ -144,38 +155,67 @@ function sendAllNotifications(message, room) {
return message;
}

const mentionIds = (message.mentions || []).map(({_id}) => _id);
const mentionIdsWithoutGroups = mentionIds.filter((_id) => _id !== 'all' && _id !== 'here');
const hasMentionToAll = mentionIds.includes('all');
const hasMentionToHere = mentionIds.includes('here');

let notificationMessage = RocketChat.callbacks.run('beforeSendMessageNotifications', message.msg);
if (mentionIds.length > 0 && RocketChat.settings.get('UI_Use_Real_Name')) {
notificationMessage = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions);
}

// Don't fetch all users if room exceeds max members
const maxMembersForNotification = RocketChat.settings.get('Notifications_Max_Room_Members');
const disableAllMessageNotifications = room.usernames.length > maxMembersForNotification && maxMembersForNotification !== 0;

// the find bellow is crucial. all subscription records returned will receive at least one kind of notification.
// the query is defined by the server's default values and Notifications_Max_Room_Members setting.
let subscriptions;
if (disableAllMessageNotifications) {
subscriptions = RocketChat.models.Subscriptions.findAllMessagesNotificationPreferencesByRoom(room._id);
} else {
const mentionsFilter = { $in: ['all', 'mentions'] };
const excludesNothingFilter = { $ne: 'nothing' };

// evaluate if doing three specific finds is better than evaluting all results
subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom({
roomId: room._id,
desktopFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'nothing' ? mentionsFilter : excludesNothingFilter,
emailFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'disabled' ? mentionsFilter : excludesNothingFilter,
mobileFilter: RocketChat.settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'nothing' ? mentionsFilter : excludesNothingFilter
});
}
const query = {
rid: room._id,
$or: [{
'userHighlights.0': { $exists: 1 }
}]
};

const mentionIds = (message.mentions || []).map(({_id}) => _id);
const hasMentionToAll = mentionIds.includes('all');
const hasMentionToHere = mentionIds.includes('here');
['audio', 'desktop', 'mobile', 'email'].map((kind) => {
const notificationField = `${ kind === 'mobile' ? 'mobilePush' : kind }Notifications`;

const filter = { [notificationField]: 'all' };

if (disableAllMessageNotifications) {
filter[`${ kind }PrefOrigin`] = { $ne: 'user' };
}

query.$or.push(filter);

if (mentionIdsWithoutGroups.length > 0) {
query.$or.push({
[notificationField]: 'mentions',
'u._id': { $in: mentionIdsWithoutGroups }
});
}

if (RocketChat.settings.get(`Accounts_Default_User_Preferences_${ notificationField }`) === 'all' && !disableAllMessageNotifications) {
query.$or.push({
[notificationField]: { $exists: false }
});
} else if (RocketChat.settings.get(`Accounts_Default_User_Preferences_${ notificationField }`) === 'mentions' && mentionIdsWithoutGroups.length) {
query.$or.push({
[notificationField]: { $exists: false },
'u._id': { $in: mentionIdsWithoutGroups }
});
}
});

// the find bellow is crucial. all subscription records returned will receive at least one kind of notification.
// the query is defined by the server's default values and Notifications_Max_Room_Members setting.
const subscriptions = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(query);
subscriptions.forEach((subscription) => sendNotification({
subscription,
sender,
hasMentionToAll,
hasMentionToHere,
message,
notificationMessage,
room,
mentionIds,
disableAllMessageNotifications
Expand All @@ -200,6 +240,7 @@ function sendAllNotifications(message, room) {
hasMentionToAll,
hasMentionToHere,
message,
notificationMessage,
room,
mentionIds
});
Expand All @@ -211,4 +252,3 @@ function sendAllNotifications(message, room) {
}

RocketChat.callbacks.add('afterSaveMessage', sendAllNotifications, RocketChat.callbacks.priority.LOW, 'sendNotificationsOnMessage');

Loading