summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/thread_windows
diff options
context:
space:
mode:
authorAlexandre Kühn <aku@odoo.com>2018-08-11 20:30:51 +0200
committerAlexandre Kühn <aku@odoo.com>2018-08-13 14:17:27 +0200
commitf2302dfb27dd7f6a0dee7c65ab75b62a01706264 (patch)
treed58fef9e2e8a5b98c0680f28fc92e24992236029 /addons/mail/static/src/js/thread_windows
parent3de97f168956a7c22c3af6b72950cf28554f4139 (diff)
[REF][FIX] mail, im_livechat: various improvements (JS)
This commit makes a lot of small improvements of the JS of mail and related modules. A. Mail Manager --------------- im_support: - [REF] Slightly more robust way to determine a support message, so that it more reliably falls back on default kind of message (i.e. support channel UUID should be clearly set). mail: - [REF] Improved method names for readability purposes. Some examples: - `fetchFromServer` becomes `fetchMailStateFromServer` - `_initialize{*}` becomes `_update{*}FromServer` - `_isModerator` becomes `_isMyselfModerator` - [REF] Use 'template' pattern design to make Include on Mail Manager much more robust. In particular, the flow is always as follow: 1. Initialize internal state. 2. Listen on buses. 3. Fetch mail data from server. - [REF] Improved workflow for thread window workflow (Shorter & more readable). B. Mail Tests ------------- - [REF] Thread Window Tests crashes with clean message when there are some thread windows open before running the tests. C. (Model) Message ------------------ - [REF] Improved method names for readability purposes (e.g. `isAuthor` becomes `isMyselfAuthor`). D. (Model) Thread ----------------- - [REF] Improved `init` parameters on each kind of thread. - [REF] 'Thread With Cache' has been renamed to 'Searchable Thread': it embraces all kinds of threads that can be used with the search view. - [REF] 'DM' has been renamed to 'DM Chat' (for clarity). - [IMP] New Livechat model in the backend. - [REF] No more direct instances of the Channel class: public and private channels are now 'Multi-User' channels, while DM Chat and Livechat are 'Two-User' channels: This change has been made so that instances of classes always have direct classes as "leaf" in the Class modeling. It eases making changes to a certain model without being too constrained by another model. - [REF] As a consequence of above changed, the method `isChat` has been renamed to `isTwoUserThread`. This is to avoid confusion with the terminology 'chat', which is more like a shortcut for 'quick real- time conversation'. In other words, the property of 'chat' depends of the way to interact on a thread, not of the intrinsic type of the thread. Still, some parts have kept the terminology 'chat', such as in the systray messagin menu. - [IMP] Added `message_added` and `message_posted` hooks, which may be useful to easily hook on those events. E. Thread Widget ---------------- - [REF] Now uses a `mail.model.AbstractThread` for the rendering of a thread, instead of a list of `{mail.model.AbstractMessage}`. - [FIX] Stick thread scroll height to bottom on new rendering, if it was previously at the bottom of the thread. - [REF] Option ORDER has its meaning changed: it now refers to the chronological order of messages in the thread, in which DESC means 'from top to bottom', while ASC means 'from bottom to top'. - [REF] Improved doc on templates, in particular with the option `ORDER`. F. Thread Window ---------------- - [REF] Moved more logic to thread windows, which were previously in Mail Window Manager. - [REF] Now hides the thread widget attribute. So instead of `threadWindow.threadWidget.isAtBottom()`, it uses `threadWindow.isAtBottom()`. - [FIX] New received messages when not having focus on Odoo tab should always make the corresponding thread windows "passive" (i.e. windows that keep threads unread until click on them). - [FIX] Blank thread window (i.e. 'New message' TW) correctly opens Discuss with 'Inbox' when clicking on 'Expand' Button in the title - [FIX] Do not display the composer for channels with property 'mass-mailing' set in thread windows. G. (Website) Livechat --------------------- - [REF] Some `trigger` have been turned into `trigger_up`. - [REF] Some logic has been moved to models (e.g. `{im_livechat.model.WebsiteLivechat}`), from `{im_livechat.im_livechat.LivechatButton}`. - [FIX] New messages should be received in real-time. The longpolling was broken with a recent refactoring on bus service (6448420c5dd160470e465dee7729d19d8d5e7bab).
Diffstat (limited to 'addons/mail/static/src/js/thread_windows')
-rw-r--r--addons/mail/static/src/js/thread_windows/abstract_thread_window.js79
-rw-r--r--addons/mail/static/src/js/thread_windows/thread_window.js141
2 files changed, 161 insertions, 59 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
index 8ec8aa84b31..ffed0e16a1f 100644
--- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js
@@ -33,10 +33,10 @@ var AbstractThreadWindow = Widget.extend({
events: {
'click .o_thread_window_close': '_onClickClose',
'click .o_thread_window_title': '_onClickFold',
- 'click .o_thread_composer': '_onComposerClick',
+ 'click .o_composer_text_field': '_onComposerClick',
'click .o_mail_thread': '_onThreadWindowClicked',
- 'keydown .o_thread_composer': '_onKeydown',
- 'keypress .o_thread_composer': '_onKeypress',
+ 'keydown .o_composer_text_field': '_onKeydown',
+ 'keypress .o_composer_text_field': '_onKeypress',
},
/**
* Children of this class must make use of `thread`, which is an object that
@@ -50,6 +50,7 @@ var AbstractThreadWindow = Widget.extend({
* 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);
@@ -65,16 +66,19 @@ var AbstractThreadWindow = Widget.extend({
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');
- this.threadWidget = new ThreadWidget(this, {
+ this._threadWidget = new ThreadWidget(this, {
displayDocumentLinks: false,
displayMarkAsRead: false,
displayStars: this.options.displayStars,
@@ -89,7 +93,9 @@ var AbstractThreadWindow = Widget.extend({
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'));
+ var def = this._threadWidget.replace(this.$('.o_thread_window_content')).then(function () {
+ self._threadWidget.$el.on('scroll', self, self._debouncedOnScroll);
+ });
return $.when(this._super(), def);
},
/**
@@ -125,8 +131,9 @@ var AbstractThreadWindow = Widget.extend({
*/
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`.
+ * 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}
*/
@@ -173,6 +180,15 @@ var AbstractThreadWindow = Widget.extend({
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.
@@ -203,12 +219,21 @@ var AbstractThreadWindow = Widget.extend({
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 });
+ this._threadWidget.render(this._thread, { displayLoadMore: false });
}
},
/**
@@ -224,6 +249,12 @@ var AbstractThreadWindow = Widget.extend({
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.
@@ -244,7 +275,7 @@ var AbstractThreadWindow = Widget.extend({
*/
updateVisualFoldState: function () {
if (!this.isFolded()) {
- this.threadWidget.scrollToBottom();
+ this._threadWidget.scrollToBottom();
this._focusInput();
}
this._animateFold();
@@ -311,13 +342,22 @@ var AbstractThreadWindow = Widget.extend({
return this._thread.getID();
},
/**
- * Post a message on this thread window
+ * Post a message on this thread window, and auto-scroll to the bottom of
+ * the thread.
*
- * @abstract
* @private
* @param {Object} messageData
*/
- _postMessage: function (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.
@@ -334,6 +374,13 @@ var AbstractThreadWindow = Widget.extend({
this._folded = folded;
}
},
+ /**
+ * Warn other mail components that the unread counter has been updated
+ *
+ * @abstract
+ * @private
+ */
+ _warnUpdatedUnreadCounter: function () {},
//--------------------------------------------------------------------------
// Handlers
@@ -417,6 +464,14 @@ var AbstractThreadWindow = Widget.extend({
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.
*
diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js
index 1ff9417d127..1d38ee338d3 100644
--- a/addons/mail/static/src/js/thread_windows/thread_window.js
+++ b/addons/mail/static/src/js/thread_windows/thread_window.js
@@ -16,7 +16,10 @@ var _t = core._t;
*/
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',
}),
/**
@@ -25,7 +28,8 @@ var ThreadWindow = AbstractThreadWindow.extend({
* @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.
+ * thread window. It lets us open a DM chat by providing the name of a
+ * user.
* @param {Object} [options={}]
* @param {boolean} [options.passively=false]
*/
@@ -34,11 +38,11 @@ var ThreadWindow = AbstractThreadWindow.extend({
// don't automatically mark unread messages as seen when at the bottom
// of the thread
- this._passive = this.options.passively;
+ 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 window
+ // replaced with newly opened DM chat window
this.directPartnerID = null;
}
},
@@ -54,10 +58,11 @@ var ThreadWindow = AbstractThreadWindow.extend({
var composerDef;
if (!this.hasThread()) {
this._startWithoutThread();
- } else if (this._needsComposer()) {
+ } else if (this.needsComposer()) {
var basicComposer = new BasicComposer(this, {
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
- isMini: true
+ isMini: true,
+ thread: this._thread,
});
basicComposer.on('post_message', this, this._postMessage);
basicComposer.once('input_focused', this, function () {
@@ -85,6 +90,8 @@ var ThreadWindow = AbstractThreadWindow.extend({
close: function () {
if (this.hasThread()) {
this._thread.close();
+ } else {
+ this.trigger_up('close_blank_thread_window');
}
},
/**
@@ -125,12 +132,66 @@ var ThreadWindow = AbstractThreadWindow.extend({
return this._passive;
},
/**
+ * 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.
+ *
+ * @override
+ * @returns {boolean}
+ */
+ needsComposer: function () {
+ return this._super() && !this._thread.isMassMailing();
+ },
+ /**
* 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;
},
+ /**
+ * 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.
+ */
+ setPassive: function () {
+ this._passive = true;
+ },
+ /**
+ * 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.
+ */
+ update: function (options) {
+ var self = this;
+ if (options.passively) {
+ 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
@@ -155,32 +216,12 @@ var ThreadWindow = AbstractThreadWindow.extend({
* @private
*/
_listenThreadWidget: function () {
- this.threadWidget
+ 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.
@@ -191,24 +232,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
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 () {
@@ -222,10 +245,10 @@ var ThreadWindow = AbstractThreadWindow.extend({
},
select: function (event, ui) {
// remember partner ID so that we can replace this window
- // with new DM window
+ // with new DM chat window
var partnerID = ui.item.id;
self.directPartnerID = partnerID;
- self.call('mail_service', 'openDmWindow', partnerID);
+ self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID);
},
})
.focus();
@@ -248,7 +271,7 @@ var ThreadWindow = AbstractThreadWindow.extend({
_onClickExpand: _.debounce(function (ev) {
var self = this;
ev.preventDefault();
- if (this._thread.getType() === 'document_thread') {
+ if (this.hasThread() && this._thread.getType() === 'document_thread') {
this.do_action({
type: 'ir.actions.act_window',
res_model: this._thread.getDocumentModel(),
@@ -306,6 +329,30 @@ var ThreadWindow = AbstractThreadWindow.extend({
}
},
/**
+ * 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
*/