diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-09-29 18:50:23 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-09-29 19:47:12 +0000 |
| commit | fb9b1c8a169840951925fa810f71e865a54e3a31 (patch) | |
| tree | 591c34b02dfdde95ebaf296db7abfee5614e22ea /addons | |
| parent | b4783500db5b7c645859227e8be5cd55025b2882 (diff) | |
[FIX] mail: reintroduce auto scroll on receiving message
task-2336251
closes odoo/odoo#58867
X-original-commit: f732980b2e5e83bcf2fdc8d3ce756f256cacbd4f
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'addons')
7 files changed, 259 insertions, 32 deletions
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js index cbd7c8136a1..67d52c33c88 100644 --- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js +++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js @@ -1809,7 +1809,20 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { "should not display 'new messages' separator" ); // scroll to top - document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0; + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0; + }, + message: "should wait until channel scrolled to top", + predicate: ({ scrollTop, threadViewer }) => { + return ( + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 && + scrollTop === 0 + ); + }, + }); // composer is focused by default, we remove that focus document.querySelector('.o_ComposerTextInput_textarea').blur(); // simulate receiving a message @@ -1835,9 +1848,21 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { '.o_MessageList_separatorNewMessages', "should display 'new messages' separator" ); - await afterNextRender(() => { - document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = - document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollHeight; + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; + }, + message: "should wait until channel scrolled to bottom", + predicate: ({ scrollTop, threadViewer }) => { + const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`); + return ( + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 20 && + scrollTop === messageList.scrollHeight - messageList.clientHeight + ); + }, }); assert.containsOnce( document.body, diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js index bc1ab67bbfc..72368ec3689 100644 --- a/addons/mail/static/src/components/message_list/message_list.js +++ b/addons/mail/static/src/components/message_list/message_list.js @@ -118,15 +118,15 @@ class MessageList extends Component { case 'change-of-thread-cache': this._adjustFromChangeOfThreadCache(hint); break; - case 'current-partner-just-posted-message': - this._adjustFromCurrentPartnerJustPostedMessage(hint); - break; case 'home-menu-hidden': this._adjustFromHomeMenuHidden(hint); break; case 'home-menu-shown': this._adjustFromHomeMenuShown(hint); break; + case 'message-received': + this._adjustFromMessageReceived(hint); + break; case 'more-messages-loaded': this._adjustFromMoreMessagesLoaded(hint); break; @@ -352,28 +352,17 @@ class MessageList extends Component { /** * @private * @param {Object} hint - * @param {Object} hint.data - * @param {integer} hint.data.messageId */ - async _adjustFromCurrentPartnerJustPostedMessage(hint) { - const threadCache = this.threadView.threadCache; - if (threadCache.isLoaded && hint.data) { - const { messageId } = hint.data; - const threadCacheMessageIds = threadCache.messages.map(message => message.id); - if (threadCacheMessageIds.includes(messageId) && this.messageRefFromId(messageId)) { - if (this.props.hasScrollAdjust) { - await this._scrollToMessage(messageId); - } - this.threadView.markComponentHintProcessed(hint); - } - } + _adjustFromHomeMenuHidden(hint) { + this._adjustScrollFromModel(); + this.threadView.markComponentHintProcessed(hint); } /** * @private * @param {Object} hint */ - _adjustFromHomeMenuHidden(hint) { + _adjustFromHomeMenuShown(hint) { this._adjustScrollFromModel(); this.threadView.markComponentHintProcessed(hint); } @@ -382,8 +371,34 @@ class MessageList extends Component { * @private * @param {Object} hint */ - _adjustFromHomeMenuShown(hint) { - this._adjustScrollFromModel(); + async _adjustFromMessageReceived(hint) { + const threadCache = this.threadView.threadCache; + if (!threadCache.isLoaded) { + return; + } + const { message } = hint.data; + if (!threadCache.messages.includes(message)) { + return; + } + if (!this.messageRefFromId(message.id)) { + return; + } + if (!this.props.hasScrollAdjust) { + this.threadView.markComponentHintProcessed(hint); + return; + } + if (!this.threadView.hasAutoScrollOnMessageReceived) { + this.threadView.markComponentHintProcessed(hint); + return; + } + if ( + this.threadView.lastVisibleMessage && + (message.id < this.threadView.lastVisibleMessage.id) + ) { + this.threadView.markComponentHintProcessed(hint); + return; + } + await this._scrollToMessage(message.id); this.threadView.markComponentHintProcessed(hint); } @@ -537,6 +552,14 @@ class MessageList extends Component { scrollTop, threadViewer: this.threadView.threadViewer, }); + // Margin to compensate for inaccurate scrolling to bottom. + const margin = 4; + // Automatically scroll to new received messages only when the list is + // currently fully scrolled. + const hasAutoScrollOnMessageReceived = (this.props.order === 'asc') + ? scrollTop >= this.el.scrollHeight - this.el.clientHeight - margin + : scrollTop <= margin; + this.threadView.update({ hasAutoScrollOnMessageReceived }); this.threadView.threadViewer.saveThreadCacheScrollPositionsAsInitial(scrollTop); if (!this._isAutoLoadOnScrollActive) { return; diff --git a/addons/mail/static/src/components/thread_view/thread_view_tests.js b/addons/mail/static/src/components/thread_view/thread_view_tests.js index 4aa2d195686..9dc5145c121 100644 --- a/addons/mail/static/src/components/thread_view/thread_view_tests.js +++ b/addons/mail/static/src/components/thread_view/thread_view_tests.js @@ -786,6 +786,157 @@ QUnit.test('basic rendering of canceled notification', async function (assert) { ); }); +QUnit.test('should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)', async function (assert) { + assert.expect(2); + + // Needed partner & user to allow simulation of message reception + this.data['res.partner'].records.push({ + id: 11, + name: "Foreigner partner", + }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 11, + }); + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i <= 10; i++) { + this.data['mail.message'].records.push({ + channel_ids: [20], + }); + } + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => this.createThreadViewComponent( + threadViewer.threadView, + { order: 'asc' }, + { isFixedSize: true }, + ), + message: "should wait until channel 20 scrolled initially", + predicate: data => threadViewer === data.threadViewer, + }); + const initialMessageList = document.querySelector('.o_ThreadView_messageList'); + assert.strictEqual( + initialMessageList.scrollTop, + initialMessageList.scrollHeight - initialMessageList.clientHeight, + "should have scrolled to bottom of channel 20 initially" + ); + + // simulate receiving a message + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => + this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + uuid: thread.uuid, + message_content: "hello", + }, + }), + message: "should wait until channel 20 scrolled after receiving a message", + predicate: data => threadViewer === data.threadViewer, + }); + const messageList = document.querySelector('.o_ThreadView_messageList'); + assert.strictEqual( + messageList.scrollTop, + messageList.scrollHeight - messageList.clientHeight, + "should scroll to bottom on receiving new message because the list is initially scrolled to bottom" + ); +}); + +QUnit.test('should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)', async function (assert) { + assert.expect(3); + + // Needed partner & user to allow simulation of message reception + this.data['res.partner'].records.push({ + id: 11, + name: "Foreigner partner", + }); + this.data['res.users'].records.push({ + id: 42, + name: "Foreigner user", + partner_id: 11, + }); + this.data['mail.channel'].records.push({ id: 20 }); + for (let i = 0; i <= 10; i++) { + this.data['mail.message'].records.push({ + channel_ids: [20], + }); + } + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => this.createThreadViewComponent( + threadViewer.threadView, + { order: 'asc' }, + { isFixedSize: true }, + ), + message: "should wait until channel 20 scrolled initially", + predicate: data => threadViewer === data.threadViewer, + }); + const initialMessageList = document.querySelector('.o_ThreadView_messageList'); + assert.strictEqual( + initialMessageList.scrollTop, + initialMessageList.scrollHeight - initialMessageList.clientHeight, + "should have scrolled to bottom of channel 20 initially" + ); + + await this.afterEvent({ + eventName: 'o-component-message-list-scrolled', + func: () => initialMessageList.scrollTop = 0, + message: "should wait until channel 20 processed manual scroll", + predicate: data => threadViewer === data.threadViewer, + }); + assert.strictEqual( + initialMessageList.scrollTop, + 0, + "should have scrolled to the top of channel 20 manually" + ); + + // simulate receiving a message + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => + this.env.services.rpc({ + route: '/mail/chat_post', + params: { + context: { + mockedUserId: 42, + }, + uuid: thread.uuid, + message_content: "hello", + }, + }), + message: "should wait until channel 20 processed new message hint", + predicate: data => threadViewer === data.threadViewer && data.hint.type === 'message-received', + }); + assert.strictEqual( + document.querySelector('.o_ThreadView_messageList').scrollTop, + 0, + "should not scroll on receiving new message because the list is initially scrolled anywhere else than bottom" + ); +}); + }); }); }); diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js index e2a7ab4fb83..7ea2e5fbc60 100644 --- a/addons/mail/static/src/models/composer/composer.js +++ b/addons/mail/static/src/models/composer/composer.js @@ -291,7 +291,8 @@ function factory(dependencies) { thread.loadNewMessages(); } for (const threadView of this.thread.threadViews) { - threadView.addComponentHint('current-partner-just-posted-message', { messageId }); + // Reset auto scroll to be able to see the newly posted message. + threadView.update({ hasAutoScrollOnMessageReceived: true }); } thread.refreshFollowers(); thread.fetchAndUpdateSuggestedRecipients(); diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js index ede9245602d..6eb06ee01af 100644 --- a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js +++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js @@ -207,6 +207,7 @@ function factory(dependencies) { } const message = this.env.models['mail.message'].insert(convertedData); + this._notifyThreadViewsMessageReceived(message); // If the message was already known: nothing else should be done, // except if it was pending moderation by the current partner, then @@ -299,7 +300,7 @@ function factory(dependencies) { // knowledge of the channel return; } - const lastMessage = this.env.models['mail.message'].insert({id: last_message_id}); + const lastMessage = this.env.models['mail.message'].insert({ id: last_message_id }); // restrict computation of seen indicator for "non-channel" channels // for performance reasons const shouldComputeSeenIndicators = channel.channel_type !== 'channel'; @@ -631,7 +632,7 @@ function factory(dependencies) { starredMailbox.update({ counter: starred ? starredMailbox.counter + 1 - : starredMailbox.counter -1, + : starredMailbox.counter - 1, }); } } @@ -648,11 +649,12 @@ function factory(dependencies) { const convertedData = this.env.models['mail.message'].convertData(data); const messageIds = this.env.models['mail.message'].all().map(message => message.id); const partnerRoot = this.env.messaging.partnerRoot; - this.env.models['mail.message'].create(Object.assign(convertedData, { + const message = this.env.models['mail.message'].create(Object.assign(convertedData, { author: [['link', partnerRoot]], id: (messageIds ? Math.max(...messageIds) : 0) + 0.01, isTransient: true, })); + this._notifyThreadViewsMessageReceived(message); } /** @@ -756,6 +758,21 @@ function factory(dependencies) { }); } + /** + * Notifies threadViews about the given message being just received. + * This can allow them adjust their scroll position if applicable. + * + * @private + * @param {mail.message} + */ + _notifyThreadViewsMessageReceived(message) { + for (const thread of message.threads) { + for (const threadView of thread.threadViews) { + threadView.addComponentHint('message-received', { message }); + } + } + } + } MessagingNotificationHandler.fields = { diff --git a/addons/mail/static/src/models/thread_view/thread_view.js b/addons/mail/static/src/models/thread_view/thread_view.js index f2c64dabe2c..ee53e616b1c 100644 --- a/addons/mail/static/src/models/thread_view/thread_view.js +++ b/addons/mail/static/src/models/thread_view/thread_view.js @@ -47,8 +47,8 @@ function factory(dependencies) { markComponentHintProcessed(hint) { let filterFun; switch (hint.type) { - case 'current-partner-just-posted-message': - filterFun = h => h.type !== hint.type && h.messageId !== hint.data.messageId; + case 'message-received': + filterFun = h => h.type !== hint.type && h.message !== hint.data.message; break; default: filterFun = h => h.type !== hint.type; @@ -57,6 +57,10 @@ function factory(dependencies) { this.update({ componentHintList: this.componentHintList.filter(filterFun), }); + this.env.messagingBus.trigger('o-thread-view-hint-processed', { + hint, + threadViewer: this.threadViewer, + }); } /** @@ -197,6 +201,12 @@ function factory(dependencies) { isPreparingLoading: attr({ default: false, }), + /** + * Determines whether `this` should automatically scroll on receiving + * a new message. Detection of new message is done through the component + * hint `message-received`. + */ + hasAutoScrollOnMessageReceived: attr(), lastMessage: many2one('mail.message', { related: 'thread.lastMessage', }), diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js index 88c95bdd748..8817664497c 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js @@ -821,7 +821,7 @@ QUnit.test('Form view not scrolled when switching record', async function (asser ); await afterNextRender(async () => { - controllerContentEl.scrollTop = controllerContentEl.scrollHeight - controllerContentEl.offsetHeight; + controllerContentEl.scrollTop = controllerContentEl.scrollHeight - controllerContentEl.clientHeight; await triggerEvent( document.querySelector('.o_ThreadView_messageList'), 'scroll' @@ -829,7 +829,7 @@ QUnit.test('Form view not scrolled when switching record', async function (asser }); assert.strictEqual( controllerContentEl.scrollTop, - controllerContentEl.scrollHeight - controllerContentEl.offsetHeight, + controllerContentEl.scrollHeight - controllerContentEl.clientHeight, "The controller container should be scrolled to its bottom" ); |
