diff options
Diffstat (limited to 'addons/mail/static/src/js/thread_windows')
| -rw-r--r-- | addons/mail/static/src/js/thread_windows/abstract_thread_window.js | 519 | ||||
| -rw-r--r-- | addons/mail/static/src/js/thread_windows/thread_window.js | 403 |
2 files changed, 0 insertions, 922 deletions
diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js deleted file mode 100644 index 7e39f9c7e2e..00000000000 --- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js +++ /dev/null @@ -1,519 +0,0 @@ -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; - -/** - * 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_composer_text_field': '_onComposerClick', - 'click .o_mail_thread': '_onThreadWindowClicked', - 'keydown .o_composer_text_field': '_onKeydown', - 'keypress .o_composer_text_field': '_onKeypress', - }, - FOLD_ANIMATION_DURATION: 200, // duration in ms for (un)fold transition - HEIGHT_OPEN: '400px', // height in px of thread window when open - HEIGHT_FOLDED: '34px', // height, in px, of thread window when folded - /** - * 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={}] - * @param {mail.model.AbstractThread} [options.thread] - */ - init: function (parent, thread, options) { - this._super(parent); - - this.options = _.defaults(options || {}, { - autofocus: true, - displayStars: true, - displayReplyIcons: false, - displayNotificationIcons: false, - placeholder: _t("Say something"), - }); - - this._hidden = false; - this._thread = thread || null; - - this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100); - - if (!this.hasThread()) { - // internal fold state of thread window without any thread - this._folded = false; - } - }, - start: function () { - var self = this; - this.$input = this.$('.o_composer_text_field'); - this.$header = this.$('.o_thread_window_header'); - var options = { - displayMarkAsRead: false, - displayStars: this.options.displayStars, - }; - if (this._thread && this._thread._type === 'document_thread') { - options.displayDocumentLinks = false; - } - this._threadWidget = new ThreadWidget(this, options); - - // animate the (un)folding of thread windows - this.$el.css({transition: 'height ' + this.FOLD_ANIMATION_DURATION + 'ms linear'}); - if (this.isFolded()) { - this.$el.css('height', this.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')).then(function () { - self._threadWidget.$el.on('scroll', self, self._debouncedOnScroll); - }); - return Promise.all([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 ID of the thread window, which is equivalent to the ID of the - * thread related to this window - * - * @returns {integer|string} - */ - getID: function () { - return this._getThreadID(); - }, - /** - * @returns {mail.model.Thread|undefined} - */ - getThread: function () { - if (!this.hasThread()) { - return undefined; - } - return this._thread; - }, - /** - * Get the status of the thread, such as the im status of a DM chat - * ('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; - }, - /** - * Tells whether the bottom of the thread in the thread window is visible - * or not. - * - * @returns {boolean} - */ - isAtBottom: function () { - return this._threadWidget.isAtBottom(); - }, - /** - * 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; - }, - /** - * States whether the input of the thread window should be displayed or not. - * By default, any thread window with a thread needs a composer. - * - * @returns {boolean} - */ - needsComposer: function () { - return this.hasThread(); - }, - /** - * 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)); - }, - /** - * Scroll to the bottom of the thread in the thread window - */ - scrollToBottom: function () { - this._threadWidget.scrollToBottom(); - }, - /** - * 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(); - if (this.options.autofocus) { - this._focusInput(); - } - } - var height = this.isFolded() ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; - this.$el.css({height: height}); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * 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(), - thread: this.getThread(), - 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(); - }, - /** - * Tells whether there is focus on this thread. Note that a thread that has - * the focus means the input has focus. - * - * @private - * @returns {boolean} - */ - _hasFocus: function () { - return this.$input.is(':focus'); - }, - /** - * Post a message on this thread window, and auto-scroll to the bottom of - * the thread. - * - * @private - * @param {Object} messageData - */ - _postMessage: function (messageData) { - var self = this; - if (!this.hasThread()) { - return; - } - this._thread.postMessage(messageData) - .then(function () { - self._threadWidget.scrollToBottom(); - }); - }, - /** - * 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; - this.updateVisualFoldState(); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Close the thread window. - * Mark the thread as read if the thread window was open. - * - * @private - * @param {MouseEvent} ev - */ - _onClickClose: function (ev) { - ev.stopPropagation(); - ev.preventDefault(); - if ( - this.hasThread() && - this._thread.getUnreadCounter() > 0 && - !this.isFolded() - ) { - this._thread.markAsRead(); - } - this.close(); - }, - /** - * Fold/unfold the thread window. - * Also mark the thread as read. - * - * @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 - }, - /** - * @private - */ - _onScroll: function () { - if (this.hasThread() && this.isAtBottom()) { - this._thread.markAsRead(); - } - }, - /** - * 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; - -}); diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js deleted file mode 100644 index 7280f704fa3..00000000000 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ /dev/null @@ -1,403 +0,0 @@ -odoo.define('mail.ThreadWindow', function (require) { -"use strict"; - -var AbstractThreadWindow = require('mail.AbstractThreadWindow'); -var BasicComposer = require('mail.composer.Basic'); -var ExtendedComposer = require('mail.composer.Extended'); - -var config = require('web.config'); -var core = require('web.core'); - -var QWeb = core.qweb; -var _t = core._t; - -/** - * This is the main widget for rendering small windows for mail.model.Thread. - * Almost all instances of this class are linked to a thread. The sole - * exception is the "blank" thread window. This window let us open another - * thread window, using this "blank" thread window. - */ -var ThreadWindow = AbstractThreadWindow.extend({ - template: 'mail.ThreadWindow', - custom_events: AbstractThreadWindow.prototype.custom_events, - events: _.extend({}, AbstractThreadWindow.prototype.events, { - 'click .o_mail_thread': '_onThreadWindowFocus', - 'click .o_thread_composer': '_onThreadWindowFocus', - 'click .o_thread_window_expand': '_onClickExpand', - }), - /** - * Version of thread window that supports {mail.model.Thread} - * - * @override - * @param {mail.Manager} parent - * @param {mail.model.Thread} [thread = null] if not set, this is a "blank" - * thread window. It lets us open a DM chat by providing the name of a - * user. - * @param {Object} [options={}] - * @param {boolean} [options.passively=false] - */ - init: function (parent, thread, options) { - this._super.apply(this, arguments); - - // don't automatically mark unread messages as seen when at the bottom - // of the thread - this._passive = this.options.passively || false; - - if (!this.hasThread()) { - // remembered partner ID of "blank" thread window in order to be - // replaced with newly opened DM chat window - this.directPartnerID = null; - } - }, - - /** - * @override - */ - start: function () { - var self = this; - - var superDef = this._super().then(this._listenThreadWidget.bind(this)); - - this.call('mail_service', 'getMailBus') - .on('update_typing_partners', this, this._onUpdateTypingPartners) - .on('update_channel', this, this._onUpdateChannel); - - var composerDef; - if (!this.hasThread()) { - this._startWithoutThread(); - } else if (this.needsComposer()) { - var composer; - var composerParams = { - mentionPartnersRestricted: this._thread.getType() !== 'document_thread', - thread: this._thread, - }; - if ( - config.device.isMobile && - this._thread.isMassMailing() - ) { - composer = new ExtendedComposer(this, composerParams); - } else { - composer = new BasicComposer(this, _.extend({ isMini: true }, composerParams)); - } - composer.on('post_message', this, this._postMessage); - composer.once('input_focused', this, function () { - var commands = this._thread.getCommands(); - var partners = this._thread.getMentionPartnerSuggestions(); - composer.mentionSetCommands(commands); - composer.mentionSetPrefetchedPartners(partners); - }); - composerDef = composer.replace(this.$('.o_thread_composer')); - composerDef.then(function () { - self.$input = self.$('.o_composer_text_field'); - }); - } - return Promise.all([superDef, composerDef]); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - close: function () { - if (this.hasThread()) { - this._thread.close(); - } else { - this.trigger_up('close_blank_thread_window'); - } - }, - /** - * Set the focus on the input of this thread window - */ - focusInput: function () { - this._focusInput(); - }, - /** - * Overrides so that if this thread window is not linked to any thread - * (= "blank" thread window), displays "New message" as its title. - * - * @override - * @returns {string} - */ - getTitle: function () { - if (!this.hasThread()) { - return _t("New message"); - } - return this._super.apply(this, arguments); - }, - /** - * Tell whether the thread window is passive or not. A passive thread window - * does not auto-mark the thread as read when scrolling at the bottom. - * - * @returns {boolean} - */ - isPassive: function () { - return this._passive; - }, - /** - * Turn the thread window in active mode, so that when the bottom of the - * thread is visible, it is automatically marked as read. - */ - removePassive: function () { - this._passive = false; - }, - renderOutOfOffice: function () { - var $outOfOffice = this.$('.o_out_of_office'); - const thread = this.getThread(); - if (!thread.hasOutOfOffice()) { - if ($outOfOffice.length) { - $outOfOffice.remove(); - } - return; - } - const $newOutOfOffice = $(QWeb.render('mail.thread.OutOfOffice', { thread })); - if ($outOfOffice.length) { - $outOfOffice.replaceWith($newOutOfOffice); - } else { - $newOutOfOffice.insertAfter(this.$('.o_thread_window_header')); - } - }, - /** - * Update this thread window - * - * @param {Object} options - * @param {boolean} [options.keepBottom=false] if set, this thread window - * should scroll to the bottom if it was at the bottom before update - * @param {boolean} [options.passively=false] if set, this thread window - * becomes passive, so that it is marked as read only when the focus is - * on it. Ignore this option if the focus is already on the thread window. - */ - update: function (options) { - var self = this; - if (options.passively && !this._hasFocus()) { - this._setPassive(); - } - var bottomVisible = !this.isFolded() && - !this.isHidden() && - this.isAtBottom(); - if (bottomVisible && !this.isPassive()) { - this._thread.markAsRead(); - } - this._thread.fetchMessages().then(function () { - self.render(); - if (bottomVisible && options.keepBottom) { - self.scrollToBottom(); - } - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _getHeaderRenderingOptions: function () { - var options = this._super.apply(this, arguments); - if (this._thread.getType() === 'document_thread') { - options.expandTitle = _t("Open document"); - } else { - options.expandTitle = _t("Open in Discuss"); - } - return options; - }, - /** - * Listen on thread widget events - * - * @private - */ - _listenThreadWidget: function () { - this._threadWidget - .on('redirect', this, this._onRedirect) - .on('redirect_to_channel', this, this._onRedirectToChannel) - .on('toggle_star_status', this, this._onToggleStarStatus); - }, - /** - * Open this thread window. - * This private method exists only for the purpose of providing a callback - * function on redirect to a partner. - * - * @private - */ - _open: function (channelID) { - this.call('mail_service', 'openThreadWindow', channelID || this.getID()); - }, - /** - * Set the thread window in passive mode, so that new received message will - * keep the thread window as unread until there is focus on the thread - * window. - * - * @private - */ - _setPassive: function () { - this._passive = true; - }, - /** - * @private - */ - _startWithoutThread: function () { - var self = this; - this.$el.addClass('o_thread_less'); - this.$('.o_thread_search_input input') - .autocomplete({ - autoFocus: true, - source: function (request, response) { - self.call('mail_service', 'searchPartner', request.term, 10) - .then(response); - }, - select: function (event, ui) { - // remember partner ID so that we can replace this window - // with new DM chat window - var partnerID = ui.item.id; - self.directPartnerID = partnerID; - self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID); - }, - }) - .focus(); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Expand the thread, which will open the thread in discuss. - * - * If there is no thread linked to this window, it means this is the "blank" - * thread window, therefore it opens discuss with the default thread (which - * should be the mailbox 'inbox') - * - * @private - * @param {MouseEvent} ev - */ - _onClickExpand: _.debounce(function (ev) { - var self = this; - ev.preventDefault(); - if (this.hasThread() && this._thread.getType() === 'document_thread') { - this.do_action({ - type: 'ir.actions.act_window', - res_model: this._thread.getDocumentModel(), - views: [[false, 'form']], - res_id: this._thread.getDocumentID(), - }); - } else { - this.do_action('mail.action_discuss', { - clear_breadcrumbs: false, - active_id: this.hasThread() ? this._getThreadID() : undefined, - on_reverse_breadcrumb: function () { - var mailBus = self.call('mail_service', 'getMailBus'); - if (mailBus) { - mailBus.trigger('discuss_open', false); - } - }, - }); - } - }, 1000, true), - /** - * @override - * @private - * @param {KeyboardEvent} ev - * - * Override _onKeydown to only prevent jquery's blockUI to cancel event, - * but without sending the message on ENTER keydown as this is handled by - * the BasicComposer - */ - _onKeydown: function (ev) { - ev.stopPropagation(); - }, - /** - * callback function receives the threadID linked to resModel and resID - * Note: this callback function is only called for 'res.partner' - * - * @private - * @param {string} resModel - * @param {integer} resID - */ - _onRedirect: function (resModel, resID) { - var callback = this._open.bind(this); - this.call('mail_service', 'redirect', resModel, resID, callback); - }, - /** - * @private - * @param {integer} channelID - */ - _onRedirectToChannel: function (channelID) { - var self = this; - var thread = this.call('mail_service', 'getThread', channelID); - if (!thread) { - this.call('mail_service', 'joinChannel', channelID) - .then(function (channelID) { - var channel = self.call('mail_service', 'getThread', channelID); - channel.detach(); - }); - } else { - this.toggleFold(false); - } - }, - /** - * Override it so that passive thread windows do not mark thread as read - * on scroll - * - * @override - * @private - */ - _onScroll: function () { - if (this.isPassive()) { - return; - } - return this._super.apply(this, arguments); - }, - /** - * Called when focusing the thread window (click on thread or composer) - * - * @private - */ - _onThreadWindowFocus: function () { - if (this.isPassive()) { - this.removePassive(); - this._thread.markAsRead(); - } - }, - /** - * @private - * @param {integer} messageID - */ - _onToggleStarStatus: function (messageID) { - var message = this.call('mail_service', 'getMessage', messageID); - message.toggleStarStatus(); - }, - /** - * @private - * @param {integer|string} threadID - */ - _onUpdateTypingPartners: function (threadID) { - if (!this.hasThread()) { - return; - } - if (this._thread.getID() !== threadID) { - return; - } - this.renderHeader(); - }, - /** - * @private - * @param {integer} channelID - */ - _onUpdateChannel: function (channelID) { - if (!this.hasThread()) { - return; - } - if (this._thread.getID() !== channelID) { - return; - } - this.render(); - }, -}); - -return ThreadWindow; - -}); |
