summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-09-29 18:50:23 +0000
committerSébastien Theys <seb@odoo.com>2020-09-29 19:47:12 +0000
commitfb9b1c8a169840951925fa810f71e865a54e3a31 (patch)
tree591c34b02dfdde95ebaf296db7abfee5614e22ea
parentb4783500db5b7c645859227e8be5cd55025b2882 (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>
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js33
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js63
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js151
-rw-r--r--addons/mail/static/src/models/composer/composer.js3
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js23
-rw-r--r--addons/mail/static/src/models/thread_view/thread_view.js14
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js4
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"
);