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 { 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() {
            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));
                    default:
                        console.warn(`mail.messaging_notification_handler: Unhandled notification "${model}"`);
                        return;
                }
            });
            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,
            });
            this.env.models['mail.message_seen_indicator'].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: moderation.counter - 1 });
                }
                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'
            ) {
                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();
                }
                // (re)open chat on receiving new message
                if (channel.channel_type !== 'channel') {
                    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';
            if (shouldComputeSeenIndicators) {
                this.env.models['mail.thread_partner_seen_info'].insert({
                    channelId: channel.id,
                    lastSeenMessage: [['link', lastMessage]],
                    partnerId: partner_id,
                });
                this.env.models['mail.message_seen_indicator'].insert({
                    channelId: channel.id,
                    messageId: lastMessage.id,
                });
            }
            if (this.env.messaging.currentPartner.id === partner_id) {
                channel.update({
                    lastSeenByCurrentPartnerMessageId: last_message_id,
                    pendingSeenMessageId: undefined,
                });
            }
            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)
            );
            const inboxMailbox = this.env.messaging.inbox;
            inboxMailbox.update({ counter: inboxMailbox.counter + 1 });
            for (const thread of message.threads) {
                if (
                    thread.channel_type === 'channel' &&
                    message.isNeedaction
                ) {
                    thread.update({ message_needaction_counter: thread.message_needaction_counter + 1 });
                }
            }
            // 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 escapedTitle = owl.utils.escape(data.title);
                const escapedMessage = owl.utils.escape(data.message);
                this.env.services['notification'].notify({
                    message: escapedMessage,
                    sticky: data.sticky,
                    title: escapedTitle,
                    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)
                    ),
                    title: this.env._t("Invitation"),
                    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'].find(message => message.id === id);
                if (message) {
                    if (
                        message.moderation_status === 'pending_moderation' &&
                        message.originThread.isModeratedByCurrentPartner
                    ) {
                        moderationMailbox.update({ counter: moderationMailbox.counter - 1 });
                    }
                    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=[]]
         */
        _handleNotificationPartnerMarkAsRead({ channel_ids, message_ids = [] }) {
            const inboxMailbox = this.env.messaging.inbox;

            // 1. move messages from inbox to history
            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'].find(m => m.id === message_id);
                if (message) {
                    message.update({
                        isNeedaction: false,
                        isHistory: true,
                    });
                }
            }

            // 2. remove "needaction" from channels
            let channels;
            if (channel_ids) {
                channels = channel_ids
                    .map(id => this.env.models['mail.thread'].find(thread =>
                        thread.id === id &&
                        thread.model === 'mail.channel'
                    ))
                    .filter(thread => !!thread);
            } else {
                // flux specific: channel_ids unset means "mark all as read"
                channels = this.env.models['mail.thread'].all(thread =>
                    thread.model === 'mail.channel'
                );
            }
            for (const channel of channels) {
                channel.update({ message_needaction_counter: 0 });
            }
            inboxMailbox.update({ counter: inboxMailbox.counter - message_ids.length });

            // 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: moderationMailbox.counter + 1 });
            }
            // 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'].find(message =>
                    message.id === messageId
                );
                if (!message) {
                    continue;
                }
                message.update({ isStarred: starred });
                starredMailbox.update({
                    counter: starred
                        ? starredMailbox.counter + 1
                        : starredMailbox.counter - 1,
                });
            }
        }

        /**
         * 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'].find(thread =>
                thread.id === channelId &&
                thread.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,
                title: this.env._t("Unsubscribed"),
                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) {
                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: messaging.outOfFocusUnreadMessageCounter + 1 });
            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);

});
