summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/thread_windows/thread_window.js
diff options
context:
space:
mode:
authorAlexandre Kühn <aku@odoo.com>2018-06-12 15:46:46 +0200
committerAlexandre Kühn <aku@odoo.com>2018-07-02 15:06:54 +0200
commitcd34f6de727d5b3858420cff3c9d3c5995c4e75c (patch)
tree8d25861f98a038b71728355375d634a5c178290e /addons/mail/static/src/js/thread_windows/thread_window.js
parentee9c1f52c316c83775969a3c9038e7e9f560f7e7 (diff)
[REF] mail: JS mail refactoring
---------------------- Summary ---------------------- The purpose of this commit is to improve the JS code of the `mail` module. It applies the new coding guidelines and makes some changes on the design of some modules, such as the old ChatManager module. Here is a short summary of the changes that have been made: 1. New coding guidelines - snake_case to camelCase - prefix private attributes and methods with '_' - jsdoc on most methods - one class per module 2. Rename/Merge some classes - 'chat manager' becomes 'mail manager' (internal) and 'mail service' (external) - 'chat window manager' is now included in mail manager - 'thread' widget now named 'thread widget' 3. New model abstraction for mail objects: - modules 'mail.model.*' - modeling: 0..1 0..1 * * ThreadWindow <------> Thread <-------> Message / \ / \ Thread With Cache Document Thread / | \ / | \ Mailbox Channel Support Channel | | DM - Thread: the superclass of threads. - ThreadWindow: the window component of a thread. - Message: mail objects representing messages. - Thread With Cache: threads that can be used with search view (Discuss app compatible). - Document Thread: represents the thread part of a chatter. - Mailbox: represents what was previously called 'static' channel, e.g. 'Inbox'. - Channel: mail objects representing channels, including livechat. - DM: special kind of Channel for 1:1 communication in the backend. - Support Channel: special channel for im_support module. This new modeling approach let us easily add features on all threads, such as the possibility to put any thread in a small window. ---------------------- Known issues ---------------------- [Already Present in Master] 1. When the Discuss app is in the background with 'Inbox' as the selected Thread, when clicking on a document thread preview in the messaging menu of the systray, the rainbow man appears. 2. When a document with the chatter is in the background, when receiving an inbox notification from this document thread, the document thread is automatically marked as read, which removes the notification right away. 3. Sometimes, opening a DM window from the "blank" thread window does not work. 4. Reply-to feature on Inbox is not working: no message is sent in the document thread. 5. On the first login of admin user with demo data, the inbox counter is wrong (it displays 6, instead of 3). Explanation after investigation: > On page load, it fetches the correct number of Inbox messages (3), but the server notifies of 3 needaction messages right away, so it wrongly assumes these are new needaction messages. > Not possible for web client to detect that these messages should not increment the Inbox counter while keeping same API. 6. Notifications for new document thread messages only work when the user sets 'handle with Odoo' for the Notification Management in the preferences. > due to notifications on the longpoll bus for document thread messages that come from needaction notifications. > requires server-side changes to send notification on the longpoll bus to mentionned user. [New] 7. When receiving a message on a unjoined channel, thread window flickers ('open' > 'close' > 'open') Explanation after investigation: > JS logic: a) On auto-join, ask server to join the channel and get channel infos. b) The info tells the channel is not detached, but JS code makes decision to detach it, and tells server the channel is now detached. c) From (a), server notifies on longpoll bus the state of channel, which is not detached. The web client thinks that the window state of the channel has been changed somewhere else, and the channel is now closed. d) From (b), server notifies on longpoll bus the state of channel, which is detached. The web client opens the thread window of this channel. > The flicker didn't occur before refactoring because the web client was only updating the model of channel when it receives the longpoll notification. > Server behaviour on 1st longpoll notification is necessary for cross-tab synchronization for channel window state. > New design implies that model and view should be synchronized, hence the issue now. > Solution: remove server-side thread window synchronization and replace with client-side synchronization. ---------------------- Hacks ---------------------- The module `im_livechat` now uses mail objects that are compatible with Message and Window objects: Modeling for messages: AbstractMessage / \ / \ LivechatMessage Message - AbstractMessage: message compatible with the thread widget. - LivechatMessage: message used by im_livechat. - Message: message used with the mail manager. Modeling for thread windows: AbstractThreadWindow / \ / \ LivechatWindow ThreadWindow - AbstractThreadWindow: behaviour share between all types of thread windows. - LivechatWindow: window used by im_livechat. - ThreadWindow: window used with mail_manager. The reason for these hacks are twofold: 1. Use the thread widget in the frontend and livechat external lib bundles. 2. Do not have a dependency with the mail manager in the frontend and external lib bundles.
Diffstat (limited to 'addons/mail/static/src/js/thread_windows/thread_window.js')
-rw-r--r--addons/mail/static/src/js/thread_windows/thread_window.js428
1 files changed, 428 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
new file mode 100644
index 00000000000..c0d7a22ea33
--- /dev/null
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -0,0 +1,428 @@
+odoo.define('mail.ThreadWindow', function (require) {
+"use strict";
+
+var AbstractThreadWindow = require('mail.AbstractThreadWindow');
+var BasicComposer = require('mail.composer.Basic');
+
+var core = require('web.core');
+
+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',
+ events: _.extend({}, AbstractThreadWindow.prototype.events, {
+ '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 by providing the name of a chat.
+ * @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;
+ this._thread = thread || null;
+
+ if (!this.hasThread()) {
+ // internal fold state of thread window without any thread
+ this._folded = false;
+ // remembered partner ID of "blank" thread window in order to be
+ // replaced with newly opened DM window
+ this.directPartnerID = null;
+ }
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+
+ var superDef = this._super().then(this._listenThreadWidget.bind(this));
+
+ var composerDef;
+ if (!this.hasThread()) {
+ this._startWithoutThread();
+ } else if (this._needsComposer()) {
+ var basicComposer = new BasicComposer(this, {
+ mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
+ isMini: true
+ });
+ basicComposer.on('post_message', this, this._postMessage);
+ basicComposer.once('input_focused', this, function () {
+ var commands = this._thread.getCommands();
+ var partners = this._thread.getMentionPartnerSuggestions();
+ basicComposer.mentionSetCommands(commands);
+ basicComposer.mentionSetPrefetchedPartners(partners);
+ });
+ composerDef = basicComposer.replace(this.$('.o_thread_composer'));
+ composerDef.then(function () {
+ self.$input = self.$('.o_composer_text_field');
+ });
+ }
+
+ return $.when(superDef, composerDef);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ close: function () {
+ if (this._hasThread()) {
+ this._thread.close();
+ }
+ },
+ /**
+ * Set the focus on the input of this thread window
+ */
+ focusInput: function () {
+ this._focusInput();
+ },
+ /**
+ * 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();
+ },
+ /**
+ * Get the status of the thread. If this window has no thread, returns
+ * 'undefined'
+ *
+ * @override
+ * @returns {string|undefined}
+ */
+ getStatus: function () {
+ if (!this.hasThread()) {
+ return undefined;
+ }
+ return this._thread.getStatus();
+ },
+ /**
+ * Get the title of the thread window, which is equivalent to the name of
+ * related thread, prefixed with "#" if this is a chat (i.e. DM, backend
+ * livechat).
+ *
+ * If there is no thread linked to this thread window, display title of
+ * "blank" thread window.
+ *
+ * @override
+ * @returns {string}
+ */
+ getTitle: function () {
+ if (!this.hasThread()) {
+ return _t("New message");
+ }
+ return this._thread.getTitle();
+ },
+ /**
+ * Get the unread counter of the related thread. If there are no thread
+ * linked to this window, returns 0.
+ *
+ * @override
+ * @returns {integer}
+ */
+ getUnreadCounter: function () {
+ if (!this.hasThread()) {
+ return 0;
+ }
+ return this._thread.getUnreadCounter();
+ },
+ /**
+ * @override
+ * @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.
+ *
+ * @override
+ * @returns {boolean}
+ */
+ isFolded: function () {
+ if (!this.hasThread()) {
+ return this._folded;
+ }
+ return this._thread.isFolded();
+ },
+ /**
+ * 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;
+ },
+ /**
+ * Update 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.
+ */
+ updateHeader: function () {
+ this._renderHeader();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Get the ID of the thread window, which is equivalent to 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.
+ *
+ * @override
+ * @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();
+ },
+ /**
+ * @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;
+ },
+ /**
+ * States whether the window is linked to a thread or not
+ *
+ * Useful to detect whether this is the "blank" thread window or not.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasThread: function () {
+ return !!this._thread;
+ },
+ /**
+ * 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);
+ },
+ /**
+ * States whether the input of the thread window should be displayed or not.
+ * This is based on the type of the thread:
+ *
+ * Do not display the input in the following cases:
+ *
+ * - no thread related to this window
+ * - window of a mailbox (temp: let us have mailboxes in window mode)
+ * - window of a thread with mass mailing
+ *
+ * Any other threads show the input in the window.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _needsComposer: function () {
+ return this.hasThread() &&
+ (this._thread.getType() !== 'mailbox') &&
+ !this._thread.isMassMailing();
+ },
+ /**
+ * 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 () {
+ this.call('mail_service', 'openThreadWindow', this.getID());
+ },
+ /**
+ * Post a message in the thread related to this window.
+ * If this window has no thread, do nothing
+ *
+ * @override
+ * @private
+ * @param {Object} messageData
+ */
+ _postMessage: function (messageData) {
+ var self = this;
+ if (!this._hasThread()) {
+ return;
+ }
+ this._thread.postMessage(messageData)
+ .then(function () {
+ self.threadWidget.scrollToBottom();
+ });
+ },
+ /**
+ * @private
+ */
+ _startWithoutThread: function () {
+ var self = this;
+ this.$el.addClass('o_thread_less');
+ this.$('.o_thread_search_input input')
+ .autocomplete({
+ source: function (request, response) {
+ self.call('mail_service', 'searchPartner', request.term, 10)
+ .done(response);
+ },
+ select: function (event, ui) {
+ // remember partner ID so that we can replace this window
+ // with new DM window
+ var partnerID = ui.item.id;
+ self.directPartnerID = partnerID;
+ self.call('mail_service', 'openDmWindow', partnerID);
+ },
+ })
+ .focus();
+ },
+ /**
+ * Update the fold state of the thread
+ * If there is no thread linked to this window, it means this is the
+ * "blank" thread window, therefore we use the internal state 'folded'
+ *
+ * @override
+ * @private
+ * @param {boolean} folded
+ */
+ _updateThreadFoldState: function (folded) {
+ if (this.hasThread()) {
+ this._thread.fold(folded);
+ } else {
+ this._folded = folded;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // 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._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.mail_channel_action_client_chat', {
+ clear_breadcrumbs: false,
+ active_id: this.hasThread() ? this._getThreadID() : undefined,
+ on_reverse_breadcrumb: function () {
+ self.call('mail_service', 'getMailBus')
+ .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 thread = this.call('mail_service', 'getThread', channelID);
+ if (!thread) {
+ this.call('mail_service', 'joinChannel', channelID)
+ .then(function (channel) {
+ channel.detach();
+ });
+ } else {
+ this.toggleFold(false);
+ }
+ },
+ /**
+ * @private
+ * @param {integer} messageID
+ */
+ _onToggleStarStatus: function (messageID) {
+ var message = this.call('mail_service', 'getMessage', messageID);
+ message.toggleStarStatus();
+ },
+
+});
+
+return ThreadWindow;
+
+});