diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-08-11 20:30:51 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-08-13 14:17:27 +0200 |
| commit | f2302dfb27dd7f6a0dee7c65ab75b62a01706264 (patch) | |
| tree | d58fef9e2e8a5b98c0680f28fc92e24992236029 /addons/mail/static/src/js/thread_windows | |
| parent | 3de97f168956a7c22c3af6b72950cf28554f4139 (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.js | 79 | ||||
| -rw-r--r-- | addons/mail/static/src/js/thread_windows/thread_window.js | 141 |
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 */ |
