summaryrefslogtreecommitdiff
path: root/addons/mail/static/src
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-11-16 12:36:47 +0000
committerSébastien Theys <seb@odoo.com>2020-11-16 12:36:47 +0000
commit2002c6a599a685f4a18fc670a9f7992c8d595c0f (patch)
treee4d273d6cd86496d68e9ebd02c3849d9f52d95d1 /addons/mail/static/src
parent8b32e558fde3cb3aaa1deb40acb07488d04d343e (diff)
[FIX] mail: fix race conditions, especially with scroll
The `_update` of some components must take place before the `_update` of other (usually sibling) components that might directly influence their layout. This is in particular the case for `_update` that are relying on height. Prevent force save of scroll position if there are pending hints to adjust it, and prevent pending hints from overriding new scroll value. Prevent flicker with message date. This should fix all known issues related to incorrectly saving or restoring scroll position. task-2344226 task-2358066 task-2369332 task-2372339 task-2373741 closes odoo/odoo#61054 Related: odoo/enterprise#14736 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/mail/static/src')
-rw-r--r--addons/mail/static/src/bugfix/bugfix.js66
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.js18
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js134
-rw-r--r--addons/mail/static/src/components/chatter/chatter.js13
-rw-r--r--addons/mail/static/src/components/chatter_container/chatter_container.js10
-rw-r--r--addons/mail/static/src/components/composer/composer.js10
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js10
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion.js10
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.js25
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.xml2
-rw-r--r--addons/mail/static/src/components/discuss/discuss.js7
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js5
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js272
-rw-r--r--addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js14
-rw-r--r--addons/mail/static/src/components/emojis_popover/emojis_popover.js11
-rw-r--r--addons/mail/static/src/components/message/message.js27
-rw-r--r--addons/mail/static/src/components/message/message.xml2
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js34
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view.js13
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js270
-rw-r--r--addons/mail/static/src/models/message/message.js29
-rw-r--r--addons/mail/static/src/models/thread/thread.js19
-rw-r--r--addons/mail/static/src/models/thread_cache/thread_cache.js8
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js41
24 files changed, 673 insertions, 377 deletions
diff --git a/addons/mail/static/src/bugfix/bugfix.js b/addons/mail/static/src/bugfix/bugfix.js
index 5a7474b5c59..d4128a9b236 100644
--- a/addons/mail/static/src/bugfix/bugfix.js
+++ b/addons/mail/static/src/bugfix/bugfix.js
@@ -8,3 +8,69 @@ odoo.define('mail/static/src/bugfix/bugfix.js', function (require) {
'use strict';
});
+
+// Should be moved to its own file in master.
+odoo.define('mail/static/src/component_hooks/use_update/use_update.js', function (require) {
+'use strict';
+
+const { Component } = owl;
+const { onMounted, onPatched } = owl.hooks;
+
+const executionQueue = [];
+
+function executeNextInQueue() {
+ if (executionQueue.length === 0) {
+ return;
+ }
+ const { component, func } = executionQueue.shift();
+ if (!component.__owl__.isDestroyed) {
+ func();
+ }
+ executeNextInQueue();
+}
+
+/**
+ * @param {Object} param0
+ * @param {Component} param0.component
+ * @param {function} param0.func
+ * @param {integer} param0.priority
+ */
+async function addFunctionToQueue({ component, func, priority }) {
+ const index = executionQueue.findIndex(item => item.priority > priority);
+ const item = { component, func, priority };
+ if (index === -1) {
+ executionQueue.push(item);
+ } else {
+ executionQueue.splice(index, 0, item);
+ }
+ // Timeout to allow all components to register their function before
+ // executing any of them, to respect all priorities.
+ await new Promise(resolve => setTimeout(resolve));
+ executeNextInQueue();
+}
+
+/**
+ * This hook provides support for executing code after update (render or patch).
+ *
+ * @param {Object} param0
+ * @param {function} param0.func the function to execute after the update.
+ * @param {integer} [param0.priority] determines the execution order of the function
+ * among the update function of other components. Lower priority is executed
+ * first. If no priority is given, the function is executed immediately.
+ */
+function useUpdate({ func, priority }) {
+ const component = Component.current;
+ onMounted(onUpdate);
+ onPatched(onUpdate);
+ function onUpdate() {
+ if (priority === undefined) {
+ func();
+ return;
+ }
+ addFunctionToQueue({ component, func, priority });
+ }
+}
+
+return useUpdate;
+
+});
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js
index 28e7f08a1fb..27d6bb68b68 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.js
+++ b/addons/mail/static/src/components/chat_window/chat_window.js
@@ -7,6 +7,7 @@ const components = {
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { isEventHandled } = require('mail/static/src/utils/utils.js');
const { Component } = owl;
@@ -29,6 +30,7 @@ class ChatWindow extends Component {
thread: thread ? thread.__state : undefined,
};
});
+ useUpdate({ func: () => this._update() });
/**
* Reference of the header of the chat window.
* Useful to prevent click on header from wrongly focusing the window.
@@ -55,11 +57,6 @@ class ChatWindow extends Component {
mounted() {
this.env.messagingBus.on('will_hide_home_menu', this, this._onWillHideHomeMenu.bind(this));
this.env.messagingBus.on('will_show_home_menu', this, this._onWillShowHomeMenu.bind(this));
- this._update();
- }
-
- patched() {
- this._update();
}
willUnmount() {
@@ -132,7 +129,16 @@ class ChatWindow extends Component {
* @private
*/
_saveThreadScrollTop() {
- if (!this._threadRef.comp || !this.chatWindow.threadViewer) {
+ if (
+ !this._threadRef.comp ||
+ !this.chatWindow.threadViewer ||
+ !this.chatWindow.threadViewer.threadView
+ ) {
+ return;
+ }
+ if (this.chatWindow.threadViewer.threadView.componentHintList.length > 0) {
+ // the current scroll position is likely incorrect due to the
+ // presence of hints to adjust it
return;
}
this.chatWindow.threadViewer.saveThreadCacheScrollHeightAsInitial(
diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
index b70f61475dd..c0a6bf1295f 100644
--- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
+++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
@@ -795,11 +795,8 @@ QUnit.test('[technical] chat window: composer state conservation on toggle home
QUnit.test('[technical] chat window: scroll conservation on toggle home menu', async function (assert) {
// technical as show/hide home menu simulation are involved and home menu implementation
// have side-effects on DOM that may make chat window components not work
- assert.expect(3);
-
+ assert.expect(2);
- // channel that is expected to be found in the messaging menu
- // with random unique id that is needed to link messages
this.data['mail.channel'].records.push({ id: 20 });
for (let i = 0; i < 10; i++) {
this.data['mail.message'].records.push({
@@ -809,9 +806,19 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
}
await this.start();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
- await afterNextRender(() =>
- document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click()
- );
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector('.o_NotificationList_preview').click(),
+ message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
// Set a scroll position to chat window
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -827,12 +834,6 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
);
},
});
- assert.strictEqual(
- document.querySelector(`.o_ThreadView_messageList`).scrollTop,
- 142,
- "chat window initial scrollTop should be 142px"
- );
-
await afterNextRender(() => this.hideHomeMenu());
assert.strictEqual(
document.querySelector(`.o_ThreadView_messageList`).scrollTop,
@@ -843,7 +844,7 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
func: () => this.showHomeMenu(),
- message: "should wait until channel 20 restored its scroll to 142 after hiding the home menu",
+ message: "should wait until channel 20 restored its scroll to 142 after showing the home menu",
predicate: ({ scrollTop, threadViewer }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
@@ -1573,9 +1574,19 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f
}
await this.start();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
- await afterNextRender(() =>
- document.querySelector(`.o_NotificationList_preview`).click()
- );
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector('.o_NotificationList_preview').click(),
+ message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
// Set a scroll position to chat window
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -1750,7 +1761,7 @@ QUnit.test('[technical] chat window: composer state conservation on toggle home
QUnit.test('[technical] chat window with a thread: keep scroll position in message list on toggle home menu when folded', async function (assert) {
// technical as show/hide home menu simulation are involved and home menu implementation
// have side-effects on DOM that may make chat window components not work
- assert.expect(3);
+ assert.expect(2);
// channel that is expected to be found in the messaging menu
// with random unique id, needed to link messages
@@ -1763,22 +1774,48 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
}
await this.start();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
- await afterNextRender(() =>
- document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click()
- );
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector('.o_NotificationList_preview').click(),
+ message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
// Set a scroll position to chat window
- document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142;
- assert.strictEqual(
- document.querySelector(`.o_ThreadView_messageList`).scrollTop,
- 142,
- "should have scrolled to 142px"
- );
-
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142,
+ message: "should wait until channel 20 scrolled to 142 after setting this value manually",
+ predicate: ({ scrollTop, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === 142
+ );
+ },
+ });
// fold chat window
await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click());
await this.hideHomeMenu();
// unfold chat window
- await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click());
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector('.o_ChatWindow_header').click(),
+ message: "should wait until channel 20 restored its scroll to 142 after unfolding it",
+ predicate: ({ scrollTop, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === 142
+ );
+ },
+ });
assert.strictEqual(
document.querySelector(`.o_ThreadView_messageList`).scrollTop,
142,
@@ -1790,7 +1827,18 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
// Show home menu
await this.showHomeMenu();
// unfold chat window
- await afterNextRender(() => document.querySelector('.o_ChatWindow_header').click());
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => document.querySelector('.o_ChatWindow_header').click(),
+ message: "should wait until channel 20 restored its scroll position to the last saved value (142)",
+ predicate: ({ scrollTop, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === 142
+ );
+ },
+ });
assert.strictEqual(
document.querySelector(`.o_ThreadView_messageList`).scrollTop,
142,
@@ -1925,8 +1973,8 @@ QUnit.test('new message separator is shown in a chat window of a chat on receivi
});
this.data['mail.channel'].records = [
{
- id: 10,
channel_type: "chat",
+ id: 10,
is_minimized: true,
is_pinned: false,
members: [this.data.currentPartnerId, 10],
@@ -1951,8 +1999,8 @@ QUnit.test('new message separator is shown in a chat window of a chat on receivi
context: {
mockedUserId: 42,
},
- uuid: 'channel-10-uuid',
message_content: "hu",
+ uuid: 'channel-10-uuid',
},
}));
assert.containsOnce(
@@ -1985,28 +2033,28 @@ QUnit.test('focusing a chat window of a chat should make new message separator d
name: "Foreigner user",
partner_id: 10,
});
- this.data['mail.channel'].records = [
+ this.data['mail.channel'].records.push(
{
- id: 10,
channel_type: "chat",
+ id: 10,
is_minimized: true,
is_pinned: false,
members: [this.data.currentPartnerId, 10],
message_unread_counter: 0,
uuid: 'channel-10-uuid',
},
- ];
+ );
await this.start();
// simulate receiving a message
- await afterNextRender(async () => this.env.services.rpc({
+ await afterNextRender(() => this.env.services.rpc({
route: '/mail/chat_post',
params: {
context: {
mockedUserId: 42,
},
- uuid: 'channel-10-uuid',
message_content: "hu",
+ uuid: 'channel-10-uuid',
},
}));
assert.containsOnce(
@@ -2015,7 +2063,17 @@ QUnit.test('focusing a chat window of a chat should make new message separator d
"should display 'new messages' separator in the conversation, from reception of new messages"
);
- await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus());
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => document.querySelector('.o_ComposerTextInput_textarea').focus(),
+ message: "should wait until last seen by current partner message id changed",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 10 &&
+ thread.model === 'mail.channel'
+ );
+ },
+ }));
assert.containsNone(
document.body,
'.o_MessageList_separatorNewMessages',
diff --git a/addons/mail/static/src/components/chatter/chatter.js b/addons/mail/static/src/components/chatter/chatter.js
index d4314108b87..8c19c3ace07 100644
--- a/addons/mail/static/src/components/chatter/chatter.js
+++ b/addons/mail/static/src/components/chatter/chatter.js
@@ -9,6 +9,7 @@ const components = {
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -37,6 +38,7 @@ class Chatter extends Component {
attachments: 1,
},
});
+ useUpdate({ func: () => this._update() });
/**
* Reference of the composer. Useful to focus it.
*/
@@ -47,14 +49,6 @@ class Chatter extends Component {
this._threadRef = useRef('thread');
}
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -84,6 +78,9 @@ class Chatter extends Component {
* @private
*/
_update() {
+ if (!this.chatter) {
+ return;
+ }
if (this.chatter.thread) {
this._notifyRendered();
}
diff --git a/addons/mail/static/src/components/chatter_container/chatter_container.js b/addons/mail/static/src/components/chatter_container/chatter_container.js
index 39331939da8..19c847d01d9 100644
--- a/addons/mail/static/src/components/chatter_container/chatter_container.js
+++ b/addons/mail/static/src/components/chatter_container/chatter_container.js
@@ -5,6 +5,7 @@ const components = {
Chatter: require('mail/static/src/components/chatter/chatter.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { clear } = require('mail/static/src/model/model_field_command.js');
const { Component } = owl;
@@ -37,10 +38,7 @@ class ChatterContainer extends Component {
}
return { chatter: this.chatter };
});
- }
-
- mounted() {
- this._update();
+ useUpdate({ func: () => this._update() });
}
/**
@@ -53,10 +51,6 @@ class ChatterContainer extends Component {
return super.willUpdateProps(...arguments);
}
- patched() {
- this._update();
- }
-
/**
* @override
*/
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
index 6c9965a57d1..4434697f8fb 100644
--- a/addons/mail/static/src/components/composer/composer.js
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -11,6 +11,7 @@ const components = {
ThreadTextualTypingStatus: require('mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js'),
};
const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const {
isEventHandled,
@@ -38,6 +39,7 @@ class Composer extends Component {
: undefined,
};
});
+ useUpdate({ func: () => this._update() });
/**
* Reference of the emoji popover. Useful to include emoji popover as
* contained "inside" the composer.
@@ -61,11 +63,6 @@ class Composer extends Component {
mounted() {
document.addEventListener('click', this._onClickCaptureGlobal, true);
- this._update();
- }
-
- patched() {
- this._update();
}
willUnmount() {
@@ -201,6 +198,9 @@ class Composer extends Component {
* @private
*/
_update() {
+ if (!this.composer) {
+ return;
+ }
if (this._subjectRef.el) {
this._subjectRef.el.value = this.composer.subjectContent;
}
diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
index cbc215a3b7e..f9b3944454b 100644
--- a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
+++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/composer_suggested_recipient/composer_su
'use strict';
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { FormViewDialog } = require('web.view_dialogs');
const { ComponentAdapter } = require('web.OwlCompatibility');
@@ -38,6 +39,7 @@ class ComposerSuggestedRecipient extends Component {
suggestedRecipientInfo: suggestedRecipientInfo && suggestedRecipientInfo.__state,
};
});
+ useUpdate({ func: () => this._update() });
/**
* Form view dialog class. Useful to reference it in the template.
*/
@@ -62,14 +64,6 @@ class ComposerSuggestedRecipient extends Component {
this._onDialogSaved = this._onDialogSaved.bind(this);
}
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js
index 80043eaf057..308dee76821 100644
--- a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion.
'use strict';
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const components = {
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
@@ -24,14 +25,7 @@ class ComposerSuggestion extends Component {
record: record ? record.__state : undefined,
};
});
- }
-
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
+ useUpdate({ func: () => this._update() });
}
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
index c2cc133417a..5afb99c4990 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.js
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/composer_text_input/composer_text_input.
'use strict';
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const components = {
ComposerSuggestionList: require('mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js'),
@@ -29,6 +30,11 @@ class ComposerTextInput extends Component {
};
});
/**
+ * Updates the composer text input content when composer is mounted
+ * as textarea content can't be changed from the DOM.
+ */
+ useUpdate({ func: () => this._update() });
+ /**
* Last content of textarea from input event. Useful to determine
* whether the current partner is typing something.
*/
@@ -39,22 +45,6 @@ class ComposerTextInput extends Component {
this._textareaRef = useRef('textarea');
}
- /**
- * Updates the composer text input content when composer is mounted
- * as textarea content can't be changed from the DOM.
- */
- mounted() {
- this._update();
- }
-
- /**
- * Updates the composer text input content when composer has changed
- * as textarea content can't be changed from the DOM.
- */
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -153,6 +143,9 @@ class ComposerTextInput extends Component {
* @private
*/
_update() {
+ if (!this.composer) {
+ return;
+ }
this._textareaRef.el.value = this.composer.textInputContent;
this._textareaRef.el.setSelectionRange(
this.composer.textInputCursorStart,
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.xml b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
index dfa7bbd1182..493acbb1b4f 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
@@ -10,7 +10,7 @@
isBelow="props.hasMentionSuggestionsBelowPosition"
/>
</t>
- <textarea class="o_ComposerTextInput_textarea" t-att-class="{ 'o-composer-is-compact': props.isCompact }" t-esc="composer.textInputContent" t-att-placeholder="textareaPlaceholder" t-on-focusin="_onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="_onKeydownTextarea" t-on-keyup="_onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
+ <textarea class="o_ComposerTextInput_textarea" t-att-class="{ 'o-composer-is-compact': props.isCompact }" style="height: 0;" t-esc="composer.textInputContent" t-att-placeholder="textareaPlaceholder" t-on-focusin="_onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="_onKeydownTextarea" t-on-keyup="_onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
</t>
</div>
</t>
diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js
index 377fde0dd72..75904de8dfd 100644
--- a/addons/mail/static/src/components/discuss/discuss.js
+++ b/addons/mail/static/src/components/discuss/discuss.js
@@ -13,6 +13,7 @@ const components = {
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -44,6 +45,7 @@ class Discuss extends Component {
uncheckedMessages: 1,
},
});
+ useUpdate({ func: () => this._update() });
this._updateLocalStoreProps();
/**
* Reference of the composer. Useful to focus it.
@@ -66,7 +68,6 @@ class Discuss extends Component {
this.discuss.openInitThread();
}
this._updateLocalStoreProps();
- this._update();
}
patched() {
@@ -85,7 +86,6 @@ class Discuss extends Component {
}
this._activeThreadCache = this.discuss.threadView && this.discuss.threadView.threadCache;
this._updateLocalStoreProps();
- this._update();
}
willUnmount() {
@@ -146,6 +146,9 @@ class Discuss extends Component {
* @private
*/
_update() {
+ if (!this.discuss) {
+ return;
+ }
if (this.discuss.isDoFocus) {
this.discuss.update({ isDoFocus: false });
const composer = this._composerRef.comp;
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js
index 21146652d28..26431207467 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js
@@ -63,6 +63,7 @@ QUnit.test('discuss should filter messages based on given domain', async functio
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
+ hint.data.fetchedMessages.length === 1 &&
threadViewer.thread.model === 'mail.box' &&
threadViewer.thread.id === 'inbox'
);
@@ -197,8 +198,8 @@ QUnit.test('discuss should refresh filtered thread on receiving new message', as
func: () => this.env.services.rpc({
route: '/mail/chat_post',
params: {
- uuid: channel.uuid,
message_content: "test",
+ uuid: channel.uuid,
},
}),
message: "should wait until channel 20 refreshed its filtered message list",
@@ -294,8 +295,8 @@ QUnit.test('discuss should refresh filtered thread on changing thread', async fu
await this.env.services.rpc({
route: '/mail/chat_post',
params: {
- uuid: channel20.uuid,
message_content: "test",
+ uuid: channel20.uuid,
},
});
assert.containsNone(
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 481e78681bc..ff7fcd47f8e 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -1706,6 +1706,18 @@ QUnit.test('auto-scroll to bottom of thread', async function (assert) {
default_active_id: 'mail.channel_20',
},
},
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
});
assert.strictEqual(
document.querySelectorAll(`
@@ -1716,8 +1728,8 @@ QUnit.test('auto-scroll to bottom of thread', async function (assert) {
);
const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
assert.strictEqual(
- messageList.scrollTop + messageList.clientHeight,
- messageList.scrollHeight,
+ messageList.scrollTop,
+ messageList.scrollHeight - messageList.clientHeight,
"should have scrolled to bottom of thread"
);
});
@@ -1726,7 +1738,6 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi
// AKU TODO: thread specific test
assert.expect(3);
- // channel expected to be rendered, with a random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 20 });
for (let i = 0; i < 40; i++) {
this.data['mail.message'].records.push({
@@ -1742,6 +1753,18 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi
default_active_id: 'mail.channel_20',
},
},
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ 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.strictEqual(
document.querySelectorAll(`
@@ -1751,8 +1774,17 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi
"should have 30 messages"
);
- await afterNextRender(() => {
- document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0;
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_ThreadView_messageList').scrollTop = 0,
+ message: "should wait until channel 20 loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
});
assert.strictEqual(
document.querySelectorAll(`
@@ -1809,6 +1841,18 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
default_active_id: 'mail.channel_20',
},
},
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until channel 20 scrolled to its last message initially",
+ 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.containsN(
document.body,
@@ -1845,8 +1889,8 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
context: {
mockedUserId: 42,
},
- uuid: 'randomuuid',
message_content: "hu",
+ uuid: 'randomuuid',
},
}));
@@ -1899,11 +1943,9 @@ QUnit.test('restore thread scroll position', async function (assert) {
this.data['mail.channel'].records.push(
{
id: 11,
- seen_message_id: 50, // prevent race condition with mark as seen (task-2372339)
},
{
id: 12,
- seen_message_id: 50, // prevent race condition with mark as seen (task-2372339)
},
);
for (let i = 1; i <= 25; i++) {
@@ -1948,8 +1990,8 @@ QUnit.test('restore thread scroll position', async function (assert) {
.o_ThreadView_messageList
`);
assert.strictEqual(
- initialMessageList.scrollTop + initialMessageList.clientHeight,
- initialMessageList.scrollHeight,
+ initialMessageList.scrollTop,
+ initialMessageList.scrollHeight - initialMessageList.clientHeight,
"should have scrolled to bottom of channel 11 initially"
);
@@ -1986,8 +2028,13 @@ QUnit.test('restore thread scroll position', async function (assert) {
`).click();
},
message: "should wait until channel 12 scrolled to its last message",
- predicate: ({ threadViewer }) => {
- return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 12;
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 12 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
},
});
assert.strictEqual(
@@ -2013,8 +2060,12 @@ QUnit.test('restore thread scroll position', async function (assert) {
`).click();
},
message: "should wait until channel 11 restored its scroll position",
- predicate: ({ threadViewer }) => {
- return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
+ predicate: ({ scrollTop, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 11 &&
+ scrollTop === 0
+ );
},
});
assert.strictEqual(
@@ -2037,18 +2088,20 @@ QUnit.test('restore thread scroll position', async function (assert) {
}"]
`).click();
},
- message: "should wait until channel 12 recovered its scroll position",
- predicate: ({ threadViewer }) => {
- return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 12;
+ message: "should wait until channel 12 recovered its scroll position (to bottom)",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 12 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
},
});
- const messageList = document.querySelector(`
- .o_Discuss_thread
- .o_ThreadView_messageList
- `);
+ const messageList = document.querySelector('.o_ThreadView_messageList');
assert.strictEqual(
- messageList.scrollTop + messageList.clientHeight,
- messageList.scrollHeight,
+ messageList.scrollTop,
+ messageList.scrollHeight - messageList.clientHeight,
"should have recovered scroll position of channel 12 (scroll to bottom)"
);
});
@@ -3259,6 +3312,7 @@ QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async
// and a random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 10, message_unread_counter: 1 });
this.data['mail.message'].records.push({
+ id: 12,
body: "not empty",
channel_ids: [10],
model: 'mail.channel',
@@ -3288,16 +3342,27 @@ QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async
"sidebar item of channel ID 10 should be unread"
);
- await afterNextRender(() =>
- document.querySelector(`
- .o_DiscussSidebar_item[data-thread-local-id="${
- this.env.models['mail.thread'].findFromIdentifyingData({
- id: 10,
- model: 'mail.channel',
- }).localId
- }"]
- `).click()
- );
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until last seen by current partner message id changed",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 10 &&
+ thread.model === 'mail.channel' &&
+ thread.lastSeenByCurrentPartnerMessageId === 12
+ );
+ },
+ }));
assert.doesNotHaveClass(
document.querySelector(`
.o_DiscussSidebar_item[data-thread-local-id="${
@@ -3591,16 +3656,28 @@ QUnit.test('load recent messages from thread (already loaded some old messages)'
"the only message initially should be the one marked as 'needaction'"
);
- await afterNextRender(() =>
- document.querySelector(`
- .o_DiscussSidebar_item[data-thread-local-id="${
- this.env.models['mail.thread'].find(thread =>
- thread.id === 20 &&
- thread.model === 'mail.channel'
- ).localId
- }"]
- `).click()
- );
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ ).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until channel scrolled to bottom after opening it from the discuss sidebar",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20 &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ });
assert.strictEqual(
document.querySelectorAll('.o_Message').length,
31,
@@ -3617,8 +3694,17 @@ QUnit.test('load recent messages from thread (already loaded some old messages)'
"should display the message from Inbox inside the channel too"
);
- await afterNextRender(() => {
- document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0;
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_Discuss_thread .o_ThreadView_messageList').scrollTop = 0,
+ message: "should wait until channel 20 loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 20
+ );
+ },
});
assert.strictEqual(
document.querySelectorAll('.o_Message').length,
@@ -3908,7 +3994,20 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
});
}
- await this.start();
+ await this.start({
+ waitUntilEvent: {
+ eventName: 'o-component-message-list-scrolled',
+ message: "should wait until inbox scrolled to its last message initially",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
+ return (
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'inbox' &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
+ },
+ });
assert.containsN(
document.body,
'.o_Message',
@@ -3926,12 +4025,24 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
"there should no message in Inbox anymore"
);
- await afterNextRender(async () => {
- document.querySelector(`
- .o_DiscussSidebarItem[data-thread-local-id="${
- this.env.messaging.history.localId
- }"]
- `).click();
+ await this.afterEvent({
+ eventName: 'o-component-message-list-scrolled',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.messaging.history.localId
+ }"]
+ `).click();
+ },
+ message: "should wait until history scrolled to its last message after opening it from the discuss sidebar",
+ predicate: ({ scrollTop, threadViewer }) => {
+ const messageList = document.querySelector('.o_MessageList');
+ return (
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'history' &&
+ scrollTop === messageList.scrollHeight - messageList.clientHeight
+ );
+ },
});
assert.containsN(
document.body,
@@ -3941,8 +4052,17 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
);
// simulate a scroll to top to load more messages
- await afterNextRender(async () => {
- document.querySelector('.o_MessageList').scrollTop = 0;
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector('.o_MessageList').scrollTop = 0,
+ message: "should wait until mailbox history loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.box' &&
+ threadViewer.thread.id === 'history'
+ );
+ },
});
assert.containsN(
document.body,
@@ -4240,20 +4360,42 @@ QUnit.test('mark channel as seen if last message is visible when switching chann
await this.start({
discuss: {
context: {
- active_id: 'mail.channel_11',
+ active_id: 'mail.channel_11',
+ },
+ },
+ waitUntilEvent: {
+ eventName: 'o-thread-view-hint-processed',
+ message: "should wait until channel 11 loaded its messages initially",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 11 &&
+ hint.type === 'messages-loaded'
+ );
},
},
});
- await afterNextRender(() => {
- document.querySelector(`
- .o_DiscussSidebar_item[data-thread-local-id="${
- this.env.models['mail.thread'].findFromIdentifyingData({
- id: 10,
- model: 'mail.channel',
- }).localId
- }"]
- `).click();
- });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => {
+ document.querySelector(`
+ .o_DiscussSidebar_item[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 10,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `).click();
+ },
+ message: "should wait until last seen by current partner message id changed",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 10 &&
+ thread.model === 'mail.channel' &&
+ thread.lastSeenByCurrentPartnerMessageId === 10
+ );
+ },
+ }));
assert.doesNotHaveClass(
document.querySelector(`
.o_DiscussSidebar_item[data-thread-local-id="${
diff --git a/addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js b/addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js
index d5eaf72618d..0d567df080b 100644
--- a/addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js
+++ b/addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js
@@ -6,6 +6,7 @@ const components = {
DiscussSidebarItem: require('mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -21,7 +22,7 @@ class DiscussSidebar extends Component {
(...args) => this._useStoreSelector(...args),
{ compareDepth: () => this._useStoreCompareDepth() }
);
-
+ useUpdate({ func: () => this._update() });
/**
* Reference of the quick search input. Useful to filter channels and
* chats based on this input content.
@@ -35,14 +36,6 @@ class DiscussSidebar extends Component {
this._onAddChatAutocompleteSource = this._onAddChatAutocompleteSource.bind(this);
}
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -140,6 +133,9 @@ class DiscussSidebar extends Component {
* @private
*/
_update() {
+ if (!this.discuss) {
+ return;
+ }
if (this._quickSearchInputRef.el) {
this._quickSearchInputRef.el.value = this.discuss.sidebarQuickSearchValue;
}
diff --git a/addons/mail/static/src/components/emojis_popover/emojis_popover.js b/addons/mail/static/src/components/emojis_popover/emojis_popover.js
index 9259b24bee5..9bda844a030 100644
--- a/addons/mail/static/src/components/emojis_popover/emojis_popover.js
+++ b/addons/mail/static/src/components/emojis_popover/emojis_popover.js
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/emojis_popover/emojis_popover.js', funct
'use strict';
const emojis = require('mail.emojis');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
@@ -13,15 +14,7 @@ class EmojisPopover extends Component {
constructor(...args) {
super(...args);
this.emojis = emojis;
- }
-
-
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
+ useUpdate({ func: () => this._update() });
}
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js
index e09f0e4946c..4a89514b39a 100644
--- a/addons/mail/static/src/components/message/message.js
+++ b/addons/mail/static/src/components/message/message.js
@@ -11,7 +11,7 @@ const components = {
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
-const { timeFromNow } = require('mail.utils');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { _lt } = require('web.core');
const { getLangDatetimeFormat } = require('web.time');
@@ -41,10 +41,6 @@ class Message extends Component {
* clicked state, it keeps displaying the commands.
*/
isClicked: false,
- /**
- * Time elapsed from message datetime to current datetime.
- */
- timeElapsed: null,
});
useStore(props => {
const message = this.env.models['mail.message'].get(props.messageLocalId);
@@ -78,6 +74,7 @@ class Message extends Component {
notifications: 1,
},
});
+ useUpdate({ func: () => this._update() });
/**
* The intent of the reply button depends on the last rendered state.
*/
@@ -103,14 +100,6 @@ class Message extends Component {
*/
_constructor() {}
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
- }
-
willUnmount() {
clearInterval(this._intervalId);
}
@@ -393,6 +382,9 @@ class Message extends Component {
* @private
*/
_update() {
+ if (!this.message) {
+ return;
+ }
// Remove all readmore before if any before reinsert them with _insertReadMoreLess.
// This is needed because _insertReadMoreLess is working with direct DOM mutations
// which are not sync with Owl.
@@ -401,14 +393,15 @@ class Message extends Component {
el.remove();
}
this._insertReadMoreLess($(this._contentRef.el));
+ this.env.messagingBus.trigger('o-component-message-read-more-less-inserted', {
+ message: this.message,
+ });
}
this._wasSelected = this.props.isSelected;
- if (!this.state.timeElapsed) {
- this.state.timeElapsed = timeFromNow(this.message.date);
- }
+ this.message.refreshDateFromNow();
clearInterval(this._intervalId);
this._intervalId = setInterval(() => {
- this.state.timeElapsed = timeFromNow(this.message.date);
+ this.message.refreshDateFromNow();
}, 60 * 1000);
}
diff --git a/addons/mail/static/src/components/message/message.xml b/addons/mail/static/src/components/message/message.xml
index 29cb10207d7..300130f6518 100644
--- a/addons/mail/static/src/components/message/message.xml
+++ b/addons/mail/static/src/components/message/message.xml
@@ -74,7 +74,7 @@
</div>
</t>
<div class="o_Message_date o_Message_headerDate" t-att-class="{ 'o-message-selected': props.isSelected }" t-att-title="datetime">
- - <t t-esc="state.timeElapsed"/>
+ - <t t-esc="message.dateFromNow"/>
</div>
<t t-if="message.isCurrentPartnerAuthor and threadView and threadView.thread">
<MessageSeenIndicator class="o_Message_seenIndicator" messageLocalId="message.localId" threadLocalId="threadView.thread.localId"/>
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 c2e8368eda7..2d2f6d2bf9d 100644
--- a/addons/mail/static/src/components/message_list/message_list.js
+++ b/addons/mail/static/src/components/message_list/message_list.js
@@ -6,6 +6,7 @@ const components = {
};
const useRefs = require('mail/static/src/component_hooks/use_refs/use_refs.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -36,6 +37,10 @@ class MessageList extends Component {
},
});
this._getRefs = useRefs();
+ useUpdate({
+ func: () => this._update(),
+ priority: 150, // must be executed after adjust (from thread view)
+ });
/**
* Determine whether the auto-scroll on load is active or not. This
* is useful to disable some times, such as when mounting message list
@@ -55,11 +60,7 @@ class MessageList extends Component {
* Snapshot computed during willPatch, which is used by patched.
*/
this._willPatchSnapshot = undefined;
- this.onScroll = _.throttle(this.onScroll.bind(this), 100);
- }
-
- mounted() {
- this._update();
+ this._onScrollThrottled = _.throttle(this._onScrollThrottled.bind(this), 100);
}
willPatch() {
@@ -73,10 +74,6 @@ class MessageList extends Component {
};
}
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -113,6 +110,9 @@ class MessageList extends Component {
if (!this.threadView) {
return;
}
+ if (!this.el) {
+ return;
+ }
for (const hint of this.threadView.componentHintList) {
switch (hint.type) {
case 'change-of-thread-cache':
@@ -561,6 +561,22 @@ class MessageList extends Component {
* @param {ScrollEvent} ev
*/
onScroll(ev) {
+ if (!this.threadView) {
+ return;
+ }
+ // Clear pending hints to prevent them from potentially overriding the
+ // new scroll position.
+ for (const hint of this.threadView.componentHintList) {
+ this.threadView.markComponentHintProcessed(hint);
+ }
+ this._onScrollThrottled(ev);
+ }
+
+ /**
+ * @private
+ * @param {ScrollEvent} ev
+ */
+ _onScrollThrottled(ev) {
if (!this.el) {
// could be unmounted in the meantime (due to throttled behavior)
return;
diff --git a/addons/mail/static/src/components/thread_view/thread_view.js b/addons/mail/static/src/components/thread_view/thread_view.js
index ceff30d1077..132425184b3 100644
--- a/addons/mail/static/src/components/thread_view/thread_view.js
+++ b/addons/mail/static/src/components/thread_view/thread_view.js
@@ -6,6 +6,7 @@ const components = {
MessageList: require('mail/static/src/components/message_list/message_list.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -18,6 +19,10 @@ class ThreadView extends Component {
constructor(...args) {
super(...args);
useStore((...args) => this._useStoreSelector(...args));
+ useUpdate({
+ func: () => this._update(),
+ priority: 100, // must be executed after composer height adjust
+ });
/**
* Reference of the composer. Useful to set focus on composer when
* thread has the focus.
@@ -29,14 +34,6 @@ class ThreadView extends Component {
this._messageListRef = useRef('messageList');
}
- mounted() {
- this._update();
- }
-
- patched() {
- this._update();
- }
-
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
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 f8d421bfb5c..2c81214c024 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
@@ -131,7 +131,18 @@ QUnit.test('message list desc order', async function (assert) {
hasThreadView: true,
thread: [['link', thread]],
});
- await this.createThreadViewComponent(threadViewer.threadView, { order: 'desc' }, { isFixedSize: true });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => this.createThreadViewComponent(threadViewer.threadView, { order: 'desc' }, { isFixedSize: true }),
+ message: "should wait until channel 100 loaded initial messages",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 100
+ );
+ },
+ });
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
assert.notOk(
messageItems[0].classList.contains("o_MessageList_loadMore"),
@@ -148,9 +159,20 @@ QUnit.test('message list desc order', async function (assert) {
);
// scroll to bottom
- await afterNextRender(() => {
- document.querySelector(`.o_ThreadView_messageList`).scrollTop =
- document.querySelector(`.o_ThreadView_messageList`).scrollHeight;
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => {
+ const messageList = document.querySelector('.o_ThreadView_messageList');
+ messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight;
+ },
+ message: "should wait until channel 100 loaded more messages after scrolling to bottom",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 100
+ );
+ },
});
assert.strictEqual(
document.querySelectorAll(`.o_Message`).length,
@@ -203,7 +225,18 @@ QUnit.test('message list asc order', async function (assert) {
hasThreadView: true,
thread: [['link', thread]],
});
- await this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true });
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true }),
+ message: "should wait until channel 100 loaded initial messages",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 100
+ );
+ },
+ });
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
assert.notOk(
messageItems[messageItems.length - 1].classList.contains("o_MessageList_loadMore"),
@@ -220,8 +253,17 @@ QUnit.test('message list asc order', async function (assert) {
);
// scroll to top
- await afterNextRender(() => {
- document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0;
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0,
+ message: "should wait until channel 100 loaded more messages after scrolling to top",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ hint.type === 'more-messages-loaded' &&
+ threadViewer.thread.model === 'mail.channel' &&
+ threadViewer.thread.id === 100
+ );
+ },
});
assert.strictEqual(
document.querySelectorAll(`.o_Message`).length,
@@ -304,8 +346,8 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
context: {
mockedUserId: 10,
},
- uuid: thread.uuid,
message_content: "new message",
+ uuid: thread.uuid,
},
}));
assert.verifySteps(
@@ -313,7 +355,17 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
"Channel should have been fetched but not seen yet"
);
- await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus());
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => document.querySelector('.o_ComposerTextInput_textarea').focus(),
+ message: "should wait until last seen by current partner message id changed after focusing the thread",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 100 &&
+ thread.model === 'mail.channel'
+ );
+ },
+ }));
assert.verifySteps(
['rpc:channel_seen'],
"Channel should have been marked as seen after threadView got the focus"
@@ -323,6 +375,16 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
QUnit.test('mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]', async function (assert) {
assert.expect(4);
+ this.data['res.partner'].records.push({
+ id: 10,
+ });
+ this.data['res.users'].records.push({
+ id: 10,
+ partner_id: 10,
+ });
+ this.data['mail.channel'].records.push({
+ id: 100,
+ });
await this.start({
mockRPC(route, args) {
if (args.method === 'channel_fetched' && args.args[0] === 100) {
@@ -343,44 +405,37 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com
return this._super(...arguments);
}
});
- const thread = this.env.models['mail.thread'].create({
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 100,
- isServerPinned: true, // just to avoid joinChannel to be called
- members: [['insert', [
- {
- email: "john@example.com",
- id: this.env.messaging.currentPartner.id,
- name: "John",
- },
- {
- email: "fred@example.com",
- id: 10,
- name: "Fred",
- },
- ]]],
model: 'mail.channel',
- serverMessageUnreadCounter: 1, // seen would not be called if not > 0
});
-
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
document.querySelector('.o_ComposerTextInput_textarea').focus();
- const notifications = [
- [['myDB', 'mail.channel', 100], {
- channelId: 100,
- id: 1,
- body: "<p>fdsfsd</p>",
- author_id: [10, "Fred"],
- model: "mail.channel",
- channel_ids: [100],
- }]
- ];
- await afterNextRender(() =>
- this.widget.call('bus_service', 'trigger', 'notification', notifications)
- );
+ // simulate receiving a message
+ await this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ context: {
+ mockedUserId: 10,
+ },
+ message_content: "<p>fdsfsd</p>",
+ uuid: thread.uuid,
+ },
+ }),
+ message: "should wait until last seen by current partner message id changed after receiving a message while thread is focused",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 100 &&
+ thread.model === 'mail.channel'
+ );
+ },
+ });
assert.verifySteps(
['rpc:channel_seen'],
"Channel should have been mark as seen directly"
@@ -489,11 +544,11 @@ QUnit.test('[technical] new messages separator on posting message', async functi
);
});
-QUnit.test('new messages separator on receiving new message', async function (assert) {
+QUnit.test('new messages separator on receiving new message [REQUIRE FOCUS]', async function (assert) {
assert.expect(6);
this.data['res.partner'].records.push({
- id:11,
+ id: 11,
name: "Foreigner partner",
});
this.data['res.users'].records.push({
@@ -539,16 +594,27 @@ QUnit.test('new messages separator on receiving new message', async function (as
document.querySelector('.o_ComposerTextInput_textarea').blur();
// simulate receiving a message
- await afterNextRender(async () => this.env.services.rpc({
- route: '/mail/chat_post',
- params: {
- context: {
- mockedUserId: 42,
+ await this.afterEvent({
+ eventName: 'o-thread-view-hint-processed',
+ func: () => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ context: {
+ mockedUserId: 42,
+ },
+ message_content: "hu",
+ uuid: thread.uuid,
},
- uuid: thread.uuid,
- message_content: "hu",
+ }),
+ message: "should wait until new message is received",
+ predicate: ({ hint, threadViewer }) => {
+ return (
+ threadViewer.thread.id === 20 &&
+ threadViewer.thread.model === 'mail.channel' &&
+ hint.type === 'message-received'
+ );
},
- }));
+ });
assert.containsN(
document.body,
'.o_Message',
@@ -564,12 +630,22 @@ QUnit.test('new messages separator on receiving new message', async function (as
assert.containsOnce(
document.body,
`.o_MessageList_separatorNewMessages ~ .o_Message[data-message-local-id="${
- this.env.models['mail.message'].findFromIdentifyingData({id: 2}).localId
+ this.env.models['mail.message'].findFromIdentifyingData({ id: 2 }).localId
}"]`,
"'new messages' separator should be shown above new message received"
);
- await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus());
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-last-seen-by-current-partner-message-id-changed',
+ func: () => document.querySelector('.o_ComposerTextInput_textarea').focus(),
+ message: "should wait until last seen by current partner message id changed after focusing the thread",
+ predicate: ({ thread }) => {
+ return (
+ thread.id === 20 &&
+ thread.model === 'mail.channel'
+ );
+ },
+ }));
assert.containsNone(
document.body,
'.o_MessageList_separatorNewMessages',
@@ -626,94 +702,6 @@ QUnit.test('new messages separator on posting message', async function (assert)
);
});
-QUnit.test('new messages separator on receiving new message', async function (assert) {
- assert.expect(6);
-
- 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({
- channel_type: 'channel',
- id: 20,
- is_pinned: true,
- message_unread_counter: 0,
- name: "General",
- seen_message_id: 1,
- uuid: 'randomuuid',
- });
- this.data['mail.message'].records.push({
- body: "blah",
- channel_ids: [20],
- id: 1,
- });
- 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.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
-
- assert.containsOnce(
- document.body,
- '.o_MessageList_message',
- "should have an initial message"
- );
- assert.containsNone(
- document.body,
- '.o_MessageList_separatorNewMessages',
- "should not display 'new messages' separator"
- );
-
- document.querySelector('.o_ComposerTextInput_textarea').blur();
- // simulate receiving a message
- await afterNextRender(async () => this.env.services.rpc({
- route: '/mail/chat_post',
- params: {
- context: {
- mockedUserId: 42,
- },
- uuid: thread.uuid,
- message_content: "hu",
- },
- }));
- assert.containsN(
- document.body,
- '.o_Message',
- 2,
- "should now have 2 messages after receiving a new message"
- );
- assert.containsOnce(
- document.body,
- '.o_MessageList_separatorNewMessages',
- "'new messages' separator should be shown"
- );
-
- assert.containsOnce(
- document.body,
- `.o_MessageList_separatorNewMessages ~ .o_Message[data-message-local-id="${
- this.env.models['mail.message'].findFromIdentifyingData({id: 2}).localId
- }"]`,
- "'new messages' separator should be shown above new message received"
- );
-
- await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus());
- assert.containsNone(
- document.body,
- '.o_MessageList_separatorNewMessages',
- "'new messages' separator should no longer be shown as last message has been seen"
- );
-});
-
QUnit.test('basic rendering of canceled notification', async function (assert) {
assert.expect(8);
@@ -861,8 +849,8 @@ QUnit.test('should scroll to bottom on receiving new message if the list is init
context: {
mockedUserId: 42,
},
- uuid: thread.uuid,
message_content: "hello",
+ uuid: thread.uuid,
},
}),
message: "should wait until channel 20 scrolled after receiving a message",
@@ -944,8 +932,8 @@ QUnit.test('should not scroll on receiving new message if the list is initially
context: {
mockedUserId: 42,
},
- uuid: thread.uuid,
message_content: "hello",
+ uuid: thread.uuid,
},
}),
message: "should wait until channel 20 processed new message hint",
@@ -1720,8 +1708,8 @@ QUnit.test('first unseen message should be directly preceded by the new message
context: {
mockedUserId: 42,
},
- uuid: 'channel20uuid',
message_content: "test",
+ uuid: 'channel20uuid',
},
}));
assert.containsN(
diff --git a/addons/mail/static/src/models/message/message.js b/addons/mail/static/src/models/message/message.js
index 6ca7cf4a5bd..c3318aa8906 100644
--- a/addons/mail/static/src/models/message/message.js
+++ b/addons/mail/static/src/models/message/message.js
@@ -4,7 +4,8 @@ odoo.define('mail/static/src/models/message/message.js', function (require) {
const emojis = require('mail.emojis');
const { registerNewModel } = require('mail/static/src/model/model_core.js');
const { attr, many2many, many2one, one2many } = require('mail/static/src/model/model_field.js');
-const { addLink, htmlToTextContentInline, parseAndTransform } = require('mail.utils');
+const { clear } = require('mail/static/src/model/model_field_command.js');
+const { addLink, htmlToTextContentInline, parseAndTransform, timeFromNow } = require('mail.utils');
const { str_to_datetime } = require('web.time');
@@ -315,6 +316,13 @@ function factory(dependencies) {
}
/**
+ * Refreshes the value of `dateFromNow` field to the "current now".
+ */
+ refreshDateFromNow() {
+ this.update({ dateFromNow: this._computeDateFromNow() });
+ }
+
+ /**
* Action to initiate reply to current message in Discuss Inbox. Assumes
* that Discuss and Inbox are already opened.
*/
@@ -361,6 +369,16 @@ function factory(dependencies) {
}
/**
+ * @returns {string}
+ */
+ _computeDateFromNow() {
+ if (!this.date) {
+ return clear();
+ }
+ return timeFromNow(this.date);
+ }
+
+ /**
* @returns {boolean}
*/
_computeFailureNotifications() {
@@ -516,6 +534,15 @@ function factory(dependencies) {
date: attr({
default: moment(),
}),
+ /**
+ * States the time elapsed since date up to now.
+ */
+ dateFromNow: attr({
+ compute: '_computeDateFromNow',
+ dependencies: [
+ 'date',
+ ],
+ }),
email_from: attr(),
failureNotifications: one2many('mail.notification', {
compute: '_computeFailureNotifications',
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index c5d1371187b..389b5aa3580 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -1427,6 +1427,15 @@ function factory(dependencies) {
}
/**
+ * @private
+ */
+ _onChangeLastSeenByCurrentPartnerMessageId() {
+ this.env.messagingBus.trigger('o-thread-last-seen-by-current-partner-message-id-changed', {
+ thread: this,
+ });
+ }
+
+ /**
* Handles change of pinned state coming from the server. Useful to
* clear pending state once server acknowledged the change.
*
@@ -1845,6 +1854,16 @@ function factory(dependencies) {
dependencies: ['messages'],
}),
/**
+ * Not a real field, used to trigger `_onChangeLastSeenByCurrentPartnerMessageId` when one of
+ * the dependencies changes.
+ */
+ onChangeLastSeenByCurrentPartnerMessageId: attr({
+ compute: '_onChangeLastSeenByCurrentPartnerMessageId',
+ dependencies: [
+ 'lastSeenByCurrentPartnerMessageId',
+ ],
+ }),
+ /**
* Not a real field, used to trigger `_onIsServerPinnedChanged` when one of
* the dependencies changes.
*/
diff --git a/addons/mail/static/src/models/thread_cache/thread_cache.js b/addons/mail/static/src/models/thread_cache/thread_cache.js
index c684861e2be..5ca4443df5f 100644
--- a/addons/mail/static/src/models/thread_cache/thread_cache.js
+++ b/addons/mail/static/src/models/thread_cache/thread_cache.js
@@ -30,13 +30,13 @@ function factory(dependencies) {
extraDomain: [['id', '<', Math.min(...messageIds)]],
limit,
}));
- for (const threadView of this.threadViews) {
- threadView.addComponentHint('more-messages-loaded', { fetchedMessages });
- }
this.update({ isLoadingMore: false });
if (fetchedMessages.length < limit) {
this.update({ isAllHistoryLoaded: true });
}
+ for (const threadView of this.threadViews) {
+ threadView.addComponentHint('more-messages-loaded', { fetchedMessages });
+ }
return fetchedMessages;
}
@@ -165,7 +165,7 @@ function factory(dependencies) {
* @returns {mail.message[]}
*/
_computeNonEmptyMessages() {
- return [['replace', this.messages.filter(message => !message.isEmpty)]]
+ return [['replace', this.messages.filter(message => !message.isEmpty)]];
}
/**
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 0984885ddb4..19f5381be37 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
@@ -40,7 +40,8 @@ QUnit.module('form_renderer_tests.js', {
},
viewParams,
);
- const { env, widget } = await start(viewArgs, ...args);
+ const { afterEvent, env, widget } = await start(viewArgs, ...args);
+ this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
});
@@ -639,6 +640,11 @@ QUnit.test('read more/less links are not duplicated when switching from read to
</div>
</form>
`,
+ waitUntilEvent: {
+ eventName: 'o-component-message-read-more-less-inserted',
+ message: "should wait until read more/less is inserted initially",
+ predicate: ({ message }) => message.id === 1000,
+ },
});
assert.containsOnce(
document.body,
@@ -655,19 +661,24 @@ QUnit.test('read more/less links are not duplicated when switching from read to
'.o_Message_readMoreLess',
"there should be only one read more"
);
-
- await afterNextRender(() => {
- document.querySelector('.o_form_button_edit').click();
- });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_edit').click(),
+ message: "should wait until read more/less is inserted after clicking on edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
assert.containsOnce(
document.body,
'.o_Message_readMoreLess',
"there should still be only one read more after switching to edit mode"
);
- await afterNextRender(() => {
- document.querySelector('.o_form_button_cancel').click();
- });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_cancel').click(),
+ message: "should wait until read more/less is inserted after canceling edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
assert.containsOnce(
document.body,
'.o_Message_readMoreLess',
@@ -718,6 +729,11 @@ QUnit.test('read more links becomes read less after being clicked', async functi
</div>
</form>
`,
+ waitUntilEvent: {
+ eventName: 'o-component-message-read-more-less-inserted',
+ message: "should wait until read more/less is inserted initially",
+ predicate: ({ message }) => message.id === 1000,
+ },
});
assert.containsOnce(
document.body,
@@ -740,9 +756,12 @@ QUnit.test('read more links becomes read less after being clicked', async functi
"read more/less link should contain 'read more' as text"
);
- await afterNextRender(() => {
- document.querySelector('.o_form_button_edit').click();
- });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-component-message-read-more-less-inserted',
+ func: () => document.querySelector('.o_form_button_edit').click(),
+ message: "should wait until read more/less is inserted after clicking on edit",
+ predicate: ({ message }) => message.id === 1000,
+ }));
assert.strictEqual(
document.querySelector('.o_Message_readMoreLess').textContent,
'read more',