odoo.define('mail.AbstractThreadWindow', function (require) { "use strict"; var ThreadWidget = require('mail.widget.Thread'); var config = require('web.config'); var core = require('web.core'); var Widget = require('web.Widget'); var QWeb = core.qweb; var _t = core._t; var HEIGHT_OPEN = '400px'; var HEIGHT_FOLDED = '34px'; /** * This is an abstract widget for rendering thread windows. * * It contains logic that are shared between mail.ThreadWindow and * im_livechat.WebsiteLivechatWindow. * * The reason for having two different implementation of thread windows is * that mail.ThreadWindow makes use of mail.Manager, which is used in the * backend, while im_livechat.WebsiteLivechatWindow must work without this * mail service. */ var AbstractThreadWindow = Widget.extend({ template: 'mail.AbstractThreadWindow', custom_events: { escape_pressed: '_onEscapePressed', document_viewer_closed: '_onDocumentViewerClose', }, events: { 'click .o_thread_window_close': '_onClickClose', 'click .o_thread_window_title': '_onClickFold', 'click .o_thread_composer': '_onComposerClick', 'click .o_mail_thread': '_onThreadWindowClicked', 'keydown .o_thread_composer': '_onKeydown', 'keypress .o_thread_composer': '_onKeypress', }, /** * Children of this class must make use of `thread`, which is an object that * represent the thread that is linked to this thread window. * * If no thread is provided, this will represent the "blank" thread window. * * @abstract * @param {Widget} parent * @param {mail.model.AbstractThread} [thread=null] the thread that this * thread window is linked to. If not set, it is the "blank" thread * window. * @param {Object} [options={}] */ init: function (parent, thread, options) { this._super(parent); this.options = _.defaults(options || {}, { autofocus: true, displayStars: true, displayReplyIcons: false, displayEmailIcons: false, placeholder: _t("Say something"), }); this._hidden = false; this._thread = thread || null; if (!this.hasThread()) { // internal fold state of thread window without any thread this._folded = false; } }, start: function () { this.$input = this.$('.o_composer_text_field'); this.$header = this.$('.o_thread_window_header'); this.threadWidget = new ThreadWidget(this, { displayDocumentLinks: false, displayMarkAsRead: false, displayStars: this.options.displayStars, }); if (this.isFolded()) { this.$el.css('height', HEIGHT_FOLDED); } else if (this.options.autofocus) { this._focusInput(); } if (!config.device.isMobile) { var margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; this.$el.css(margin_dir, $.position.scrollbarWidth()); } var def = this.threadWidget.replace(this.$('.o_thread_window_content')); return $.when(this._super(), def); }, /** * @override */ do_hide: function () { this._hidden = true; this._super.apply(this, arguments); }, /** * @override */ do_show: function () { this._hidden = false; this._super.apply(this, arguments); }, /** * @override */ do_toggle: function (display) { this._hidden = _.isBoolean(display) ? !display : !this._hidden; this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Close this window * * @abstract */ close: function () {}, /** * Get the status of the thread, such as the im status of a DM ('online', * 'offline', etc.). If this window has no thread, returns `undefined`. * * @returns {string|undefined} */ getThreadStatus: function () { if (!this.hasThread()) { return undefined; } return this._thread.getStatus(); }, /** * Get the title of the thread window, which usually contains the name of * the thread. * * @returns {string} */ getTitle: function () { if (!this.hasThread()) { return _t("Undefined"); } return this._thread.getTitle(); }, /** * Get the unread counter of the related thread. If there are no thread * linked to this window, returns 0. * * @returns {integer} */ getUnreadCounter: function () { if (!this.hasThread()) { return 0; } return this._thread.getUnreadCounter(); }, /** * States whether this thread window is related to a thread or not. * * This is useful in order to provide specific behaviour for thread windows * without any thread, e.g. let them open a thread from this "blank" thread * window. * * @returns {boolean} */ hasThread: function () { return !! this._thread; }, /** * State whether the related thread is folded or not. If there are no * thread related to this window, it means this is the "blank" thread * window, therefore we use the internal folded state. * * @returns {boolean} */ isFolded: function () { if (!this.hasThread()) { return this._folded; } return this._thread.isFolded(); }, /** * States whether the current environment is in mobile or not. This is * useful in order to customize the template rendering for mobile view. * * @returns {boolean} */ isMobile: function () { return config.device.isMobile; }, /** * States whether the thread window is hidden or not. * * @returns {boolean} */ isHidden: function () { return this._hidden; }, /** * Render the thread window */ render: function () { this.renderHeader(); if (this.hasThread()) { this.threadWidget.render(this._thread, { displayLoadMore: false }); } }, /** * Render the header of this thread window. * This is useful when some information on the header have be updated such * as the status or the title of the thread that have changed. * * @private */ renderHeader: function () { var options = this._getHeaderRenderingOptions(); this.$header.html( QWeb.render('mail.AbstractThreadWindow.HeaderContent', options)); }, /** * Toggle the fold state of this thread window. Also update the fold state * of the thread model. If the boolean parameter `folded` is provided, it * folds/unfolds the window when it is set/unset. * * @param {boolean} [folded] if not a boolean, toggle the fold state. * Otherwise, fold/unfold the window if set/unset. */ toggleFold: function (folded) { if (!_.isBoolean(folded)) { folded = !this.isFolded(); } this._updateThreadFoldState(folded); }, /** * Update the visual state of the window so that it matched the internal * fold state. This is useful in case the related thread has its fold state * that has been changed. */ updateVisualFoldState: function () { if (!this.isFolded()) { this.threadWidget.scrollToBottom(); this._focusInput(); } this._animateFold(); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Called when there is a change of the fold state of the thread window. * This method animates the change of fold state of this thread window. * * @private */ _animateFold: function () { this.$el.animate({ height: this.isFolded() ? HEIGHT_FOLDED : HEIGHT_OPEN }, 200); }, /** * Set the focus on the composer of the thread window. This operation is * ignored in mobile context. * * @private * Set the focus on the input of the window */ _focusInput: function () { if ( config.device.touch && config.device.size_class <= config.device.SIZES.SM ) { return; } this.$input.focus(); }, /** * Returns the options used by the rendering of the window's header * * @private * @returns {Object} */ _getHeaderRenderingOptions: function () { return { status: this.getThreadStatus(), title: this.getTitle(), unreadCounter: this.getUnreadCounter(), widget: this, }; }, /** * Get the ID of the related thread. * If this window is not related to a thread, it means this is the "blank" * thread window, therefore it returns "_blank" as its ID. * * @private * @returns {integer|string} the threadID, or '_blank' for the window that * is not related to any thread. */ _getThreadID: function () { if (!this.hasThread()) { return '_blank'; } return this._thread.getID(); }, /** * Post a message on this thread window * * @abstract * @private * @param {Object} messageData */ _postMessage: function (messageData) {}, /** * Update the fold state of the thread. * This function is called when toggling the fold state of this window. * If there is no thread linked to this window, it means this is the * "blank" thread window, therefore we use the internal state 'folded' * * @private * @param {boolean} folded */ _updateThreadFoldState: function (folded) { if (this.hasThread()) { this._thread.fold(folded); } else { this._folded = folded; } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Close the thread * * @private * @param {MouseEvent} ev */ _onClickClose: function (ev) { ev.stopPropagation(); ev.preventDefault(); this.close(); }, /** * Fold/unfold the thread window * * @private */ _onClickFold: function () { if (!config.device.isMobile) { this.toggleFold(); } }, /** * Called when the composer is clicked -> forces focus on input even if * jquery's blockUI is enabled. * * @private * @param {Event} ev */ _onComposerClick: function (ev) { if ($(ev.target).closest('a, button').length) { return; } this._focusInput(); }, /** * @private */ _onDocumentViewerClose: function () { this._focusInput(); }, /** * @private */ _onEscapePressed: function () { if (!this.isFolded()) { this.close(); } }, /** * Called when typing something on the composer of this thread window. * * @private * @param {KeyboardEvent} ev */ _onKeydown: function (ev) { ev.stopPropagation(); // to prevent jquery's blockUI to cancel event // ENTER key (avoid requiring jquery ui for external livechat) if (ev.which === 13) { var content = _.str.trim(this.$input.val()); var messageData = { content: content, attachment_ids: [], partner_ids: [], }; this.$input.val(''); if (content) { this._postMessage(messageData); } } }, /** * @private * @param {KeyboardEvent} ev */ _onKeypress: function (ev) { ev.stopPropagation(); // to prevent jquery's blockUI to cancel event }, /** * When a thread window is clicked on, we want to give the focus to the main * input. An exception is made when the user is selecting something. * * @private */ _onThreadWindowClicked: function () { var selectObj = window.getSelection(); if (selectObj.anchorOffset === selectObj.focusOffset) { this.$input.focus(); } }, }); return AbstractThreadWindow; });