summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/models/messaging_notification_handler
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/models/messaging_notification_handler
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/models/messaging_notification_handler')
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js795
1 files changed, 795 insertions, 0 deletions
diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
new file mode 100644
index 00000000..a42ede1c
--- /dev/null
+++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -0,0 +1,795 @@
+odoo.define('mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js', function (require) {
+'use strict';
+
+const { registerNewModel } = require('mail/static/src/model/model_core.js');
+const { one2one } = require('mail/static/src/model/model_field.js');
+const { decrement, increment } = require('mail/static/src/model/model_field_command.js');
+const { htmlToTextContentInline } = require('mail.utils');
+
+const PREVIEW_MSG_MAX_SIZE = 350; // optimal for native English speakers
+
+function factory(dependencies) {
+
+ class MessagingNotificationHandler extends dependencies['mail.model'] {
+
+ /**
+ * @override
+ */
+ _willDelete() {
+ if (this.env.services['bus_service']) {
+ this.env.services['bus_service'].off('notification');
+ this.env.services['bus_service'].stopPolling();
+ }
+ return super._willDelete(...arguments);
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * Fetch messaging data initially to populate the store specifically for
+ * the current users. This includes pinned channels for instance.
+ */
+ start() {
+ this.env.services.bus_service.onNotification(null, notifs => this._handleNotifications(notifs));
+ this.env.services.bus_service.startPolling();
+ }
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Object[]} notifications
+ * @returns {Object[]}
+ */
+ _filterNotificationsOnUnsubscribe(notifications) {
+ const unsubscribedNotif = notifications.find(notif =>
+ notif[1].info === 'unsubscribe');
+ if (unsubscribedNotif) {
+ notifications = notifications.filter(notif =>
+ notif[0][1] !== 'mail.channel' ||
+ notif[0][2] !== unsubscribedNotif[1].id
+ );
+ }
+ return notifications;
+ }
+
+ /**
+ * @private
+ * @param {Object[]} notifications
+ * @param {Array|string} notifications[i][0] meta-data of the notification.
+ * @param {string} notifications[i][0][0] name of database this
+ * notification comes from.
+ * @param {string} notifications[i][0][1] type of notification.
+ * @param {integer} notifications[i][0][2] usually id of related type
+ * of notification. For instance, with `mail.channel`, this is the id
+ * of the channel.
+ * @param {Object} notifications[i][1] payload of the notification
+ */
+ async _handleNotifications(notifications) {
+ const filteredNotifications = this._filterNotificationsOnUnsubscribe(notifications);
+ const proms = filteredNotifications.map(notification => {
+ const [channel, message] = notification;
+ if (typeof channel === 'string') {
+ // uuid notification, only for (livechat) public handler
+ return;
+ }
+ const [, model, id] = channel;
+ switch (model) {
+ case 'ir.needaction':
+ return this._handleNotificationNeedaction(message);
+ case 'mail.channel':
+ return this._handleNotificationChannel(id, message);
+ case 'res.partner':
+ if (id !== this.env.messaging.currentPartner.id) {
+ // ignore broadcast to other partners
+ return;
+ }
+ return this._handleNotificationPartner(Object.assign({}, message));
+ }
+ });
+ await this.async(() => Promise.all(proms));
+ }
+
+ /**
+ * @private
+ * @param {integer} channelId
+ * @param {Object} data
+ * @param {string} [data.info]
+ * @param {boolean} [data.is_typing]
+ * @param {integer} [data.last_message_id]
+ * @param {integer} [data.partner_id]
+ */
+ async _handleNotificationChannel(channelId, data) {
+ const {
+ info,
+ is_typing,
+ last_message_id,
+ partner_id,
+ partner_name,
+ } = data;
+ switch (info) {
+ case 'channel_fetched':
+ return this._handleNotificationChannelFetched(channelId, {
+ last_message_id,
+ partner_id,
+ });
+ case 'channel_seen':
+ return this._handleNotificationChannelSeen(channelId, {
+ last_message_id,
+ partner_id,
+ });
+ case 'typing_status':
+ return this._handleNotificationChannelTypingStatus(channelId, {
+ is_typing,
+ partner_id,
+ partner_name,
+ });
+ default:
+ return this._handleNotificationChannelMessage(channelId, data);
+ }
+ }
+
+ /**
+ * @private
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {integer} param1.last_message_id
+ * @param {integer} param1.partner_id
+ */
+ async _handleNotificationChannelFetched(channelId, {
+ last_message_id,
+ partner_id,
+ }) {
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ if (!channel) {
+ // for example seen from another browser, the current one has no
+ // knowledge of the channel
+ return;
+ }
+ if (channel.channel_type === 'channel') {
+ // disabled on `channel` channels for performance reasons
+ return;
+ }
+ this.env.models['mail.thread_partner_seen_info'].insert({
+ channelId: channel.id,
+ lastFetchedMessage: [['insert', { id: last_message_id }]],
+ partnerId: partner_id,
+ });
+ channel.update({
+ messageSeenIndicators: [['insert',
+ {
+ channelId: channel.id,
+ messageId: last_message_id,
+ }
+ ]],
+ });
+ // FIXME force the computing of message values (cf task-2261221)
+ this.env.models['mail.message_seen_indicator'].recomputeFetchedValues(channel);
+ }
+
+ /**
+ * @private
+ * @param {integer} channelId
+ * @param {Object} messageData
+ */
+ async _handleNotificationChannelMessage(channelId, messageData) {
+ let channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ const wasChannelExisting = !!channel;
+ const convertedData = this.env.models['mail.message'].convertData(messageData);
+ const oldMessage = this.env.models['mail.message'].findFromIdentifyingData(convertedData);
+ // locally save old values, as insert would overwrite them
+ const oldMessageModerationStatus = (
+ oldMessage && oldMessage.moderation_status
+ );
+ const oldMessageWasModeratedByCurrentPartner = (
+ oldMessage && oldMessage.isModeratedByCurrentPartner
+ );
+
+ // Fetch missing info from channel before going further. Inserting
+ // a channel with incomplete info can lead to issues. This is in
+ // particular the case with the `uuid` field that is assumed
+ // "required" by the rest of the code and is necessary for some
+ // features such as chat windows.
+ if (!channel) {
+ channel = (await this.async(() =>
+ this.env.models['mail.thread'].performRpcChannelInfo({ ids: [channelId] })
+ ))[0];
+ }
+ if (!channel.isPinned) {
+ channel.pin();
+ }
+
+ const message = this.env.models['mail.message'].insert(convertedData);
+ this._notifyThreadViewsMessageReceived(message);
+
+ // If the message was already known: nothing else should be done,
+ // except if it was pending moderation by the current partner, then
+ // decrement the moderation counter.
+ if (oldMessage) {
+ if (
+ oldMessageModerationStatus === 'pending_moderation' &&
+ message.moderation_status !== 'pending_moderation' &&
+ oldMessageWasModeratedByCurrentPartner
+ ) {
+ const moderation = this.env.messaging.moderation;
+ moderation.update({ counter: decrement() });
+ }
+ return;
+ }
+
+ // If the current partner is author, do nothing else.
+ if (message.author === this.env.messaging.currentPartner) {
+ return;
+ }
+
+ // Message from mailing channel should not make a notification in
+ // Odoo for users with notification "Handled by Email".
+ // Channel has been marked as read server-side in this case, so
+ // it should not display a notification by incrementing the
+ // unread counter.
+ if (
+ channel.mass_mailing &&
+ this.env.session.notification_type === 'email'
+ ) {
+ this._handleNotificationChannelSeen(channelId, {
+ last_message_id: messageData.id,
+ partner_id: this.env.messaging.currentPartner.id,
+ });
+ return;
+ }
+ // In all other cases: update counter and notify if necessary
+
+ // Chat from OdooBot is considered disturbing and should only be
+ // shown on the menu, but no notification and no thread open.
+ const isChatWithOdooBot = (
+ channel.correspondent &&
+ channel.correspondent === this.env.messaging.partnerRoot
+ );
+ if (!isChatWithOdooBot) {
+ const isOdooFocused = this.env.services['bus_service'].isOdooFocused();
+ // Notify if out of focus
+ if (!isOdooFocused && channel.isChatChannel) {
+ this._notifyNewChannelMessageWhileOutOfFocus({
+ channel,
+ message,
+ });
+ }
+ if (channel.model === 'mail.channel' && channel.channel_type !== 'channel') {
+ // disabled on non-channel threads and
+ // on `channel` channels for performance reasons
+ channel.markAsFetched();
+ }
+ // open chat on receiving new message if it was not already opened or folded
+ if (channel.channel_type !== 'channel' && !this.env.messaging.device.isMobile && !channel.chatWindow) {
+ this.env.messaging.chatWindowManager.openThread(channel);
+ }
+ }
+
+ // If the channel wasn't known its correct counter was fetched at
+ // the start of the method, no need update it here.
+ if (!wasChannelExisting) {
+ return;
+ }
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * Called when a channel has been seen, and the server responds with the
+ * last message seen. Useful in order to track last message seen.
+ *
+ * @private
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {integer} param1.last_message_id
+ * @param {integer} param1.partner_id
+ */
+ async _handleNotificationChannelSeen(channelId, {
+ last_message_id,
+ partner_id,
+ }) {
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ if (!channel) {
+ // for example seen from another browser, the current one has no
+ // knowledge of the channel
+ return;
+ }
+ const lastMessage = this.env.models['mail.message'].insert({ id: last_message_id });
+ // restrict computation of seen indicator for "non-channel" channels
+ // for performance reasons
+ const shouldComputeSeenIndicators = channel.channel_type !== 'channel';
+ const updateData = {};
+ if (shouldComputeSeenIndicators) {
+ this.env.models['mail.thread_partner_seen_info'].insert({
+ channelId: channel.id,
+ lastSeenMessage: [['link', lastMessage]],
+ partnerId: partner_id,
+ });
+ Object.assign(updateData, {
+ // FIXME should no longer use computeId (task-2335647)
+ messageSeenIndicators: [['insert',
+ {
+ channelId: channel.id,
+ messageId: lastMessage.id,
+ },
+ ]],
+ });
+ }
+ if (this.env.messaging.currentPartner.id === partner_id) {
+ Object.assign(updateData, {
+ lastSeenByCurrentPartnerMessageId: last_message_id,
+ pendingSeenMessageId: undefined,
+ });
+ }
+ channel.update(updateData);
+ if (shouldComputeSeenIndicators) {
+ // FIXME force the computing of thread values (cf task-2261221)
+ this.env.models['mail.thread'].computeLastCurrentPartnerMessageSeenByEveryone(channel);
+ // FIXME force the computing of message values (cf task-2261221)
+ this.env.models['mail.message_seen_indicator'].recomputeSeenValues(channel);
+ }
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {boolean} param1.is_typing
+ * @param {integer} param1.partner_id
+ * @param {string} param1.partner_name
+ */
+ _handleNotificationChannelTypingStatus(channelId, { is_typing, partner_id, partner_name }) {
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ if (!channel) {
+ return;
+ }
+ const partner = this.env.models['mail.partner'].insert({
+ id: partner_id,
+ name: partner_name,
+ });
+ if (partner === this.env.messaging.currentPartner) {
+ // Ignore management of current partner is typing notification.
+ return;
+ }
+ if (is_typing) {
+ if (channel.typingMembers.includes(partner)) {
+ channel.refreshOtherMemberTypingMember(partner);
+ } else {
+ channel.registerOtherMemberTypingMember(partner);
+ }
+ } else {
+ if (!channel.typingMembers.includes(partner)) {
+ // Ignore no longer typing notifications of members that
+ // are not registered as typing something.
+ return;
+ }
+ channel.unregisterOtherMemberTypingMember(partner);
+ }
+ }
+
+ /**
+ * @private
+ * @param {Object} data
+ */
+ _handleNotificationNeedaction(data) {
+ const message = this.env.models['mail.message'].insert(
+ this.env.models['mail.message'].convertData(data)
+ );
+ this.env.messaging.inbox.update({ counter: increment() });
+ const originThread = message.originThread;
+ if (originThread && message.isNeedaction) {
+ originThread.update({ message_needaction_counter: increment() });
+ }
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} data
+ * @param {string} [data.info]
+ * @param {string} [data.type]
+ */
+ async _handleNotificationPartner(data) {
+ const {
+ info,
+ type,
+ } = data;
+ if (type === 'activity_updated') {
+ this.env.bus.trigger('activity_updated', data);
+ } else if (type === 'author') {
+ return this._handleNotificationPartnerAuthor(data);
+ } else if (info === 'channel_seen') {
+ return this._handleNotificationChannelSeen(data.channel_id, data);
+ } else if (type === 'deletion') {
+ return this._handleNotificationPartnerDeletion(data);
+ } else if (type === 'message_notification_update') {
+ return this._handleNotificationPartnerMessageNotificationUpdate(data.elements);
+ } else if (type === 'mark_as_read') {
+ return this._handleNotificationPartnerMarkAsRead(data);
+ } else if (type === 'moderator') {
+ return this._handleNotificationPartnerModerator(data);
+ } else if (type === 'simple_notification') {
+ const escapedMessage = owl.utils.escape(data.message);
+ this.env.services['notification'].notify({
+ message: escapedMessage,
+ sticky: data.sticky,
+ type: data.warning ? 'warning' : 'danger',
+ });
+ } else if (type === 'toggle_star') {
+ return this._handleNotificationPartnerToggleStar(data);
+ } else if (info === 'transient_message') {
+ return this._handleNotificationPartnerTransientMessage(data);
+ } else if (info === 'unsubscribe') {
+ return this._handleNotificationPartnerUnsubscribe(data.id);
+ } else if (type === 'user_connection') {
+ return this._handleNotificationPartnerUserConnection(data);
+ } else if (!type) {
+ return this._handleNotificationPartnerChannel(data);
+ }
+ }
+
+ /**
+ * @private
+ * @param {Object} data
+ * @param {Object} data.message
+ */
+ _handleNotificationPartnerAuthor(data) {
+ this.env.models['mail.message'].insert(
+ this.env.models['mail.message'].convertData(data.message)
+ );
+ }
+
+ /**
+ * @private
+ * @param {Object} data
+ * @param {string} data.channel_type
+ * @param {integer} data.id
+ * @param {string} [data.info]
+ * @param {boolean} data.is_minimized
+ * @param {string} data.name
+ * @param {string} data.state
+ * @param {string} data.uuid
+ */
+ _handleNotificationPartnerChannel(data) {
+ const convertedData = this.env.models['mail.thread'].convertData(
+ Object.assign({ model: 'mail.channel' }, data)
+ );
+ if (!convertedData.members) {
+ // channel_info does not return all members of channel for
+ // performance reasons, but code is expecting to know at
+ // least if the current partner is member of it.
+ // (e.g. to know when to display "invited" notification)
+ // Current partner can always be assumed to be a member of
+ // channels received through this notification.
+ convertedData.members = [['link', this.env.messaging.currentPartner]];
+ }
+ let channel = this.env.models['mail.thread'].findFromIdentifyingData(convertedData);
+ const wasCurrentPartnerMember = (
+ channel &&
+ channel.members.includes(this.env.messaging.currentPartner)
+ );
+
+ channel = this.env.models['mail.thread'].insert(convertedData);
+ if (
+ channel.channel_type === 'channel' &&
+ data.info !== 'creation' &&
+ !wasCurrentPartnerMember
+ ) {
+ this.env.services['notification'].notify({
+ message: _.str.sprintf(
+ this.env._t("You have been invited to: %s"),
+ owl.utils.escape(channel.name)
+ ),
+ type: 'warning',
+ });
+ }
+ // a new thread with unread messages could have been added
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {integer[]} param0.messag_ids
+ */
+ _handleNotificationPartnerDeletion({ message_ids }) {
+ const moderationMailbox = this.env.messaging.moderation;
+ for (const id of message_ids) {
+ const message = this.env.models['mail.message'].findFromIdentifyingData({ id });
+ if (message) {
+ if (
+ message.moderation_status === 'pending_moderation' &&
+ message.originThread.isModeratedByCurrentPartner
+ ) {
+ moderationMailbox.update({ counter: decrement() });
+ }
+ message.delete();
+ }
+ }
+ // deleting message might have deleted notifications, force recompute
+ this.messaging.notificationGroupManager.computeGroups();
+ // manually force recompute of counter (after computing the groups)
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} data
+ */
+ _handleNotificationPartnerMessageNotificationUpdate(data) {
+ for (const messageData of data) {
+ const message = this.env.models['mail.message'].insert(
+ this.env.models['mail.message'].convertData(messageData)
+ );
+ // implicit: failures are sent by the server as notification
+ // only if the current partner is author of the message
+ if (!message.author && this.messaging.currentPartner) {
+ message.update({ author: [['link', this.messaging.currentPartner]] });
+ }
+ }
+ this.messaging.notificationGroupManager.computeGroups();
+ // manually force recompute of counter (after computing the groups)
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {integer[]} [param0.channel_ids
+ * @param {integer[]} [param0.message_ids=[]]
+ * @param {integer} [param0.needaction_inbox_counter]
+ */
+ _handleNotificationPartnerMarkAsRead({ channel_ids, message_ids = [], needaction_inbox_counter }) {
+ for (const message_id of message_ids) {
+ // We need to ignore all not yet known messages because we don't want them
+ // to be shown partially as they would be linked directly to mainCache
+ // Furthermore, server should not send back all message_ids marked as read
+ // but something like last read message_id or something like that.
+ // (just imagine you mark 1000 messages as read ... )
+ const message = this.env.models['mail.message'].findFromIdentifyingData({ id: message_id });
+ if (!message) {
+ continue;
+ }
+ // update thread counter
+ const originThread = message.originThread;
+ if (originThread && message.isNeedaction) {
+ originThread.update({ message_needaction_counter: decrement() });
+ }
+ // move messages from Inbox to history
+ message.update({
+ isHistory: true,
+ isNeedaction: false,
+ });
+ }
+ const inbox = this.env.messaging.inbox;
+ if (needaction_inbox_counter !== undefined) {
+ inbox.update({ counter: needaction_inbox_counter });
+ } else {
+ // kept for compatibility in stable
+ inbox.update({ counter: decrement(message_ids.length) });
+ }
+ if (inbox.counter > inbox.mainCache.fetchedMessages.length) {
+ // Force refresh Inbox because depending on what was marked as
+ // read the cache might become empty even though there are more
+ // messages on the server.
+ inbox.mainCache.update({ hasToLoadMessages: true });
+ }
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {Object} param0.message
+ */
+ _handleNotificationPartnerModerator({ message: data }) {
+ this.env.models['mail.message'].insert(
+ this.env.models['mail.message'].convertData(data)
+ );
+ const moderationMailbox = this.env.messaging.moderation;
+ if (moderationMailbox) {
+ moderationMailbox.update({ counter: increment() });
+ }
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {integer[]} param0.message_ids
+ * @param {boolean} param0.starred
+ */
+ _handleNotificationPartnerToggleStar({ message_ids = [], starred }) {
+ const starredMailbox = this.env.messaging.starred;
+ for (const messageId of message_ids) {
+ const message = this.env.models['mail.message'].findFromIdentifyingData({
+ id: messageId,
+ });
+ if (!message) {
+ continue;
+ }
+ message.update({ isStarred: starred });
+ starredMailbox.update({
+ counter: starred ? increment() : decrement(),
+ });
+ }
+ }
+
+ /**
+ * 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
+ */
+ _handleNotificationPartnerTransientMessage(data) {
+ const convertedData = this.env.models['mail.message'].convertData(data);
+ const lastMessageId = this.env.models['mail.message'].all().reduce(
+ (lastMessageId, message) => Math.max(lastMessageId, message.id),
+ 0
+ );
+ const partnerRoot = this.env.messaging.partnerRoot;
+ const message = this.env.models['mail.message'].create(Object.assign(convertedData, {
+ author: [['link', partnerRoot]],
+ id: lastMessageId + 0.01,
+ isTransient: true,
+ }));
+ this._notifyThreadViewsMessageReceived(message);
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
+ }
+
+ /**
+ * @private
+ * @param {integer} channelId
+ */
+ _handleNotificationPartnerUnsubscribe(channelId) {
+ const channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ if (!channel) {
+ return;
+ }
+ let message;
+ if (channel.correspondent) {
+ const correspondent = channel.correspondent;
+ message = _.str.sprintf(
+ this.env._t("You unpinned your conversation with <b>%s</b>."),
+ owl.utils.escape(correspondent.name)
+ );
+ } else {
+ message = _.str.sprintf(
+ this.env._t("You unsubscribed from <b>%s</b>."),
+ owl.utils.escape(channel.name)
+ );
+ }
+ // We assume that arriving here the server has effectively
+ // unpinned the channel
+ channel.update({ isServerPinned: false });
+ this.env.services['notification'].notify({
+ message,
+ type: 'warning',
+ });
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {string} param0.message
+ * @param {integer} param0.partner_id
+ * @param {string} param0.title
+ */
+ async _handleNotificationPartnerUserConnection({ message, partner_id, title }) {
+ // If the current user invited a new user, and the new user is
+ // connecting for the first time while the current user is present
+ // then open a chat for the current user with the new user.
+ this.env.services['bus_service'].sendNotification(title, message);
+ const chat = await this.async(() =>
+ this.env.messaging.getChat({ partnerId: partner_id }
+ ));
+ if (!chat || this.env.messaging.device.isMobile) {
+ return;
+ }
+ this.env.messaging.chatWindowManager.openThread(chat);
+ }
+
+ /**
+ * @private
+ * @param {Object} param0
+ * @param {mail.thread} param0.channel
+ * @param {mail.message} param0.message
+ */
+ _notifyNewChannelMessageWhileOutOfFocus({ channel, message }) {
+ const author = message.author;
+ const messaging = this.env.messaging;
+ let notificationTitle;
+ if (!author) {
+ notificationTitle = this.env._t("New message");
+ } else {
+ const authorName = author.nameOrDisplayName;
+ if (channel.channel_type === 'channel') {
+ // hack: notification template does not support OWL components,
+ // so we simply use their template to make HTML as if it comes
+ // from component
+ const channelIcon = this.env.qweb.renderToString('mail.ThreadIcon', {
+ env: this.env,
+ thread: channel,
+ });
+ const channelName = owl.utils.escape(channel.displayName);
+ const channelNameWithIcon = channelIcon + channelName;
+ notificationTitle = _.str.sprintf(
+ this.env._t("%s from %s"),
+ owl.utils.escape(authorName),
+ channelNameWithIcon
+ );
+ } else {
+ notificationTitle = owl.utils.escape(authorName);
+ }
+ }
+ const notificationContent = htmlToTextContentInline(message.body).substr(0, PREVIEW_MSG_MAX_SIZE);
+ this.env.services['bus_service'].sendNotification(notificationTitle, notificationContent);
+ messaging.update({ outOfFocusUnreadMessageCounter: increment() });
+ const titlePattern = messaging.outOfFocusUnreadMessageCounter === 1
+ ? this.env._t("%d Message")
+ : this.env._t("%d Messages");
+ this.env.bus.trigger('set_title_part', {
+ part: '_chat',
+ title: _.str.sprintf(titlePattern, messaging.outOfFocusUnreadMessageCounter),
+ });
+ }
+
+ /**
+ * Notifies threadViews about the given message being just received.
+ * This can allow them adjust their scroll position if applicable.
+ *
+ * @private
+ * @param {mail.message}
+ */
+ _notifyThreadViewsMessageReceived(message) {
+ for (const thread of message.threads) {
+ for (const threadView of thread.threadViews) {
+ threadView.addComponentHint('message-received', { message });
+ }
+ }
+ }
+
+ }
+
+ MessagingNotificationHandler.fields = {
+ messaging: one2one('mail.messaging', {
+ inverse: 'notificationHandler',
+ }),
+ };
+
+ MessagingNotificationHandler.modelName = 'mail.messaging_notification_handler';
+
+ return MessagingNotificationHandler;
+}
+
+registerNewModel('mail.messaging_notification_handler', factory);
+
+});