odoo.define('mail.Manager.Notification', function (require) {
"use strict";
/**
* Mail Notification Manager
*
* This part of the mail manager is responsible for receiving notifications on
* the longpoll bus, which are data received from the server.
*/
var MailManager = require('mail.Manager');
var MailFailure = require('mail.model.MailFailure');
var core = require('web.core');
var session = require('web.session');
var _t = core._t;
MailManager.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Remove channel notifications if there is an unsubscribe notification
* on this channel.
*
* @private
* @param {Object[]} notifications
* @returns {Object[]} notifications filtered of channel notifications
* matching unsubscribe notifs
*/
_filterNotificationsOnUnsubscribe: function (notifications) {
var unsubscribedNotif = _.find(notifications, function (notif) {
return notif[1].info === 'unsubscribe';
});
if (unsubscribedNotif) {
notifications = _.reject(notifications, function (notif) {
return notif[0][1] === 'mail.channel' &&
notif[0][2] === unsubscribedNotif[1].id;
});
}
return notifications;
},
/**
* Called when receiving a notification on a channel (all members of a
* channel receive this notification)
*
* @private
* @param {Object} params
* @param {integer} [params.channelID]
* @param {Object} params.data
* @param {string} [params.data.info] if set, specify the type of data on
* this channel notification
*/
_handleChannelNotification: function (params) {
if (params.data && params.data.info === 'typing_status') {
this._handleChannelTypingNotification(params.channelID, params.data);
} else if (params.data && params.data.info === 'channel_fetched') {
this._handleChannelFetchedNotification(params.channelID, params.data);
} else if (params.data && params.data.info === 'channel_seen') {
this._handleChannelSeenNotification(params.channelID, params.data);
} else {
this._handleChannelMessageNotification(params.data);
}
},
/**
* Called when a channel has been fetched, and the server responses with the
* last message fetched. Useful in order to track last message fetched.
*
* @private
* @param {integer} channelID
* @param {Object} data
* @param {string} data.info 'channel_fetched'
* @param {integer} data.last_message_id
* @param {integer} data.partner_id
*/
_handleChannelFetchedNotification: function (channelID, data) {
var channel = this.getChannel(channelID);
if (!channel) {
return;
}
if (channel.hasSeenFeature()) {
channel.updateSeenPartnersInfo(data);
}
},
/**
* Called when a new or updated message is received on a channel
*
* @private
* @param {Object} messageData
* @param {integer[]} messageData.channel_ids channel IDs of this message
* (note that 'pending moderation' messages in moderated channels do not
* have the moderated channels in this array).
*/
_handleChannelMessageNotification: function (messageData) {
var self = this;
var def;
var channelAlreadyInCache = true;
if (messageData.channel_ids.length === 1) {
channelAlreadyInCache = !!this.getChannel(messageData.channel_ids[0]);
def = this.joinChannel(messageData.channel_ids[0], { autoswitch: false });
} else {
def = Promise.resolve();
}
def.then(function () {
// don't increment unread if channel wasn't in cache yet as
// its unread counter has just been fetched
return self.addMessage(messageData, {
showNotification: true,
incrementUnread: channelAlreadyInCache
});
});
},
/**
* Called when a channel has been seen, and the server responses with the
* last message seen. Useful in order to track last message seen.
*
* @private
* @param {integer} channelID
* @param {Object} data
* @param {string} data.info 'channel_seen'
* @param {integer} data.last_message_id
* @param {integer} data.partner_id
*/
_handleChannelSeenNotification: function (channelID, data) {
var channel = this.getChannel(channelID);
if (!channel) {
return;
}
if (channel.hasSeenFeature()) {
channel.updateSeenPartnersInfo(data);
}
if (session.partner_id !== data.partner_id) {
return;
}
channel.setLastSeenMessageID(data.last_message_id);
if (channel.hasUnreadMessages()) {
channel.resetUnreadCounter();
}
},
/**
* Called when someone starts or stops typing a message in a channel
*
* @private
* @param {integer} channelID
* @param {Object} typingData
* @param {boolean} typingData.is_typing
* @param {boolean} typingData.is_website_user
* @param {integer} typingData.partner_id
*/
_handleChannelTypingNotification: function (channelID, typingData) {
var isWebsiteUser = typingData.is_website_user;
var partnerID = typingData.partner_id;
if (partnerID === session.partner_id && !isWebsiteUser) {
return; // ignore typing notification of myself
}
var channel = this.getChannel(channelID);
if (!channel) {
return;
}
var typingID = {
isWebsiteUser: typingData.is_website_user,
partnerID: typingData.partner_id,
};
if (typingData.is_typing) {
channel.registerTyping(typingID);
} else {
channel.unregisterTyping(typingID);
}
},
/**
* On message becoming a need action (pinned to inbox)
*
* @private
* @param {Object} messageData
* @param {integer[]} messageData.channel_ids
*/
_handleNeedactionNotification: function (messageData) {
var self = this;
var inbox = this.getMailbox('inbox');
this.addMessage(messageData, {
incrementUnread: true,
showNotification: true,
}).then(function (message) {
inbox.incrementMailboxCounter();
_.each(message.getThreadIDs(), function (threadID) {
var channel = self.getChannel(threadID);
if (channel) {
channel.incrementNeedactionCounter();
}
});
self._mailBus.trigger('update_needaction', inbox.getMailboxCounter());
});
},
/**
* Called when an activity record has been updated on the server
*
* @private
* @param {Object} data key, value to decide activity created or deleted
*/
_handlePartnerActivityUpdateNotification: function (data) {
this._mailBus.trigger('activity_updated', data);
},
/**
* Called when receiving a channel state as a partner notification:
*
* - if it is a new channel, it means we have been invited to this channel
* - if it is an existing channel, it means the window state of the channel
* may have changed (as this state is stored server-side)
*
* @private
* @param {Object} channelData
* @param {integer} channelData.id
* @param {string} [channelData.info]
* @param {boolean} channelData.is_minimized
* @param {string} channelData.state
*/
_handlePartnerChannelNotification: function (channelData) {
if (
(channelData.channel_type === 'channel') &&
(channelData.state === 'open')
) {
// invited to a new channel
this._addChannel(channelData, { autoswitch: false });
if (
!channelData.is_minimized &&
channelData.info !== 'creation'
) {
this.do_notify(
_t("Invitation"),
_t("You have been invited to: ") + channelData.name);
}
}
var channel = this.getChannel(channelData.id);
if (channel && channelData.info !== 'join') {
channel.updateWindowState({
folded: channelData.state === 'folded' ? true : false,
detached: channelData.is_minimized,
});
}
},
/**
* Called when receiving a multi_user_channel seen notification. Only
* the current user is notified. This must be handled as if this is a
* channel seen notification.
*
* Note that this is a 'res.partner' notification because only the current
* user is notified on channel seen. This is a consequence from disabling
* the seen feature on multi_user_channel, because we still need to get
* the last seen message ID in order to display the "New Messages" separator
* in Discuss.
*
* @private
* @param {Object} data
* @param {integer} data.channel_id
* @param {string} data.info 'channel_seen'
* @param {integer} data.last_message_id
* @param {integer} data.partner_id
*/
_handlePartnerChannnelSeenNotification: function (data) {
this._handleChannelSeenNotification(data.channel_id, data);
},
/**
* Add or remove failure when receiving a failure update message
*
* @private
* @param {Object} datas
* @param {Object[]} datas.elements list of mail failure data
* @param {string} datas.elements[].message_id ID of related message that
* has a mail failure.
* @param {Array} datas.elements[].notifications list of notifications
* that is related to a mail failure.
* @param {string} datas.elements[].notifications[0] sending state of a mail
* failure (e.g. 'exception').
*/
_handlePartnerMailFailureNotification: function (datas) {
var self = this;
_.each(datas.elements, function (data) {
var isNewFailure = _.some(data.notifications, function (notif) {
return notif[0] === 'exception' || notif[0] === 'bounce';
});
var matchedFailure = _.find(self._mailFailures, function (failure) {
return failure.getMessageID() === data.message_id;
});
if (matchedFailure) {
var index = _.findIndex(self._mailFailures, matchedFailure);
if (isNewFailure) {
self._mailFailures[index] = new MailFailure(self, data);
} else {
self._mailFailures.splice(index, 1);
}
} else if (isNewFailure) {
self._mailFailures.push(new MailFailure(self, data));
}
var message = _.find(self._messages, function (msg) {
return msg.getID() === data.message_id;
});
if (message) {
if (isNewFailure) {
message.updateCustomerEmailStatus('exception');
} else {
message.updateCustomerEmailStatus('sent');
}
self._updateMessageNotificationStatus(data, message);
self._mailBus.trigger('update_message', message);
}
});
this._mailBus.trigger('update_needaction', this.needactionCounter);
},
/**
* Updates mailbox_inbox when a message has marked as read.
*
* @private
* @param {Object} data
* @param {integer[]} [data.channel_ids]
* @param {integer[]} [data.message_ids]
* @param {string} [data.type]
*/
_handlePartnerMarkAsReadNotification: function (data) {
var self = this;
_.each(data.message_ids, function (messageID) {
var message = _.find(self._messages, function (msg) {
return msg.getID() === messageID;
});
if (message) {
self._removeMessageFromThread('mailbox_inbox', message);
self._mailBus.trigger('update_message', message, data.type);
}
});
if (data.channel_ids) {
_.each(data.channel_ids, function (channelID) {
var channel = self.getChannel(channelID);
if (channel) {
channel.decrementNeedactionCounter(data.message_ids.length, 0);
}
});
} else {
// if no channel_ids specified, this is a 'mark all read' in inbox
_.each(this.getChannels(), function (channel) {
channel.resetNeedactionCounter();
});
}
var inbox = this.getMailbox('inbox');
inbox.decrementMailboxCounter(data.message_ids.length);
this._mailBus.trigger('update_needaction', inbox.getMailboxCounter());
},
/**
* On receiving a message made by the current user, on a moderated channel,
* which is pending moderation.
*
* @private
* @param {Object} data
* @param {Object} data.message server-side data of the message
*/
_handlePartnerMessageAuthorNotification: function (data) {
this.addMessage(data.message);
},
/**
* Notification to delete several messages locally
* Useful when a pending moderation message has been rejected, so that
* this message should not be displayed anymore.
*
* @private
* @param {Object} data
* @param {Object[]} [data.message_ids] IDs of messages to delete locally.
*/
_handlePartnerMessageDeletionNotification: function (data) {
var self = this;
_.each(data.message_ids, function (messageID) {
var message = self.getMessage(messageID);
if (message) {
message.setModerationStatus('rejected');
}
});
},
/**
* On receiving a message pending moderation, and current user is moderator
* of such message.
*
* @param {Object} data notification data
* @param {Object} data.message data of message
*/
_handlePartnerMessageModeratorNotification: function (data) {
var self = this;
this.addMessage(data.message).then(function () {
self._mailBus.trigger('update_moderation_counter');
});
},
/**
* On receiving a notification that is specific to a user
*
* @private
* @param {Object} data structure depending on the type
* @param {integer} data.id
*/
_handlePartnerNotification: function (data) {
if (data.info === 'unsubscribe') {
this._handlePartnerUnsubscribeNotification(data);
} else if (data.type === 'toggle_star') {
this._handlePartnerToggleStarNotification(data);
} else if (data.type === 'mark_as_read') {
this._handlePartnerMarkAsReadNotification(data);
} else if (data.type === 'moderator') {
this._handlePartnerMessageModeratorNotification(data);
} else if (data.type === 'author') {
this._handlePartnerMessageAuthorNotification(data);
} else if (data.type === 'deletion') {
this._handlePartnerMessageDeletionNotification(data);
} else if (data.info === 'transient_message') {
this._handlePartnerTransientMessageNotification(data);
} else if (data.type === 'activity_updated') {
this._handlePartnerActivityUpdateNotification(data);
} else if (data.type === 'mail_failure') {
this._handlePartnerMailFailureNotification(data);
} else if (data.type === 'user_connection') {
this._handlePartnerUserConnectionNotification(data);
} else if (data.info === 'channel_seen') {
this._handlePartnerChannnelSeenNotification(data);
} else if (data.type === 'simple_notification') {
var title = _.escape(data.title), message = _.escape(data.message);
data.warning ? this.do_warn(title, message, data.sticky) : this.do_notify(title, message, data.sticky);
} else {
this._handlePartnerChannelNotification(data);
}
},
/**
* On toggling on or off the star status of one or several messages.
* As the information is stored server-side, the web client must adapt
* itself from server's data on the messages.
*
* @private
* @param {Object} data
* @param {integer[]} data.message_ids IDs of messages that have a change
* of their starred status.
* @param {boolean} data.starred states whether the messages with id in
* `data.message_ids` have become either starred or unstarred
*/
_handlePartnerToggleStarNotification: function (data) {
var self = this;
var starred = this.getMailbox('starred');
_.each(data.message_ids, function (messageID) {
var message = _.find(self._messages, function (msg) {
return msg.getID() === messageID;
});
if (message) {
message.setStarred(data.starred);
if (!message.isStarred()) {
self._removeMessageFromThread('mailbox_starred', message);
} else {
self._addMessageToThreads(message, []);
var channelStarred = self.getMailbox('starred');
channelStarred.invalidateCaches();
}
self._mailBus.trigger('update_message', message);
}
});
if (data.starred) {
// increase starred counter if message is marked as star
starred.incrementMailboxCounter(data.message_ids.length);
} else {
// decrease starred counter if message is remove from starred
// if unstar_all then it will set to 0.
starred.decrementMailboxCounter(data.message_ids.length);
}
this._mailBus.trigger('update_starred', starred.getMailboxCounter());
},
/**
* On receiving a transient message, i.e. a message which does not come from
* a member of the channel. Usually a log message, such as one generated
* from a command with ('/').
*
* @private
* @param {Object} data
*/
_handlePartnerTransientMessageNotification: function (data) {
var lastMessage = _.last(this._messages);
data.id = (lastMessage ? lastMessage.getID() : 0) + 0.01;
data.author_id = this.getOdoobotID();
this.addMessage(data);
},
/**
* On receiving a unsubscribe from channel notification, confirm
* unsubscription from channel and adapt screen accordingly.
*
* @private
* @param {Object} data
* @param {Object} data.id ID of the unsubscribed channel
*/
_handlePartnerUnsubscribeNotification: function (data) {
var channel = this.getChannel(data.id);
if (channel) {
var message;
if (_.contains(['public', 'private'], channel.getType())) {
message = _.str.sprintf(
_t("You unsubscribed from %s."),
channel.getName()
);
} else {
message = _.str.sprintf(
_t("You unpinned your conversation with %s."),
channel.getName()
);
}
this._removeChannel(channel);
this._mailBus.trigger('unsubscribe_from_channel', data.id);
this.do_notify(_t("Unsubscribed"), message);
}
},
/**
* Shows a popup to notify a user connection
*
* @private
* @param {Object} data
* @param {Object[]} data.partner_id id of the connected partner
* @param {string} data.title title to display on notification
* @param {Array} data.messages message to display on notification
*/
_handlePartnerUserConnectionNotification: function (data) {
var self = this;
var partnerID = data.partner_id;
this.call('bus_service', 'sendNotification', data.title, data.message, function ( ){
self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID);
});
},
/**
* @override
* @private
*/
_listenOnBuses: function () {
this._super.apply(this, arguments);
this.call('bus_service', 'onNotification', this, this._onNotification);
},
/**
* Update the message notification status of message based on update_message
*
* @private
* @param {Object} data
* @param {Object[]} data.notifications
* @param {mail.model.Message} message
*/
_updateMessageNotificationStatus: function (data, message) {
_.each(data.notifications, function (notif, id) {
var partnerName = notif[1];
var notifStatus = notif[0];
var res = _.find(message.getCustomerEmailData(), function (entry) {
return entry[0] === parseInt(id);
});
if (res) {
res[2] = notifStatus;
} else {
message.addCustomerEmailData([parseInt(id), partnerName, notifStatus]);
}
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Notification handlers
* Sometimes, the web client receives unsubscribe notification and an extra
* notification on that channel. This is then followed by an attempt to
* rejoin the channel that we just left. The next few lines remove the
* extra notification to prevent that situation to occur.
*
* @private
* @param {Array} notifs
*/
_onNotification: function (notifs) {
var self = this;
notifs = this._filterNotificationsOnUnsubscribe(notifs);
_.each(notifs, function (notif) {
var model = notif[0][1];
if (model === 'ir.needaction') {
self._handleNeedactionNotification(notif[1]);
} else if (model === 'mail.channel') {
// new message in a channel
self._handleChannelNotification({
channelID: notif[0][2],
data: notif[1],
});
} else if (model === 'res.partner') {
self._handlePartnerNotification(notif[1]);
}
});
},
});
return MailManager;
});