odoo.define('mail.composer.Basic', function (require) {
"use strict";

var emojis = require('mail.emojis');
var MentionManager = require('mail.composer.MentionManager');
var DocumentViewer = require('mail.DocumentViewer');
var utils = require('web.utils');

var config = require('web.config');
var core = require('web.core');
var data = require('web.data');
var dom = require('web.dom');
var session = require('web.session');
var Widget = require('web.Widget');

var QWeb = core.qweb;
var _t = core._t;

var BasicComposer = Widget.extend({
    template: "mail.Composer",
    events: {
        'change input.o_input_file': '_onAttachmentChange',
        'click .o_attachment_delete': '_onAttachmentDelete',
        'click .o_attachment_download': '_onAttachmentDownload',
        'click .o_attachment_view': '_onAttachmentView',
        'click .o_composer_button_add_attachment': '_onClickAddAttachment',
        'click .o_composer_button_emoji': '_onEmojiButtonClick',
        'focusout .o_composer_button_emoji': '_onEmojiButtonFocusout',
        'mousedown .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick',
        'focus .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageFocus',
        'dragover .o_file_drop_zone_container': '_onFileDragover',
        'drop .o_file_drop_zone_container': '_onFileDrop',
        'input .o_input': '_onInput',
        'keydown .o_composer_input textarea': '_onKeydown',
        'keyup .o_composer_input': '_onKeyup',
        'click .o_composer_button_send': '_sendMessage',
    },
    // RPCs done to fetch the mention suggestions are throttled with the
    // following value
    MENTION_THROTTLE: 200,

    init: function (parent, options) {
        this._super.apply(this, arguments);
        this.options = _.defaults(options || {}, {
            commandsEnabled: true,
            context: {},
            inputMinHeight: 28,
            mentionFetchLimit: 8,
            // set to true to only suggest prefetched partners
            mentionPartnersRestricted: false,
            sendText: _t("Send"),
            defaultBody: '',
            defaultMentionSelections: {},
            isMobile: config.device.isMobile
        });
        this.context = this.options.context;

        // Attachments
        this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
        this.fileuploadID = _.uniqueId('o_chat_fileupload');
        this.set('attachment_ids', options.attachmentIds || []);

        // Mention
        this._mentionManager = new MentionManager(this);
        this._mentionManager.register({
            delimiter: '@',
            fetchCallback: this._mentionFetchPartners.bind(this),
            generateLinks: true,
            model: 'res.partner',
            redirectClassname: 'o_mail_redirect',
            selection: this.options.defaultMentionSelections['@'],
            suggestionTemplate: 'mail.MentionSuggestions.Partner',
        });
        this._mentionManager.register({
            delimiter: '#',
            fetchCallback: this._mentionFetchChannels.bind(this),
            generateLinks: true,
            model: 'mail.channel',
            redirectClassname: 'o_channel_redirect',
            selection: this.options.defaultMentionSelections['#'],
            suggestionTemplate: 'mail.MentionSuggestions.Channel',
        });
        this._mentionManager.register({
            delimiter: ':',
            fetchCallback: this._mentionGetCannedResponses.bind(this),
            selection: this.options.defaultMentionSelections[':'],
            suggestionTemplate: 'mail.MentionSuggestions.CannedResponse',
        });
        if (this.options.commandsEnabled) {
            this._mentionManager.register({
                beginningOnly: true,
                delimiter: '/',
                fetchCallback: this._mentionGetCommands.bind(this),
                selection: this.options.defaultMentionSelections['/'],
                suggestionTemplate: 'mail.MentionSuggestions.Command',
            });
        }

        this.isMini = options.isMini;

        this.avatarURL = session.uid > 0 ? session.url('/web/image', {
            model: 'res.users',
            field: 'image_128',
            id: session.uid,
        }) : '/web/static/src/img/user_menu_avatar.png';
    },
    start: function () {
        var self = this;

        this._$attachmentButton = this.$('.o_composer_button_add_attachment');
        this._$attachmentsList = this.$('.o_composer_attachments_list');
        this.$input = this.$('.o_composer_input textarea');
        this.$input.focus(function () {
            self.trigger('input_focused');
        });
        this.$input.val(this.options.defaultBody);
        dom.autoresize(this.$input, {
            parent: this,
            min_height: this.options.inputMinHeight
        });

        // Attachments
        this._renderAttachments();
        $(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
        this.on('change:attachment_ids', this, this._renderAttachments);

        this.call('mail_service', 'getMailBus')
            .on('update_typing_partners', this, this._onUpdateTypingPartners);

        // Mention
        var prependPromise = this._mentionManager.prependTo(this.$('.o_composer'));

        // Drag-Drop files
        // Allowing body to detect dragenter and dragleave for display
        var $body = $('body');
        this._dropZoneNS = _.uniqueId('o_dz_');  // For event namespace used when multiple chat window is open
        $body.on('dragleave.' + this._dropZoneNS, this._onBodyFileDragLeave.bind(this));
        $body.on("dragover." + this._dropZoneNS, this._onBodyFileDragover.bind(this));
        $body.on("drop." + this._dropZoneNS, this._onBodyFileDrop.bind(this));
        return Promise.all([this._super(), prependPromise]);
    },
    destroy: function () {
        $("body").off('dragleave.' + this._dropZoneNS);
        $("body").off('dragover.' + this._dropZoneNS);
        $("body").off('drop.' + this._dropZoneNS);
        $(window).off(this.fileuploadID);
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Empty input, selected partners and attachments
     */
    clearComposer: function () {
        this.$input.val('');
        this._mentionManager.resetSelections();
        this.set('attachment_ids', []);
    },
    /**
     * Set the focus on the input
     */
    focus: function () {
        this.$input.focus();
    },
    /**
     * Get cursor position and selection
     *
     * @returns {Object} a current cursor position as { start: {integer}, end: {integer} }
    */
   getSelectionPositions: function () {
        var InputElement = this.$input.get(0);
        return InputElement ? dom.getSelectionRange(InputElement) : { start: 0, end: 0 };
    },
    /**
     * Get the state of the composer.
     *
     * This is useful in order to (re)store its state when switchng to another
     * thread with a composer, and coming back to the thread with this composer.
     *
     * @returns {Object}
     */
    getState: function () {
        return {
            attachments: this.get('attachment_ids'),
            text: this.$input.val(),
        };
    },
    /**
     * @returns {integer} it returns length of attachment and trim content of
     *   input box to check for empty.
     */
    isEmpty: function () {
        return !this.$input.val().trim() &&
                !this.$('.o_attachments').children().length;
    },
    /**
     * Set the state of the composer.
     *
     * This is useful in order to (re)store its state when switching to another
     * thread with a composer, and coming back to the thread with this composer.
     *
     * @param {Object} state
     * @param {Array} state.attachments
     * @param {string} state.text
     */
    setState: function (state) {
        this.set('attachment_ids', state.attachments);
        this.$input.val(state.text);
    },
    /**
     * Set the thread that this composer refers to.
     *
     * @param {mail.model.Thread} thread
     */
    setThread: function (thread) {
        this.options.thread = thread;
    },
    /**
     * Set the list of command suggestions on the thread.
     *
     * When the user partially types a command, matching commands are displayed
     * to the user (@see _mentionGetCommands).
     *
     * @param {Object[]} commands
     */
    mentionSetCommands: function (commands) {
        this._mentionCommands = commands;
    },
    /**
     * Set the list of partner mentions suggestions that has been prefetched.
     *
     * When the user partially types a mention, matching prefetched partners are
     * displayed to the user. If none of them match, then it will fetch for more
     * partner suggestions (@see _mentionFetchPartners).
     *
     * @param {Promise<Object[]>} prefetchedPartners list of list of
     *   prefetched partners.
     */
    mentionSetPrefetchedPartners: function (prefetchedPartners) {
        this._mentionPrefetchedPartners = prefetchedPartners;
    },
    /**
     * @returns {Object} containing listener selections, grouped by delimiter
     *   as key of the object.
     */
    getMentionListenerSelections: function () {
        return this._mentionManager.getListenerSelections();
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     */
    _clearComposerOnSend: function () {
        this.clearComposer();
    },
    /**
     * @private
     */
    _doCheckAttachmentUpload: function () {
        if (_.find(this.get('attachment_ids'), function (file) { return file.upload; })) {
            this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
            return false;
        }
        return true;
    },
    /**
     * Hides the emojis container.
     *
     * @private
     */
    _hideEmojis: function () {
        this._$emojisContainer.remove();
    },
    /**
     * Making sure that dragging content is external files.
     * Ignoring other content draging like text.
     *
     * @private
     * @param {DataTransfer} dataTransfer
     * @returns {boolean}
     */
    _isDragSourceExternalFile: function (dataTransfer) {
        var DragDataType = dataTransfer.types;
        if (DragDataType.constructor === DOMStringList) {
            return DragDataType.contains('Files');
        }
        if (DragDataType.constructor === Array) {
            return DragDataType.indexOf('Files') !== -1;
        }
        return false;
    },
    /**
     * @private
     * @param {string} search
     * @returns {Promise<Object[]>}
     */
    _mentionGetCannedResponses: function (search) {
        var self = this;
        var def = new Promise(function (resolve, reject) {
            clearTimeout(self._cannedTimeout);
            self._cannedTimeout = setTimeout(function () {
                var cannedResponses = self.call('mail_service', 'getCannedResponses');
                var matches = fuzzy.filter(utils.unaccent(search), _.pluck(cannedResponses, 'source'));
                var indexes = _.pluck(matches.slice(0, self.options.mentionFetchLimit), 'index');
                resolve(_.map(indexes, function (index) {
                    return cannedResponses[index];
                }));
            }, 500);
        });
        return def;
    },
    /**
     * @private
     * @param {string} search
     * @returns {Array}
     */
    _mentionGetCommands: function (search) {
        var searchRegexp = new RegExp(_.str.escapeRegExp(utils.unaccent(search)), 'i');
        return _.filter(this._mentionCommands, function (command) {
            return searchRegexp.test(command.name);
        }).slice(0, this.options.mentionFetchLimit);
    },
    /**
     * @private
     * @param {string} search
     */
    _mentionFetchChannels: function (search) {
        return this._mentionFetchThrottled('mail.channel', 'get_mention_suggestions', {
            limit: this.options.mentionFetchLimit,
            search: search,
        }).then(function (suggestions) {
            return _.partition(suggestions, function (suggestion) {
                return _.contains(['public', 'groups'], suggestion.public);
            });
        });
    },
    /**
     * @private
     * @param {string} search
     */
    _mentionFetchPartners: function (search) {
        var self = this;
        return Promise.resolve(this._mentionPrefetchedPartners).then(function (prefetchedPartners) {
            // filter prefetched partners with the given search string
            var suggestions = [];
            var limit = self.options.mentionFetchLimit;
            var searchRegexp = new RegExp(_.str.escapeRegExp(utils.unaccent(search)), 'i');
            _.each(prefetchedPartners, function (partners) {
                if (limit > 0) {
                    var filteredPartners = _.filter(partners, function (partner) {
                        return partner.email && searchRegexp.test(partner.email) ||
                            partner.name && searchRegexp.test(utils.unaccent(partner.name));
                    });
                    if (filteredPartners.length) {
                        suggestions.push(filteredPartners.slice(0, limit));
                        limit -= filteredPartners.length;
                    }
                }
            });
            if (!suggestions.length && !self.options.mentionPartnersRestricted) {
                // no result found among prefetched partners, fetch other suggestions
                suggestions = self._mentionFetchThrottled(
                    'res.partner',
                    'get_mention_suggestions',
                    { limit: limit, search: search }
                );
            }
            return Promise.resolve(suggestions).then(function (suggestions) {
                //add im_status on suggestions
                _.each(suggestions, function (suggestionsSet) {
                    _.each(suggestionsSet, function (suggestion) {
                        suggestion.im_status = self.call('mail_service', 'getImStatus', { partnerID: suggestion.id });
                    });
                });
                return suggestions;
            });
        });
    },
    /**
     * @private
     * @param {string} model
     * @param {string} method
     * @param {Object} kwargs
     * @return {Promise}
     */
    _mentionFetchThrottled: function (model, method, kwargs) {
        var self = this;
        // Delays the execution of the RPC to prevent unnecessary RPCs when the user is still typing
        return new Promise(function (resolve, reject) {
            clearTimeout(self.mentionFetchTimer);
            self.mentionFetchTimer = setTimeout(function () {
                return self._rpc({model: model, method: method, kwargs: kwargs})
                .then(function (results) {
                    resolve(results);
                });
            }, self.MENTION_THROTTLE);
        });
    },
    /**
     * @private
     * @returns {Promise}
     */
    _preprocessMessage: function () {
        // Return a promise as this function is extended with asynchronous
        // behavior for the chatter composer

        //Removing unwanted extra spaces from message
        var value = _.escape(this.$input.val()).trim();
        value = value.replace(/(\r|\n){2,}/g, '<br/><br/>');
        value = value.replace(/(\r|\n)/g, '<br/>');

        // prevent html space collapsing
        value = value.replace(/ /g, '&nbsp;').replace(/([^>])&nbsp;([^<])/g, '$1 $2');
        var commands = this.options.commandsEnabled ?
                        this._mentionManager.getListenerSelection('/') :
                        [];
        return Promise.resolve({
            content: this._mentionManager.generateLinks(value),
            attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
            partner_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('@'), 'id')),
            canned_response_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelections()[':'], 'id')),
            channel_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('#'), 'id')),
            command: commands.length > 0 ? commands[0].name : undefined,
        });
    },
    /**
     * Allowing to upload attachment with file selector as well drag drop feature.
     *
     * @private
     * @param {Array<File>} params.files
     * @param {boolean} params.submitForm [optional]
     */
    _processAttachmentChange: function (params) {
        var self = this;
        var attachments = this.get('attachment_ids');
        var files = params.files;
        var submitForm = params.submitForm;
        var $form = this.$('form.o_form_binary_form');

        /**
         * makes a new formData as formData.delete() is not supported by IE or Safari Mobile.
         *
         * @return {FormData}
         */
        function makeFormDataWithoutUfile() {
            var newFormData = new window.FormData();
            $form.find('input').each(function (index, input) {
                if (input.name !== 'ufile') {
                    newFormData.append(input.name, input.value);
                }
            });
            return newFormData;
        }

        _.each(files, function (file) {
            var attachment = _.findWhere(attachments, {
                name: file.name,
                size: file.size
            });
            // if the files already exits, delete the file before upload
            if (attachment) {
                self._attachmentDataSet.unlink([attachment.id]);
                attachments = _.without(attachments, attachment);
            }
        });
        if (submitForm) {
            $form.submit();
            this._$attachmentButton.prop('disabled', true);
        } else {
            _.each(files, function (file) {
                var formData = makeFormDataWithoutUfile();
                formData.append("ufile", file, file.name);
                $.ajax({
                    url: $form.attr("action"),
                    type: "POST",
                    enctype: 'multipart/form-data',
                    processData: false,
                    contentType: false,
                    data: formData,
                    success: function (result) {
                        var $el = $(result);
                        $.globalEval($el.contents().text());
                    }
                });
            });
        }
        var uploadAttachments = _.map(files, function (file){
            return {
                id: 0,
                name: file.name,
                filename: file.name,
                url: '',
                upload: true,
                mimetype: '',
            };
        });
        attachments = attachments.concat(uploadAttachments);
        this.set('attachment_ids', attachments);
    },
    /**
     * @private
     */
    _renderAttachments: function () {
        this._$attachmentsList.html(QWeb.render('mail.composer.Attachments', {
            attachments: this.get('attachment_ids'),
        }));
    },
    /**
     * @private
     */
    _sendMessage: function () {
        if (this.isEmpty() || !this._doCheckAttachmentUpload()) {
            return;
        }

        clearTimeout(this._cannedTimeout);
        var self = this;
        this._preprocessMessage().then(function (message) {
            self.trigger('post_message', message);
            self._clearComposerOnSend();
            self.$input.focus();
        });
    },
    /**
     * Send the message on ENTER, but go to new line on SHIFT+ENTER
     *
     * @private
     */
    _shouldSend: function (ev) {
        return !ev.shiftKey;
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {jQuery.Event} ev
     */
    _onAttachmentChange: function (ev) {
        this._processAttachmentChange({
            files: ev.currentTarget.files,
            submitForm: true
        });
        ev.target.value = "";
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onAttachmentDelete: function (ev){
        ev.stopPropagation();
        var self = this;
        var attachmentID = $(ev.currentTarget).data('id');
        if (attachmentID) {
            var attachments = [];
            _.each(this.get('attachment_ids'), function (attachment){
                if (attachmentID !== attachment.id) {
                    attachments.push(attachment);
                } else {
                    self._attachmentDataSet.unlink([attachmentID]);
                }
            });
            this.set('attachment_ids', attachments);
            this.$('input.o_input_file').val('');
        }
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onAttachmentDownload: function (ev) {
        ev.stopPropagation();
    },
    /**
     * @private
     */
    _onAttachmentLoaded: function () {
        var attachments = this.get('attachment_ids');
        var files = Array.prototype.slice.call(arguments, 1);

        _.each(files, function (file) {
            if (file.error || !file.id){
                this.do_warn(file.error);
                attachments = _.filter(attachments, function (attachment) {
                    return !attachment.upload;
                });
            } else {
                var attachment = _.findWhere(attachments, { filename: file.filename, upload: true });
                if (attachment) {
                    attachments = _.without(attachments, attachment);
                    attachments.push({
                        id: file.id,
                        name: file.name || file.filename,
                        filename: file.filename,
                        mimetype: file.mimetype,
                        size: file.size,
                        url: session.url('/web/content', { id: file.id, download: true }),
                    });
                }
            }
        }.bind(this));
        this.set('attachment_ids', attachments);
        this._$attachmentButton.prop('disabled', false);
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onAttachmentView: function (ev) {
        var activeAttachmentID = $(ev.currentTarget).data('id');
        var attachments = this.get('attachment_ids');
        if (activeAttachmentID) {
            var attachmentViewer = new DocumentViewer(this, attachments, activeAttachmentID);
            attachmentViewer.appendTo($('body'));
        }
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onBodyFileDragLeave: function (ev) {
        // On every dragenter chain created with parent child element
        // That's why dragleave is fired every time when a child elemnt is hovered
        // so here we hide dropzone based on mouse position
        if (ev.originalEvent.clientX <= 0
            || ev.originalEvent.clientY <= 0
            || ev.originalEvent.clientX >= window.innerWidth
            || ev.originalEvent.clientY >= window.innerHeight
        ) {
            this.$(".o_file_drop_zone_container").addClass("d-none");
        }
    },
    /**
     * When user start dragging on element drop area will be visible to drop selected files.
     *
     * @private
     * @param {MouseEvent} ev
     */
    _onBodyFileDragover: function (ev) {
        ev.preventDefault();
        if (this._isDragSourceExternalFile(ev.originalEvent.dataTransfer)) {
            this.$(".o_file_drop_zone_container").removeClass("d-none");
        }
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onBodyFileDrop: function (ev) {
        ev.preventDefault();
        ev.stopPropagation();
        this.$(".o_file_drop_zone_container").addClass("d-none");
    },
    /**
     * @private
     */
    _onClickAddAttachment: function () {
        this.$('input.o_input_file').click();
        this.$input.focus();
    },
    /**
     * Called when the emoji button is clicked -> opens/hides the emoji panel.
     * Also, this method is in charge of the rendering of this panel the first
     * time it is opened.
     *
     * @private
     */
    _onEmojiButtonClick: function () {
        if (!this._$emojisContainer) { // lazy rendering
            this._$emojisContainer = $(QWeb.render('mail.Composer.emojis', {
                emojis: emojis,
            }));
        }
        if (this._$emojisContainer.parent().length) {
            this._hideEmojis();
        } else {
            this._$emojisContainer.appendTo(this.$('.o_composer'));
        }
    },
    /**
     * Called when the emoji button is blurred -> closes the emoji panel. The
     * closing is scheduled to be done at the end of the current execution
     * stack to allow stoping this closing if the button was focusout to select
     * an emoji (for example).
     *
     * @private
     */
    _onEmojiButtonFocusout: function () {
        if (this._$emojisContainer) {
            this._hideEmojisTimeout = setTimeout(this._hideEmojis.bind(this), 0);
        }
    },
    /**
     * Called when an emoji is clicked from the emoji panel.
     * Emoji is inserted in the composer based on the position of the cursor,
     * and it automatically focuses the composer and set the cursor position
     * just after the newly inserted emoji.
     *
     * @private
     * @param {Event} ev
     */
    _onEmojiImageClick: function (ev) {
        ev.preventDefault();
        var cursorPosition = this.getSelectionPositions();
        var inputVal = this.$input.val();
        var leftSubstring = inputVal.substring(0, cursorPosition.start);
        var rightSubstring = inputVal.substring(cursorPosition.end);
        var newInputVal  = [leftSubstring , $(ev.currentTarget).data('emoji'), rightSubstring].join(" ");
        var newCursorPosition = newInputVal.length - rightSubstring.length;
        this.$input.val(newInputVal);
        this.$input.focus();
        this.$input[0].setSelectionRange(newCursorPosition, newCursorPosition);
        this._hideEmojis();
    },
    /**
     * Called when an emoji is focused -> @see _onEmojiButtonFocusout
     *
     * @private
     */
    _onEmojiImageFocus: function () {
        clearTimeout(this._hideEmojisTimeout);
    },
    /**
     * Setting drop Effect to copy so when mouse pointer on dropzone
     * cursor icon changed to copy ('+')
     *
     * @private
     * @param {MouseEvent} ev
     */
    _onFileDragover: function (ev) {
        ev.originalEvent.dataTransfer.dropEffect = "copy";
    },
    /**
     * Called when user drop selected files on drop area
     *
     * @private
     * @param {MouseEvent} ev
     */
    _onFileDrop: function (ev) {
        ev.preventDefault();
        // FIX: In case multiple chat windows are opened, and file droped in one of them
        // at that time, other chat windows are still displaing drop areas so here hide them all with $ selector
        $(".o_file_drop_zone_container").addClass("d-none");
        if (this._isDragSourceExternalFile(ev.originalEvent.dataTransfer)) {
            var files = ev.originalEvent.dataTransfer.files;
            this._processAttachmentChange({ files: files });
        }
    },
    /**
     * Called when the input in the composer changes
     *
     * @private
     */
    _onInput: function () {
        if (this.options.thread && this.options.thread.hasTypingNotification()) {
            var isTyping = this.$input.val().length > 0;
            this.options.thread.setMyselfTyping({ typing: isTyping });
        }
    },
    /**
     * _onKeydown event is triggered when is key is pressed
     *      - on UP and DOWN arrow is pressed then event prevents it's default
     *              behaviour if mention manager is open else it break it.
     *      - on ENTER key is pressed and mention manager is open then event
     *              prevents it's default behaviour else check if ControlKey is
     *              pressed or not if yes then it send a message
     *
     * @private
     * @param {KeyboardEvent} ev
    */
    _onKeydown: function (ev) {
        switch (ev.which) {
            // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
            case $.ui.keyCode.UP:
            case $.ui.keyCode.DOWN:
            case $.ui.keyCode.TAB:
                if (this._mentionManager.isOpen()) {
                    ev.preventDefault();
                }
                break;
            // ENTER: submit the message only if the dropdown mention proposition is not displayed
            case $.ui.keyCode.ENTER:
                if (this._mentionManager.isOpen()) {
                    ev.preventDefault();
                } else {
                    var sendMessage = ev.ctrlKey || this._shouldSend(ev);
                    if (sendMessage) {
                        ev.preventDefault();
                        this._sendMessage();
                    }
                }
                break;
        }
    },
    /**
     * _onKeyup event is triggered when key is released.
     * on ESCAP key it close the mention suggestion dropdown menu.
     * on ENTER key it selects that mention and add to a content editable div.
     * on UP or DOWN key it highlights previous or next mention respectively.
     *
     * @private
     * @param {KeyboardEvent} ev
    */
    _onKeyup: function (ev) {
        switch (ev.which) {
            // ESCAPED KEYS: do nothing
            case $.ui.keyCode.END:
            case $.ui.keyCode.PAGE_UP:
            case $.ui.keyCode.PAGE_DOWN:
                break;
            // ESCAPE: close mention propositions
            case $.ui.keyCode.ESCAPE:
                if (this._mentionManager.isOpen()) {
                    ev.stopPropagation();
                    this._mentionManager.resetSuggestions();
                } else {
                    this.trigger_up('escape_pressed');
                }
                break;
            // ENTER, UP, DOWN, TAB: check if navigation in mention propositions
            case $.ui.keyCode.ENTER:
            case $.ui.keyCode.UP:
            case $.ui.keyCode.DOWN:
            case $.ui.keyCode.TAB:
                if (this._mentionManager.isOpen()) {
                    this._mentionManager.propositionNavigation(ev.which);
                }
                break;
            // Otherwise, check if a mention is typed
            default:
                this._mentionManager.detectDelimiter();
        }
    },
    /**
     * @private
     * @param {integer|string} threadID
     */
    _onUpdateTypingPartners: function (threadID) {
        if (!this.options.showTyping) {
            return;
        }
        if (!this.options.thread) {
            return;
        }
        if (this.options.thread.getID() !== threadID) {
            return;
        }
        this.$('.o_composer_thread_typing').html(QWeb.render('mail.Composer.ThreadTyping', {
            thread: this.options.thread,
        }));
    },
});

return BasicComposer;

});
