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/mail/static/src/components | |
| 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/mail/static/src/components')
3 files changed, 223 insertions, 24 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" + ); +}); + }); }); }); |
