summaryrefslogtreecommitdiff
path: root/addons/mail
diff options
context:
space:
mode:
authorXavier Dubuc <xdu@odoo.com>2020-09-03 16:24:19 +0000
committerSébastien Theys <seb@odoo.com>2020-09-04 14:59:01 +0000
commit8d3413ff647de8d98c6858062dd07dde4160af95 (patch)
tree3dc134e83c874d5c7f7bf739f13a88ab986ddb1b /addons/mail
parenta5ab687be4e7890262ce9429e18dd011021513d5 (diff)
[FIX] mail, sms, snailmail: prevent message_fetch for hidden threads
Automatically load messages from a thread cache only if the thread cache is used in at least one thread view. Ensure thread viewer don't keep thread view records alive when they are not displayed. This change is particularly important for performance, to avoid spamming RPC at init if many threads are opened in chat windows. task-2310623 closes odoo/odoo#57061 X-original-commit: dd6981c7812134e0588f80a157eaba45512cd68e Related: odoo/enterprise#12936 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/mail')
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.js31
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.xml54
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js116
-rw-r--r--addons/mail/static/src/components/chatter/chatter.xml33
-rw-r--r--addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml2
-rw-r--r--addons/mail/static/src/components/discuss/discuss.js15
-rw-r--r--addons/mail/static/src/components/discuss/discuss.xml2
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js8
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js86
-rw-r--r--addons/mail/static/src/components/message/message_tests.js86
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js26
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view.xml4
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js43
-rw-r--r--addons/mail/static/src/models/chat_window/chat_window.js128
-rw-r--r--addons/mail/static/src/models/chat_window_manager/chat_window_manager.js4
-rw-r--r--addons/mail/static/src/models/chatter/chatter.js51
-rw-r--r--addons/mail/static/src/models/discuss/discuss.js119
-rw-r--r--addons/mail/static/src/models/message/message.js45
-rw-r--r--addons/mail/static/src/models/messaging_initializer/messaging_initializer.js17
-rw-r--r--addons/mail/static/src/models/messaging_menu/messaging_menu.js2
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js14
-rw-r--r--addons/mail/static/src/models/thread_cache/thread_cache.js211
-rw-r--r--addons/mail/static/src/models/thread_view/thread_view.js200
-rw-r--r--addons/mail/static/src/models/thread_view/thread_viewer.js257
-rw-r--r--addons/mail/static/src/widgets/discuss/discuss.js12
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer.js2
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js12
-rw-r--r--addons/mail/static/tests/helpers/mock_server.js5
-rw-r--r--addons/mail/views/assets.xml1
29 files changed, 1119 insertions, 467 deletions
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 43353c6086f..a5d0b2c9a20 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.js
+++ b/addons/mail/static/src/components/chat_window/chat_window.js
@@ -115,12 +115,10 @@ class ChatWindow extends Component {
isDoFocus: false,
isFocused: true,
});
- if (this.chatWindow.isFolded) {
- return;
- }
- if (!this.chatWindow.thread) {
+ if (this._inputRef.comp) {
this._inputRef.comp.focus();
- } else {
+ }
+ if (this._threadRef.comp) {
this._threadRef.comp.focus();
}
}
@@ -134,7 +132,10 @@ class ChatWindow extends Component {
* @private
*/
_saveThreadScrollTop() {
- this.chatWindow.threadView.saveThreadCacheScrollPositionsAsInitial(
+ if (!this._threadRef.comp || !this.chatWindow.threadViewer) {
+ return;
+ }
+ this.chatWindow.threadViewer.saveThreadCacheScrollPositionsAsInitial(
this._threadRef.comp.getScrollTop()
);
}
@@ -248,9 +249,7 @@ class ChatWindow extends Component {
this.chatWindow.unfold();
this.chatWindow.focus();
} else {
- if (this.chatWindow.thread) {
- this._saveThreadScrollTop();
- }
+ this._saveThreadScrollTop();
this.chatWindow.fold();
}
}
@@ -336,12 +335,7 @@ class ChatWindow extends Component {
* @private
*/
async _onWillHideHomeMenu() {
- if (!this.chatWindow.thread) {
- return;
- }
- if (!this.chatWindow.isFolded) {
- this._saveThreadScrollTop();
- }
+ this._saveThreadScrollTop();
}
/**
@@ -353,12 +347,7 @@ class ChatWindow extends Component {
* @private
*/
async _onWillShowHomeMenu() {
- if (!this.chatWindow.thread) {
- return;
- }
- if (!this.chatWindow.isFolded) {
- this._saveThreadScrollTop();
- }
+ this._saveThreadScrollTop();
}
}
diff --git a/addons/mail/static/src/components/chat_window/chat_window.xml b/addons/mail/static/src/components/chat_window/chat_window.xml
index f92a32cf2fe..240cf0f974c 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.xml
+++ b/addons/mail/static/src/components/chat_window/chat_window.xml
@@ -20,35 +20,33 @@
t-on-o-clicked="_onClickedHeader"
t-ref="header"
/>
- <t t-if="!chatWindow.isFolded">
- <t t-if="chatWindow.thread">
- <ThreadView
- class="o_ChatWindow_thread"
- composerAttachmentsDetailsMode="'card'"
- hasComposer="chatWindow.thread.model !== 'mail.box' and (!chatWindow.thread.mass_mailing or env.messaging.device.isMobile)"
- hasComposerCurrentPartnerAvatar="false"
- hasComposerSendButton="env.messaging.device.isMobile"
- hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
- threadViewLocalId="chatWindow.threadView.localId"
- t-on-focusin="_onFocusinThread"
- t-ref="thread"
+ <t t-if="chatWindow.threadView">
+ <ThreadView
+ class="o_ChatWindow_thread"
+ composerAttachmentsDetailsMode="'card'"
+ hasComposer="chatWindow.thread.model !== 'mail.box' and (!chatWindow.thread.mass_mailing or env.messaging.device.isMobile)"
+ hasComposerCurrentPartnerAvatar="false"
+ hasComposerSendButton="env.messaging.device.isMobile"
+ hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
+ threadViewLocalId="chatWindow.threadView.localId"
+ t-on-focusin="_onFocusinThread"
+ t-ref="thread"
+ />
+ </t>
+ <t t-if="chatWindow.hasNewMessageForm">
+ <div class="o_ChatWindow_newMessageForm">
+ <span class="o_ChatWindow_newMessageFormLabel">
+ To:
+ </span>
+ <AutocompleteInput
+ class="o_ChatWindow_newMessageFormInput"
+ isHtml="true"
+ placeholder="newMessageFormInputPlaceholder"
+ select="_onAutocompleteSelect"
+ source="_onAutocompleteSource"
+ t-ref="input"
/>
- </t>
- <t t-else="">
- <div class="o_ChatWindow_newMessageForm">
- <span class="o_ChatWindow_newMessageFormLabel">
- To:
- </span>
- <AutocompleteInput
- class="o_ChatWindow_newMessageFormInput"
- isHtml="true"
- placeholder="newMessageFormInputPlaceholder"
- select="_onAutocompleteSelect"
- source="_onAutocompleteSource"
- t-ref="input"
- />
- </div>
- </t>
+ </div>
</t>
</t>
</div>
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 efba64cecde..220f4278fa9 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
@@ -1753,6 +1753,122 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
);
});
+QUnit.test('chat window does not fetch messages if hidden', async function (assert) {
+ /**
+ * computation uses following info:
+ * ([mocked] global window width: 900px)
+ * (others: @see `mail/static/src/models/chat_window_manager/chat_window_manager.js:visual`)
+ *
+ * - chat window width: 325px
+ * - start/end/between gap width: 10px/10px/5px
+ * - hidden menu width: 200px
+ * - global width: 1080px
+ *
+ * Enough space for 2 visible chat windows, and one hidden chat window:
+ * 3 visible chat windows:
+ * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 > 900
+ * 2 visible chat windows + hidden menu:
+ * 10 + 325 + 5 + 325 + 10 + 200 + 5 = 875 < 900
+ */
+ assert.expect(14);
+
+ // 3 channels are expected to be found in the messaging menu, each with a
+ // random unique id that will be referenced in the test
+ this.data['mail.channel'].records = [
+ {
+ id: 10,
+ is_minimized: true,
+ name: "Channel #10",
+ state: 'open',
+ },
+ {
+ id: 11,
+ is_minimized: true,
+ name: "Channel #11",
+ state: 'open',
+ },
+ {
+ id: 12,
+ is_minimized: true,
+ name: "Channel #12",
+ state: 'open',
+ },
+ ];
+ await this.start({
+ env: {
+ browser: {
+ innerWidth: 900,
+ },
+ },
+ mockRPC(route, args) {
+ if (args.method === 'message_fetch') {
+ // domain should be like [['channel_id', 'in', [X]]] with X the channel id
+ const channel_ids = args.kwargs.domain[0][2];
+ assert.strictEqual(channel_ids.length, 1, "messages should be fetched channel per channel");
+ assert.step(`rpc:message_fetch:${channel_ids[0]}`);
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 2,
+ "2 chat windows should be visible"
+ );
+ assert.containsNone(
+ document.body,
+ `.o_ChatWindow[data-thread-local-id="${
+ this.env.models['mail.thread'].find(t =>
+ t.model === 'mail.channel' && t.id === 12
+ ).localId
+ }"]`,
+ "chat window for Channel #12 should be hidden"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindowHiddenMenu',
+ "chat window hidden menu should be displayed"
+ );
+ assert.verifySteps(
+ ['rpc:message_fetch:10', 'rpc:message_fetch:11'],
+ "messages should be fetched for the two visible chat windows"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_ChatWindowHiddenMenu_dropdownToggle').click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindowHiddenMenu_chatWindowHeader',
+ "1 hidden chat window should be listed in hidden menu"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_ChatWindowHiddenMenu_chatWindowHeader').click()
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 2,
+ "2 chat windows should still be visible"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_ChatWindow[data-thread-local-id="${
+ this.env.models['mail.thread'].find(t =>
+ t.model === 'mail.channel' && t.id === 12
+ ).localId
+ }"]`,
+ "chat window for Channel #12 should now be visible"
+ );
+ assert.verifySteps(
+ ['rpc:message_fetch:12'],
+ "messages should now be fetched for Channel #12"
+ );
+});
+
});
});
});
diff --git a/addons/mail/static/src/components/chatter/chatter.xml b/addons/mail/static/src/components/chatter/chatter.xml
index e5601dd7974..53550da516d 100644
--- a/addons/mail/static/src/components/chatter/chatter.xml
+++ b/addons/mail/static/src/components/chatter/chatter.xml
@@ -8,7 +8,7 @@
class="o_Chatter_topbar"
chatterLocalId="chatter.localId"
/>
- <t t-if="chatter.hasThread and chatter.thread and chatter.isComposerVisible">
+ <t t-if="chatter.threadView and chatter.isComposerVisible">
<Composer
class="o_Chatter_composer"
t-att-class="{ 'o-bordered': chatter.hasExternalBorder }"
@@ -27,22 +27,21 @@
threadLocalId="chatter.thread.localId"
/>
</t>
- <t t-if="chatter.thread">
- <t t-if="chatter.hasActivities and chatter.activities.length > 0">
- <ActivityBox
- class="o_Chatter_activityBox"
- chatterLocalId="chatter.localId"
- />
- </t>
- <t t-if="chatter.hasThread">
- <ThreadView
- class="o_Chatter_thread"
- hasComposer="false"
- hasScrollAdjust="chatter.hasMessageListScrollAdjust"
- order="'desc'"
- threadViewLocalId="chatter.threadView.localId"
- />
- </t>
+ <t t-if="chatter.thread and chatter.hasActivities and chatter.activities.length > 0">
+ <ActivityBox
+ class="o_Chatter_activityBox"
+ chatterLocalId="chatter.localId"
+ />
+ </t>
+ <t t-if="chatter.threadView">
+ <ThreadView
+ class="o_Chatter_thread"
+ hasComposer="false"
+ hasScrollAdjust="chatter.hasMessageListScrollAdjust"
+ order="'desc'"
+ threadViewLocalId="chatter.threadView.localId"
+ t-ref="thread"
+ />
</t>
</t>
</div>
diff --git a/addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml b/addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml
index 3512f93332e..6911dfb959e 100644
--- a/addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml
+++ b/addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml
@@ -5,7 +5,7 @@
<div class="o_ChatterTopbar">
<t t-if="chatter">
<div class="o_ChatterTopbar_actions" t-att-class="{'o-has-active-button': chatter.isComposerVisible }">
- <t t-if="chatter.hasThread">
+ <t t-if="chatter.threadView">
<button class="btn btn-link o_ChatterTopbar_button o_ChatterTopbar_buttonSendMessage"
type="button"
t-att-class="{
diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js
index 4c8c4a58ab7..377fde0dd72 100644
--- a/addons/mail/static/src/components/discuss/discuss.js
+++ b/addons/mail/static/src/components/discuss/discuss.js
@@ -26,18 +26,17 @@ class Discuss extends Component {
super(...args);
useStore(props => {
const discuss = this.env.messaging && this.env.messaging.discuss;
+ const threadView = discuss && discuss.threadView;
return {
- checkedMessages: discuss ? discuss.threadView.checkedMessages.map(message => message.__state) : [],
+ checkedMessages: threadView ? threadView.checkedMessages.map(message => message.__state) : [],
discuss: discuss ? discuss.__state : undefined,
isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
isMessagingInitialized: this.env.isMessagingInitialized(),
thread: discuss && discuss.thread ? discuss.thread.__state : undefined,
- threadCache: (discuss && discuss.threadView && discuss.threadView.threadCache)
- ? discuss.threadView.threadCache.__state
+ threadCache: (threadView && threadView.threadCache)
+ ? threadView.threadCache.__state
: undefined,
- uncheckedMessages: discuss && discuss.threadView
- ? discuss.threadView.uncheckedMessages.map(message => message.__state)
- : [],
+ uncheckedMessages: threadView ? threadView.uncheckedMessages.map(message => message.__state) : [],
};
}, {
compareDepth: {
@@ -78,12 +77,13 @@ class Discuss extends Component {
if (
this.discuss.thread &&
this.discuss.thread === this.env.messaging.inbox &&
+ this.discuss.threadView &&
this._lastThreadCache === this.discuss.threadView.threadCache.localId &&
this._lastThreadCounter > 0 && this.discuss.thread.counter === 0
) {
this.trigger('o-show-rainbow-man');
}
- this._activeThreadCache = this.discuss.threadView.threadCache;
+ this._activeThreadCache = this.discuss.threadView && this.discuss.threadView.threadCache;
this._updateLocalStoreProps();
this._update();
}
@@ -170,6 +170,7 @@ class Discuss extends Component {
* rainbox man on inbox.
*/
this._lastThreadCache = (
+ this.discuss.threadView &&
this.discuss.threadView.threadCache &&
this.discuss.threadView.threadCache.localId
);
diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml
index 79838422f81..39f09da0b5b 100644
--- a/addons/mail/static/src/components/discuss/discuss.xml
+++ b/addons/mail/static/src/components/discuss/discuss.xml
@@ -48,7 +48,7 @@
/>
</div>
</t>
- <t t-if="(env.messaging.device.isMobile and discuss.activeMobileNavbarTabId === 'mailbox' and discuss.thread and discuss.thread.model === 'mail.box') or (!env.messaging.device.isMobile and discuss.thread)">
+ <t t-if="discuss.threadView">
<ThreadView
class="o_Discuss_thread"
t-att-class="{ 'o-mobile': env.messaging.device.isMobile }"
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
index bb780bad22d..ae6af0995b6 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
@@ -126,8 +126,8 @@ QUnit.test('sidebar: pinned channel 3: open pinned channel and unpin it', async
);
assert.containsOnce(
document.body,
- `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
- "The Inbox is opened in discuss"
+ '.o_Discuss_noThread',
+ "should have no thread opened in discuss"
);
});
@@ -182,8 +182,8 @@ QUnit.test('sidebar: unpin channel from bus', async function (assert) {
});
assert.containsOnce(
document.body,
- `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
- "The Inbox is opened in discuss"
+ '.o_Discuss_noThread',
+ "should have no thread opened in discuss"
);
assert.containsNone(
document.body,
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 c12e47f02cf..87ad3640919 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -3,6 +3,7 @@ odoo.define('mail/static/src/components/discuss/tests/discuss_tests.js', functio
const BusService = require('bus.BusService');
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
const {
afterEach,
afterNextRender,
@@ -1115,7 +1116,7 @@ QUnit.test('default thread rendering', async function (assert) {
});
QUnit.test('initially load messages from inbox', async function (assert) {
- assert.expect(5);
+ assert.expect(4);
await this.start({
async mockRPC(route, args) {
@@ -1126,13 +1127,8 @@ QUnit.test('initially load messages from inbox', async function (assert) {
30,
"should fetch up to 30 messages"
);
- assert.strictEqual(
- args.args.length,
- 1,
- "should have a single item in args"
- );
assert.deepEqual(
- args.args[0],
+ args.kwargs.domain,
[["needaction", "=", true]],
"should fetch needaction messages"
);
@@ -1186,7 +1182,7 @@ QUnit.test('auto-select thread in discuss context', async function (assert) {
});
QUnit.test('load single message from channel initially', async function (assert) {
- assert.expect(8);
+ assert.expect(7);
// 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 });
@@ -1210,13 +1206,8 @@ QUnit.test('load single message from channel initially', async function (assert)
30,
"should fetch up to 30 messages"
);
- assert.strictEqual(
- args.args.length,
- 1,
- "should have a single item in args"
- );
assert.deepEqual(
- args.args[0],
+ args.kwargs.domain,
[["channel_ids", "in", [20]]],
"should fetch messages from channel"
);
@@ -1829,7 +1820,7 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
});
QUnit.test('restore thread scroll position', async function (assert) {
- assert.expect(4);
+ assert.expect(5);
// channels expected to be rendered, with random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 11 }, { id: 12 });
for (let i = 1; i <= 25; i++) {
@@ -1839,26 +1830,43 @@ QUnit.test('restore thread scroll position', async function (assert) {
res_id: 11,
});
}
- for (let i = 1; i <= 25; i++) {
+ for (let i = 1; i <= 24; i++) {
this.data['mail.message'].records.push({
channel_ids: [12],
model: 'mail.channel',
res_id: 12,
});
}
- await this.start({
- discuss: {
- params: {
- default_active_id: 'mail.channel_11',
+ const messageFetchChannel11Def = makeDeferred();
+ const messageFetchChannel12Def = makeDeferred();
+ await afterNextRender(async () => {
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_11',
+ },
},
- },
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ // domain should be like [['channel_id', 'in', [X]]] with X the channel id
+ if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(11)) {
+ messageFetchChannel11Def.resolve();
+ }
+ if (route.includes('message_fetch') && args.kwargs.domain[0][2].includes(12)) {
+ messageFetchChannel12Def.resolve();
+ }
+ return res;
+ },
+ });
+ // wait until messages are fetched, ignore other renders that are too early
+ await messageFetchChannel11Def;
});
assert.strictEqual(
document.querySelectorAll(`
.o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
`).length,
25,
- "should have 25 messages"
+ "should have 25 messages in channel 11"
);
// scroll to top of channel11
@@ -1871,8 +1879,13 @@ QUnit.test('restore thread scroll position', async function (assert) {
"should have scrolled to top of thread"
);
- // select channel12
- await afterNextRender(() =>
+ const scrollChannel12Def = makeDeferred();
+ const onScroll = () => {
+ scrollChannel12Def.resolve();
+ };
+ document.addEventListener('o-message-list-scrolled', onScroll);
+ // select channel 12
+ await afterNextRender(async () => {
document.querySelector(`
.o_DiscussSidebar_groupChannel
.o_DiscussSidebar_item[data-thread-local-id="${
@@ -1881,9 +1894,26 @@ QUnit.test('restore thread scroll position', async function (assert) {
thread.model === 'mail.channel'
).localId
}"]
- `).click()
+ `).click();
+ // wait until messages are fetched, ignore other renders that are too early
+ await messageFetchChannel12Def;
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Discuss_thread .o_ThreadView_messageList .o_MessageList_message
+ `).length,
+ 24,
+ "should have 24 messages in channel 12"
);
- // select channel11
+
+ // Ensure scrollIntoView of channel 12 has enough time to complete before
+ // going back to channel 11. Await is needed to prevent the scrollIntoView
+ // initially planned for channel 12 to actually apply on channel 11.
+ // task-2333535
+ await scrollChannel12Def;
+ document.removeEventListener('o-message-list-scrolled', onScroll);
+
+ // select channel 11
await afterNextRender(() =>
document.querySelector(`
.o_DiscussSidebar_groupChannel
@@ -1898,7 +1928,7 @@ QUnit.test('restore thread scroll position', async function (assert) {
assert.strictEqual(
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop,
0,
- "should have recovered scroll position of channel11 (scroll to top)"
+ "should have recovered scroll position of channel 11 (scroll to top)"
);
// select channel12
@@ -1920,7 +1950,7 @@ QUnit.test('restore thread scroll position', async function (assert) {
assert.strictEqual(
messageList.scrollTop + messageList.clientHeight,
messageList.scrollHeight,
- "should have recovered scroll position of channel12 (scroll to bottom)"
+ "should have recovered scroll position of channel 12 (scroll to bottom)"
);
});
diff --git a/addons/mail/static/src/components/message/message_tests.js b/addons/mail/static/src/components/message/message_tests.js
index df964458fea..e69c40e35aa 100644
--- a/addons/mail/static/src/components/message/message_tests.js
+++ b/addons/mail/static/src/components/message/message_tests.js
@@ -190,7 +190,8 @@ QUnit.test('Notification Sent', async function (assert) {
assert.expect(9);
await this.start();
- const threadView = this.env.models['mail.thread_view'].create({
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
thread: [['create', {
id: 11,
model: 'mail.channel',
@@ -205,10 +206,10 @@ QUnit.test('Notification Sent', async function (assert) {
notification_type: 'email',
partner: [['insert', { id: 12, name: "Someone" }]],
}]],
- originThread: [['link', threadView.thread]],
+ originThread: [['link', threadViewer.thread]],
});
await this.createMessageComponent(message, {
- threadViewLocalId: threadView.localId
+ threadViewLocalId: threadViewer.threadView.localId
});
assert.containsOnce(
@@ -281,7 +282,8 @@ QUnit.test('Notification Error', async function (assert) {
});
await this.start({ env: { bus } });
- const threadView = this.env.models['mail.thread_view'].create({
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
thread: [['create', {
id: 11,
model: 'mail.channel',
@@ -295,10 +297,10 @@ QUnit.test('Notification Error', async function (assert) {
notification_status: 'exception',
notification_type: 'email',
}]],
- originThread: [['link', threadView.thread]],
+ originThread: [['link', threadViewer.thread]],
});
await this.createMessageComponent(message, {
- threadViewLocalId: threadView.localId
+ threadViewLocalId: threadViewer.threadView.localId
});
assert.containsOnce(
@@ -347,7 +349,10 @@ QUnit.test("'channel_fetch' notification received is correctly handled", async f
],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const message = this.env.models['mail.message'].create({
author: [['link', currentPartner]],
body: "<p>Test</p>",
@@ -355,7 +360,9 @@ QUnit.test("'channel_fetch' notification received is correctly handled", async f
originThread: [['link', thread]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId,
+ });
assert.containsOnce(
document.body,
@@ -403,14 +410,19 @@ QUnit.test("'channel_seen' notification received is correctly handled", async fu
],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const message = this.env.models['mail.message'].create({
author: [['link', currentPartner]],
body: "<p>Test</p>",
id: 100,
originThread: [['link', thread]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId,
+ });
assert.containsOnce(
document.body,
@@ -458,14 +470,19 @@ QUnit.test("'channel_fetch' notification then 'channel_seen' received are corre
],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const message = this.env.models['mail.message'].create({
author: [['link', currentPartner]],
body: "<p>Test</p>",
id: 100,
originThread: [['link', thread]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId,
+ });
assert.containsOnce(
document.body,
@@ -527,25 +544,28 @@ QUnit.test('do not show messaging seen indicator if not authored by me', async f
partnerSeenInfos: [['create', [
{
id: this.env.session.partner_id,
- lastFetchedMessage: [['insert', {id: 100}]],
- partner: [['insert', {id: this.env.messaging.currentPartner.id}]],
+ lastFetchedMessage: [['insert', { id: 100 }]],
+ partner: [['insert', { id: this.env.messaging.currentPartner.id }]],
},
{
id: 100,
- lastFetchedMessage: [['insert', {id: 100}]],
+ lastFetchedMessage: [['insert', { id: 100 }]],
partner: [['link', author]],
},
]]],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const message = this.env.models['mail.message'].insert({
author: [['link', author]],
body: "<p>Test</p>",
id: 100,
originThread: [['link', thread]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, { threadViewLocalId: threadViewer.threadView.localId });
assert.containsOnce(
document.body,
@@ -571,11 +591,14 @@ QUnit.test('do not show messaging seen indicator if before last seen by all mess
id: 11,
messageSeenIndicators: [['insert', {
id: this.env.models['mail.message_seen_indicator'].computeId(99, 11),
- message: [['insert', {id: 99}]],
+ message: [['insert', { id: 99 }]],
}]],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const lastSeenMessage = this.env.models['mail.message'].create({
author: [['link', currentPartner]],
body: "<p>You already saw me</p>",
@@ -598,11 +621,13 @@ QUnit.test('do not show messaging seen indicator if before last seen by all mess
{
id: 100,
lastSeenMessage: [['link', lastSeenMessage]],
- partner: [['insert', {id: 100}]],
+ partner: [['insert', { id: 100 }]],
},
]]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId,
+ });
assert.containsOnce(
document.body,
@@ -634,30 +659,35 @@ QUnit.test('only show messaging seen indicator if authored by me, after last see
partnerSeenInfos: [['create', [
{
id: currentPartner.id,
- lastSeenMessage: [['insert', {id: 100}]],
+ lastSeenMessage: [['insert', { id: 100 }]],
partner: [['link', currentPartner]],
},
{
id: 100,
- partner: [['insert', {id: 100}]],
- lastFetchedMessage: [['insert', {id: 100}]],
- lastSeenMessage: [['insert', {id: 99}]],
+ partner: [['insert', { id: 100 }]],
+ lastFetchedMessage: [['insert', { id: 100 }]],
+ lastSeenMessage: [['insert', { id: 99 }]],
},
]]],
messageSeenIndicators: [['insert', {
id: this.env.models['mail.message_seen_indicator'].computeId(100, 11),
- message: [['insert', {id: 100}]],
+ message: [['insert', { id: 100 }]],
}]],
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
const message = this.env.models['mail.message'].insert({
author: [['link', currentPartner]],
body: "<p>Test</p>",
id: 100,
originThread: [['link', thread]],
});
- await this.createMessageComponent(message, { threadViewLocalId: threadView.localId });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId,
+ });
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 966bff9a5ba..17531eabbca 100644
--- a/addons/mail/static/src/components/message_list/message_list.js
+++ b/addons/mail/static/src/components/message_list/message_list.js
@@ -52,17 +52,6 @@ class MessageList extends Component {
*/
this._loadMoreRef = useRef('loadMore');
/**
- * Tracked last thread cache rendered. Useful to determine scroll
- * position on patch if it is on the same thread cache or not.
- */
- this._renderedThreadCache = null;
- /**
- * Tracked last selected message. Useful to determine when patch comes
- * from a message selection on a given thread cache, so that it
- * auto-scroll to that message.
- */
- this._selectedMessage = null;
- /**
* Snapshot computed during willPatch, which is used by patched.
*/
this._willPatchSnapshot = undefined;
@@ -121,14 +110,14 @@ class MessageList extends Component {
* fixed height, which is the case for the moment.
*/
async adjustFromComponentHints() {
+ if (!this.threadView) {
+ return;
+ }
for (const hint of this.threadView.componentHintList) {
switch (hint.type) {
case 'change-of-thread-cache':
this._adjustFromChangeOfThreadCache(hint);
break;
- case 'chat-window-unfolded':
- this._adjustFromChatWindowUnfolded(hint);
- break;
case 'current-partner-just-posted-message':
this._adjustFromCurrentPartnerJustPostedMessage(hint);
break;
@@ -427,6 +416,9 @@ class MessageList extends Component {
* @private
*/
_checkMostRecentMessageIsVisible() {
+ if (!this.threadView) {
+ return;
+ }
const thread = this.threadView.thread;
const threadCache = this.threadView.threadCache;
const lastMessageIsVisible =
@@ -531,7 +523,11 @@ class MessageList extends Component {
// could be unmounted in the meantime (due to throttled behavior)
return;
}
- this.threadView.saveThreadCacheScrollPositionsAsInitial(this.el.scrollTop);
+ if (!this.threadView || !this.threadView.threadViewer) {
+ return;
+ }
+ this.trigger('o-message-list-scrolled');
+ this.threadView.threadViewer.saveThreadCacheScrollPositionsAsInitial(this.el.scrollTop);
if (!this._isAutoLoadOnScrollActive) {
return;
}
diff --git a/addons/mail/static/src/components/thread_view/thread_view.xml b/addons/mail/static/src/components/thread_view/thread_view.xml
index e7dd82ccf43..25e93400368 100644
--- a/addons/mail/static/src/components/thread_view/thread_view.xml
+++ b/addons/mail/static/src/components/thread_view/thread_view.xml
@@ -4,12 +4,12 @@
<t t-name="mail.ThreadView" owl="1">
<div class="o_ThreadView" t-att-data-correspondent-id="threadView and threadView.thread and threadView.thread.correspondent and threadView.thread.correspondent.id" t-att-data-thread-local-id="threadView and threadView.thread and threadView.thread.localId">
<t t-if="threadView">
- <t t-if="threadView.isShowingLoading" name="loadingCondition">
+ <t t-if="threadView.isLoading" name="loadingCondition">
<div class="o_ThreadView_loading">
<span><i class="o_ThreadView_loadingIcon fa fa-spinner fa-spin" title="Loading..." role="img"/>Loading...</span>
</div>
</t>
- <t t-elif="threadView.threadCache.isLoaded">
+ <t t-elif="threadView.threadCache.isLoaded or threadView.thread.isTemporary">
<MessageList
class="o_ThreadView_messageList"
hasMessageCheckbox="props.hasMessageCheckbox"
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 c8f41632ae3..2d0b3207ae1 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
@@ -81,8 +81,11 @@ QUnit.test('dragover files on thread with composer', async function (assert) {
name: "General",
public: 'public',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView, { hasComposer: true });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
await afterNextRender(() =>
dragenterFiles(document.querySelector('.o_ThreadView'))
);
@@ -122,8 +125,11 @@ QUnit.test('message list desc order', async function (assert) {
name: "General",
public: 'public',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView, { order: 'desc' }, { isFixedSize: true });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { order: 'desc' }, { isFixedSize: true });
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
assert.notOk(
messageItems[0].classList.contains("o_MessageList_loadMore"),
@@ -190,8 +196,11 @@ QUnit.test('message list asc order', async function (assert) {
name: "General",
public: 'public',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView, { order: 'asc' }, { isFixedSize: true });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { order: 'asc' }, { isFixedSize: true });
const messageItems = document.querySelectorAll(`.o_MessageList_item`);
assert.notOk(
messageItems[messageItems.length - 1].classList.contains("o_MessageList_loadMore"),
@@ -283,8 +292,11 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView, { hasComposer: true });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
const notifications = [
[['myDB', 'mail.channel', 100], {
channelId: 100,
@@ -353,9 +365,11 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com
model: 'mail.channel',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView, { hasComposer: true });
-
+ 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], {
@@ -394,8 +408,11 @@ QUnit.test('show message subject if thread is mailing channel', async function (
name: "General",
public: 'public',
});
- const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
- await this.createThreadViewComponent(threadView);
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView);
assert.containsOnce(
document.body,
diff --git a/addons/mail/static/src/models/chat_window/chat_window.js b/addons/mail/static/src/models/chat_window/chat_window.js
index a4db3e638de..5efcaf3b6fe 100644
--- a/addons/mail/static/src/models/chat_window/chat_window.js
+++ b/addons/mail/static/src/models/chat_window/chat_window.js
@@ -77,7 +77,7 @@ function factory(dependencies) {
if (this.thread) {
this.thread.update({ pendingFoldState: 'folded' });
} else {
- this.update({ _isFolded: true });
+ this.update({ isFolded: true });
}
}
@@ -120,9 +120,8 @@ function factory(dependencies) {
unfold() {
if (this.thread) {
this.thread.update({ pendingFoldState: 'open' });
- this.threadView.addComponentHint('chat-window-unfolded');
} else {
- this.update({ _isFolded: false });
+ this.update({ isFolded: false });
}
}
@@ -134,6 +133,14 @@ function factory(dependencies) {
* @private
* @returns {boolean}
*/
+ _computeHasNewMessageForm() {
+ return this.isVisible && !this.isFolded && !this.thread;
+ }
+
+ /**
+ * @private
+ * @returns {boolean}
+ */
_computeHasShiftLeft() {
if (!this.manager) {
return false;
@@ -165,12 +172,28 @@ function factory(dependencies) {
* @private
* @returns {boolean}
*/
+ _computeHasThreadView() {
+ return this.isVisible && !this.isFolded && this.thread;
+ }
+
+ /**
+ * @private
+ * @returns {boolean}
+ */
_computeIsFolded() {
const thread = this.thread;
if (thread) {
return thread.foldState === 'folded';
}
- return this._isFolded;
+ return this.isFolded;
+ }
+
+ /**
+ * @private
+ * @returns {boolean}
+ */
+ _computeIsVisible() {
+ return this.manager.allOrderedVisible.includes(this);
}
/**
@@ -197,6 +220,22 @@ function factory(dependencies) {
/**
* @private
+ * @returns {integer|undefined}
+ */
+ _computeVisibleIndex() {
+ if (!this.manager) {
+ return undefined;
+ }
+ const visible = this.manager.visual.visible;
+ const index = visible.findIndex(visible => visible.chatWindowLocalId === this.localId);
+ if (index === -1) {
+ return undefined;
+ }
+ return index;
+ }
+
+ /**
+ * @private
* @returns {integer}
*/
_computeVisibleOffset() {
@@ -278,33 +317,19 @@ function factory(dependencies) {
this.threadView.addComponentHint('home-menu-shown');
}
- /**
- * @private
- * @returns {integer|undefined}
- */
- _computeVisibleIndex() {
- if (!this.manager) {
- return undefined;
- }
- const visible = this.manager.visual.visible;
- const index = visible.findIndex(visible => visible.chatWindowLocalId === this.localId);
- if (index === -1) {
- return undefined;
- }
- return index;
- }
}
ChatWindow.fields = {
/**
- * Determine whether the chat window is folded or not, when not
- * linked to a thread.
- * Note: this value only make sense for chat window not linked
- * to a thread. State of chat window of a thread is entirely
- * based on thread.foldState. @see isFolded .
+ * Determines whether "new message form" should be displayed.
*/
- _isFolded: attr({
- default: false,
+ hasNewMessageForm: attr({
+ compute: '_computeHasNewMessageForm',
+ dependencies: [
+ 'isFolded',
+ 'isVisible',
+ 'thread',
+ ],
}),
hasShiftLeft: attr({
compute: '_computeHasShiftLeft',
@@ -317,6 +342,17 @@ function factory(dependencies) {
default: false,
}),
/**
+ * Determines whether `this.thread` should be displayed.
+ */
+ hasThreadView: attr({
+ compute: '_computeHasThreadView',
+ dependencies: [
+ 'isFolded',
+ 'isVisible',
+ 'thread',
+ ],
+ }),
+ /**
* Determine whether the chat window should be programmatically
* focused by observed component of chat window. Those components
* are responsible to unmark this record afterwards, otherwise
@@ -326,18 +362,33 @@ function factory(dependencies) {
default: false,
}),
/**
- * Determine whether the chat window is focused or not. Useful for
- * visual clue.
+ * States whether `this` is focused. Useful for visual clue.
*/
isFocused: attr({
default: false,
}),
+ /**
+ * Determines whether `this` is folded.
+ *
+ * Note: writing this value directly only makes sense when `this.thread`
+ * is empty. State of chat window of a thread is entirely based on
+ * `thread.foldState`.
+ */
isFolded: attr({
compute: '_computeIsFolded',
dependencies: [
'thread',
'threadFoldState',
- '_isFolded',
+ ],
+ default: false,
+ }),
+ /**
+ * States whether `this` is visible or not.
+ */
+ isVisible: attr({
+ compute: '_computeIsVisible',
+ dependencies: [
+ 'managerAllOrderedVisible',
],
default: false,
}),
@@ -368,17 +419,30 @@ function factory(dependencies) {
'threadDisplayName',
],
}),
- thread: many2one('mail.thread', {
- related: 'threadView.thread',
- }),
+ /**
+ * Determines the `mail.thread` that should be displayed by `this`.
+ * If no `mail.thread` is linked, `this` is considered "new message".
+ */
+ thread: many2one('mail.thread'),
threadDisplayName: attr({
related: 'thread.displayName',
}),
threadFoldState: attr({
related: 'thread.foldState',
}),
+ /**
+ * States the `mail.thread_view` displaying `this.thread`.
+ */
threadView: one2one('mail.thread_view', {
+ related: 'threadViewer.threadView',
+ }),
+ /**
+ * Determines the `mail.thread_viewer` managing the display of `this.thread`.
+ */
+ threadViewer: one2one('mail.thread_viewer', {
+ default: [['create']],
inverse: 'chatWindow',
+ isCausal: true,
}),
/**
* This field handle the "order" (index) of the visible chatWindow inside the UI.
diff --git a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
index 8be11eba453..ed4db95f585 100644
--- a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
+++ b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
@@ -110,9 +110,7 @@ function factory(dependencies) {
if (!chatWindow) {
chatWindow = this.env.models['mail.chat_window'].create({
manager: [['link', this]],
- threadView: [['create', {
- thread: [['link', thread]],
- }]],
+ thread: [['link', thread]],
});
}
if (thread.foldState === 'closed') {
diff --git a/addons/mail/static/src/models/chatter/chatter.js b/addons/mail/static/src/models/chatter/chatter.js
index 836aac94060..a94731910bc 100644
--- a/addons/mail/static/src/models/chatter/chatter.js
+++ b/addons/mail/static/src/models/chatter/chatter.js
@@ -110,6 +110,14 @@ function factory(dependencies) {
* @private
* @returns {boolean}
*/
+ _computeHasThreadView() {
+ return this.thread && this.hasMessageList;
+ }
+
+ /**
+ * @private
+ * @returns {boolean}
+ */
_computeIsDisabled() {
return !this.threadId;
}
@@ -136,7 +144,7 @@ function factory(dependencies) {
_prepareAttachmentsLoading() {
this._isPreparingAttachmentsLoading = true;
this._attachmentsLoaderTimeout = this.env.browser.setTimeout(() => {
- this.update({isShowingAttachmentsLoading: true});
+ this.update({ isShowingAttachmentsLoading: true });
this._isPreparingAttachmentsLoading = false;
}, this.env.loadingBaseDelayDuration);
}
@@ -147,7 +155,7 @@ function factory(dependencies) {
_stopAttachmentsLoading() {
this.env.browser.clearTimeout(this._attachmentsLoaderTimeout);
this._attachmentsLoaderTimeout = null;
- this.update({isShowingAttachmentsLoading: false});
+ this.update({ isShowingAttachmentsLoading: false });
this._isPreparingAttachmentsLoading = false;
}
@@ -232,7 +240,7 @@ function factory(dependencies) {
id: getMessageNextTemporaryId(),
isTemporary: true,
});
- this.threadView.update({ thread: [['link', thread]] });
+ this.update({ thread: [['link', thread]] });
for (const cache of thread.caches) {
cache.update({ messages: [['link', message]] });
}
@@ -242,7 +250,7 @@ function factory(dependencies) {
id: this.threadId,
model: this.threadModel,
});
- this.threadView.update({ thread: [['link', thread]] });
+ this.update({ thread: [['link', thread]] });
}
}
@@ -281,6 +289,12 @@ function factory(dependencies) {
default: true,
}),
/**
+ * Determines whether `this` should display a message list.
+ */
+ hasMessageList: attr({
+ default: true,
+ }),
+ /**
* Whether the message list should manage its scroll.
* In particular, when the chatter is on the form view's side,
* then the scroll is managed by the message list.
@@ -290,8 +304,15 @@ function factory(dependencies) {
hasMessageListScrollAdjust: attr({
default: false,
}),
- hasThread: attr({
- default: true,
+ /**
+ * Determines whether `this.thread` should be displayed.
+ */
+ hasThreadView: attr({
+ compute: '_computeHasThreadView',
+ dependencies: [
+ 'hasMessageList',
+ 'thread',
+ ],
}),
hasTopbarCloseButton: attr({
default: false,
@@ -326,16 +347,28 @@ function factory(dependencies) {
compute: '_computeOverdueActivities',
dependencies: ['activitiesState'],
}),
- thread: many2one('mail.thread', {
- related: 'threadView.thread',
- }),
+ /**
+ * Determines the `mail.thread` that should be displayed by `this`.
+ */
+ thread: many2one('mail.thread'),
threadAttachmentCount: attr({
default: 0,
}),
threadId: attr(),
threadModel: attr(),
+ /**
+ * States the `mail.thread_view` displaying `this.thread`.
+ */
threadView: one2one('mail.thread_view', {
+ related: 'threadViewer.threadView',
+ }),
+ /**
+ * Determines the `mail.thread_viewer` managing the display of `this.thread`.
+ */
+ threadViewer: one2one('mail.thread_viewer', {
default: [['create']],
+ inverse: 'chatter',
+ isCausal: true,
}),
todayActivities: one2many('mail.activity', {
compute: '_computeTodayActivities',
diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js
index 2dae66f811f..221d276ecd1 100644
--- a/addons/mail/static/src/models/discuss/discuss.js
+++ b/addons/mail/static/src/models/discuss/discuss.js
@@ -151,10 +151,10 @@ function factory(dependencies) {
*/
openInitThread() {
const [model, id] = this.initActiveId.split('_');
- const thread = this.env.models['mail.thread'].find(thread =>
- thread.id === (model !== 'mail.box' ? Number(id) : id) &&
- thread.model === model
- );
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: model !== 'mail.box' ? Number(id) : id,
+ model,
+ });
if (!thread) {
return;
}
@@ -168,7 +168,7 @@ function factory(dependencies) {
* @param {mail.thread} thread
*/
async openThread(thread) {
- this.threadView.update({
+ this.update({
stringifiedDomain: '[]',
thread: [['link', thread]],
});
@@ -253,16 +253,22 @@ function factory(dependencies) {
/**
* @private
- * @returns {string}
+ * @returns {boolean}
*/
- _computeInitActiveId() {
- if (!this.isOpen) {
- return this.defaultInitActiveId;
+ _computeHasThreadView() {
+ if (!this.thread || !this.isOpen) {
+ return false;
}
- if (this.thread) {
- return this.threadToActiveId(this.thread);
+ if (
+ this.env.messaging.device.isMobile &&
+ (
+ this.activeMobileNavbarTabId !== 'mailbox' ||
+ this.thread.model !== 'mail.box'
+ )
+ ) {
+ return false;
}
- return this.initActiveId;
+ return true;
}
/**
@@ -308,23 +314,21 @@ function factory(dependencies) {
return [];
}
+
/**
+ * Only pinned threads are allowed in discuss.
+ *
* @private
+ * @returns {mail.thread|undefined}
*/
- _onChangeThreadIsPinned() {
+ _computeThread() {
let thread = this.thread;
- // No thread, or thread is being removed
- // so we display discuss the messaging's Inbox.
- if (
- (!thread || !thread.isPinned) &&
- this.messaging
- ) {
- thread = this.messaging.inbox;
- }
- if (thread && this.threadView && thread !== this.thread) {
- this.threadView.update({ thread: [['link', thread]] });
+ if (!thread || !thread.isPinned) {
+ return [['unlink']];
}
+ return [];
}
+
}
Discuss.fields = {
@@ -350,8 +354,17 @@ function factory(dependencies) {
default: "",
dependencies: ['isOpen'],
}),
- defaultInitActiveId: attr({
- default: 'mail.box_inbox',
+ /**
+ * Serves as compute dependency.
+ */
+ device: one2one('mail.device', {
+ related: 'messaging.device',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ deviceIsMobile: attr({
+ related: 'device.isMobile',
}),
/**
* Determine if the moderation discard dialog is displayed.
@@ -366,20 +379,26 @@ function factory(dependencies) {
default: false,
}),
/**
+ * Determines whether `this.thread` should be displayed.
+ */
+ hasThreadView: attr({
+ compute: '_computeHasThreadView',
+ dependencies: [
+ 'activeMobileNavbarTabId',
+ 'deviceIsMobile',
+ 'isOpen',
+ 'thread',
+ 'threadModel',
+ ],
+ }),
+ /**
* Formatted init thread on opening discuss for the first time,
* when no active thread is defined. Useful to set a thread to
* open without knowing its local id in advance.
* format: <threadModel>_<threadId>
*/
initActiveId: attr({
- compute: '_computeInitActiveId',
default: 'mail.box_inbox',
- dependencies: [
- 'isOpen',
- 'thread',
- 'threadId',
- 'threadModel',
- ],
}),
/**
* Determine whether current user is currently adding a channel from
@@ -430,17 +449,6 @@ function factory(dependencies) {
messaging: one2one('mail.messaging', {
inverse: 'discuss',
}),
- /**
- * When a thread changes, or some properties of it change
- * Computes whether we should display it or change it
- */
- onChangeThreadIsPinned: attr({
- compute: '_onChangeThreadIsPinned',
- dependencies: [
- 'isThreadPinned',
- 'thread',
- ],
- }),
renamingThreads: one2many('mail.thread'),
/**
* The message that is currently selected as being replied to in Inbox.
@@ -476,8 +484,21 @@ function factory(dependencies) {
sidebarQuickSearchValue: attr({
default: "",
}),
+ /**
+ * Determines the domain to apply when fetching messages for `this.thread`.
+ */
+ stringifiedDomain: attr({
+ default: '[]',
+ }),
+ /**
+ * Determines the `mail.thread` that should be displayed by `this`.
+ */
thread: many2one('mail.thread', {
- related: 'threadView.thread',
+ compute: '_computeThread',
+ dependencies: [
+ 'isThreadPinned',
+ 'thread',
+ ],
}),
threadId: attr({
related: 'thread.id',
@@ -485,8 +506,18 @@ function factory(dependencies) {
threadModel: attr({
related: 'thread.model',
}),
+ /**
+ * States the `mail.thread_view` displaying `this.thread`.
+ */
threadView: one2one('mail.thread_view', {
+ related: 'threadViewer.threadView',
+ }),
+ /**
+ * Determines the `mail.thread_viewer` managing the display of `this.thread`.
+ */
+ threadViewer: one2one('mail.thread_viewer', {
default: [['create']],
+ inverse: 'discuss',
isCausal: true,
}),
};
diff --git a/addons/mail/static/src/models/message/message.js b/addons/mail/static/src/models/message/message.js
index c92714fb3e8..2c3e3b53c91 100644
--- a/addons/mail/static/src/models/message/message.js
+++ b/addons/mail/static/src/models/message/message.js
@@ -197,6 +197,51 @@ function factory(dependencies) {
kwargs: kwargs,
});
}
+ /**
+ * Performs the `message_fetch` RPC on `mail.message`.
+ *
+ * @static
+ * @param {Array[]} domain
+ * @param {integer} [limit]
+ * @param {integer[]} [moderated_channel_ids]
+ * @param {Object} [context]
+ * @returns {mail.message[]}
+ */
+ static async performRpcMessageFetch(domain, limit, moderated_channel_ids, context) {
+ const messagesData = await this.env.services.rpc({
+ model: 'mail.message',
+ method: 'message_fetch',
+ kwargs: {
+ context,
+ domain,
+ limit,
+ moderated_channel_ids,
+ },
+ }, { shadow: true });
+ const messages = this.env.models['mail.message'].insert(messagesData.map(
+ messageData => this.env.models['mail.message'].convertData(messageData)
+ ));
+ // compute seen indicators (if applicable)
+ for (const message of messages) {
+ for (const thread of message.threads) {
+ if (thread.model !== 'mail.channel' || thread.channel_type === 'channel') {
+ // disabled on non-channel threads and
+ // on `channel` channels for performance reasons
+ continue;
+ }
+ thread.update({
+ messageSeenIndicators: [[
+ 'insert',
+ {
+ id: this.env.models['mail.message_seen_indicator'].computeId(message.id, thread.id),
+ message: [['link', message]],
+ },
+ ]],
+ });
+ }
+ }
+ return messages;
+ }
/**
* @static
diff --git a/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js b/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
index eace6856ebb..4daa33bb374 100644
--- a/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
+++ b/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
@@ -30,6 +30,11 @@ function factory(dependencies) {
model: 'mail.box',
name: this.env._t("Inbox"),
}]],
+ moderation: [['create', {
+ id: 'moderation',
+ model: 'mail.box',
+ name: this.env._t("Moderation"),
+ }]],
starred: [['create', {
id: 'starred',
isServerPinned: true,
@@ -37,7 +42,6 @@ function factory(dependencies) {
name: this.env._t("Starred"),
}]],
});
-
const device = this.messaging.device;
device.start();
const context = Object.assign({
@@ -199,14 +203,9 @@ function factory(dependencies) {
this.env.messaging.inbox.update({ counter: needaction_inbox_counter });
this.env.messaging.starred.update({ counter: starred_counter });
if (moderation_channel_ids.length > 0) {
- this.messaging.update({
- moderation: [['create', {
- counter: moderation_counter,
- id: 'moderation',
- isServerPinned: true,
- model: 'mail.box',
- name: this.env._t("Moderation"),
- }]],
+ this.messaging.moderation.update({
+ counter: moderation_counter,
+ isServerPinned: true,
});
}
}
diff --git a/addons/mail/static/src/models/messaging_menu/messaging_menu.js b/addons/mail/static/src/models/messaging_menu/messaging_menu.js
index 13c87880e6a..9f1cd8d7218 100644
--- a/addons/mail/static/src/models/messaging_menu/messaging_menu.js
+++ b/addons/mail/static/src/models/messaging_menu/messaging_menu.js
@@ -59,7 +59,7 @@ function factory(dependencies) {
return;
}
// populate some needaction messages on threads.
- inbox.mainCache.loadMessages();
+ inbox.mainCache.update({ hasToLoadMessages: true });
}
/**
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 29923af1e39..0ce0279e87d 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
@@ -155,6 +155,10 @@ function factory(dependencies) {
// knowledge of the channel
return;
}
+ if (channel.channel_type === 'channel') {
+ // disabled on `channel` channels for performance reasons
+ return;
+ }
channel.update({
partnerSeenInfos: [['insert',
{
@@ -259,7 +263,11 @@ function factory(dependencies) {
message,
});
}
- channel.markAsFetched();
+ if (channel.model === 'mail.channel' && channel.channel_type !== 'channel') {
+ // disabled on non-channel threads and
+ // on `channel` channels for performance reasons
+ channel.markAsFetched();
+ }
// (re)open chat on receiving new message
if (channel.channel_type !== 'channel') {
this.env.messaging.chatWindowManager.openThread(channel);
@@ -303,6 +311,10 @@ function factory(dependencies) {
// knowledge of the channel
return;
}
+ if (channel.channel_type === 'channel') {
+ // disabled on `channel` channels for performance reasons
+ return;
+ }
const lastMessage = this.env.models['mail.message'].insert({id: last_message_id});
const updateData = {
partnerSeenInfos: [['insert',
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 e6147fd68a0..d909fb88984 100644
--- a/addons/mail/static/src/models/thread_cache/thread_cache.js
+++ b/addons/mail/static/src/models/thread_cache/thread_cache.js
@@ -2,9 +2,7 @@ odoo.define('mail/static/src/models/thread_cache/thread_cache.js', function (req
'use strict';
const { registerNewModel } = require('mail/static/src/model/model_core.js');
-const { attr, many2many, many2one } = require('mail/static/src/model/model_field.js');
-
-const MESSAGE_FETCH_LIMIT = 30;
+const { attr, many2many, many2one, one2many } = require('mail/static/src/model/model_field.js');
function factory(dependencies) {
@@ -15,79 +13,49 @@ function factory(dependencies) {
//----------------------------------------------------------------------
/**
- * Load this thread cache, by fetching the most recent messages in this
- * conversation.
+ * @returns {mail.message[]|undefined}
*/
- async loadMessages() {
- if (this.isLoaded && this.isLoading) {
+ async loadMoreMessages() {
+ if (this.isAllHistoryLoaded || this.isLoading) {
return;
}
- const searchDomain = JSON.parse(this.stringifiedDomain);
- let domain = searchDomain.length ? searchDomain : [];
- domain = this._extendMessageDomain(domain);
- this.update({ isLoading: true });
- let messagesData = [];
- if (!this.thread.isTemporary) {
- messagesData = await this.async(() => this.env.services.rpc({
- model: 'mail.message',
- method: 'message_fetch',
- args: [domain],
- kwargs: this._getFetchMessagesKwargs(),
- }, { shadow: true }));
- }
- this._handleMessagesLoaded(messagesData);
- }
-
- async loadMoreMessages() {
- const searchDomain = JSON.parse(this.stringifiedDomain);
- let domain = searchDomain.length ? searchDomain : [];
- domain = this._extendMessageDomain(domain);
- if (this.isAllHistoryLoaded && this.isLoadingMore) {
+ if (!this.isLoaded) {
+ this.update({ hasToLoadMessages: true });
return;
}
this.update({ isLoadingMore: true });
- const minMessageId = Math.min(
- ...this.fetchedMessages.map(message => message.id)
- );
- domain = [['id', '<', minMessageId]].concat(domain);
- const messagesData = await this.async(() => this.env.services.rpc({
- model: 'mail.message',
- method: 'message_fetch',
- args: [domain],
- kwargs: this._getFetchMessagesKwargs(),
- }, { shadow: true }));
- for (const threadView of this.thread.threadViews) {
+ const messageIds = this.fetchedMessages.map(message => message.id);
+ const limit = 30;
+ const fetchedMessages = await this.async(() => this._loadMessages({
+ extraDomain: [['id', '<', Math.min(...messageIds)]],
+ limit,
+ }));
+ for (const threadView of this.threadViews) {
threadView.addComponentHint('more-messages-loaded');
}
- this._handleMessagesLoaded(messagesData);
+ this.update({ isLoadingMore: false });
+ if (fetchedMessages.length < limit) {
+ this.update({ isAllHistoryLoaded: true });
+ }
+ return fetchedMessages;
}
+ /**
+ * @returns {mail.message[]|undefined}
+ */
async loadNewMessages() {
if (this.isLoading) {
return;
}
if (!this.isLoaded) {
- await this.async(() => this.loadMessages());
+ this.update({ hasToLoadMessages: true });
return;
}
const messageIds = this.fetchedMessages.map(message => message.id);
- const searchDomain = JSON.parse(this.stringifiedDomain);
- let domain = searchDomain.length ? searchDomain : [];
- domain = this._extendMessageDomain(domain);
- if (messageIds.length > 0) {
- const lastMessageId = Math.max(...messageIds);
- domain = [['id', '>', lastMessageId]].concat(domain);
- }
- this.update({ isLoading: true });
- const messageFetchKwargs = this._getFetchMessagesKwargs();
- messageFetchKwargs.limit = false;
- const messagesData = await this.async(() => this.env.services.rpc({
- model: 'mail.message',
- method: 'message_fetch',
- args: [domain],
- kwargs: messageFetchKwargs,
- }, { shadow: true }));
- this._handleMessagesLoaded(messagesData);
+ return this._loadMessages({
+ extraDomain: [['id', '>', Math.max(...messageIds)]],
+ limit: false,
+ });
}
//----------------------------------------------------------------------
@@ -207,6 +175,20 @@ function factory(dependencies) {
/**
* @private
+ * @returns {boolean}
+ */
+ _computeHasToLoadMessages() {
+ return (
+ this.thread &&
+ !this.thread.isTemporary &&
+ this.threadViews.length > 0 &&
+ !this.isLoaded &&
+ !this.isLoading
+ );
+ }
+
+ /**
+ * @private
* @returns {mail.message[]}
*/
_computeUncheckedMessages() {
@@ -247,54 +229,53 @@ function factory(dependencies) {
/**
* @private
- * @returns {Object}
+ * @param {Object} [param0={}]
+ * @param {Array[]} [param0.extraDomain]
+ * @param {integer} [param0.limit=30]
+ * @returns {mail.message[]}
*/
- _getFetchMessagesKwargs() {
- const thread = this.thread;
- let kwargs = {
- limit: MESSAGE_FETCH_LIMIT,
- context: this.env.session.user_context,
- };
- if (thread.moderation) {
- // thread is a channel
- kwargs.moderated_channel_ids = [thread.id];
+ async _loadMessages({ extraDomain, limit = 30 } = {}) {
+ this.update({ isLoading: true });
+ const searchDomain = JSON.parse(this.stringifiedDomain);
+ let domain = searchDomain.length ? searchDomain : [];
+ domain = this._extendMessageDomain(domain);
+ if (extraDomain) {
+ domain = extraDomain.concat(domain);
+ }
+ const context = this.env.session.user_context;
+ const moderated_channel_ids = this.thread.moderation
+ ? [this.thread.id]
+ : undefined;
+ const messages = await this.async(() =>
+ this.env.models['mail.message'].performRpcMessageFetch(
+ domain,
+ limit,
+ moderated_channel_ids,
+ context,
+ )
+ );
+ this.update({
+ fetchedMessages: [['link', messages]],
+ isLoaded: true,
+ isLoading: false,
+ });
+ if (!extraDomain && messages.length < limit) {
+ this.update({ isAllHistoryLoaded: true });
}
- return kwargs;
+ return messages;
}
/**
+ * Loads this thread cache, by fetching the most recent messages in this
+ * conversation.
+ *
* @private
- * @param {Object[]} messageData
*/
- _handleMessagesLoaded(messagesData) {
- const messages = this.env.models['mail.message'].insert(messagesData.map(
- messageData => this.env.models['mail.message'].convertData(messageData)
- ));
-
- if (!this.thread) {
+ _onHasToLoadMessagesChanged() {
+ if (!this.hasToLoadMessages) {
return;
}
- this.thread.update({
- messageSeenIndicators: [[
- 'insert',
- messagesData.map(messageData => {
- return {
- id: this.env.models['mail.message_seen_indicator'].computeId(messageData.id, this.thread.id),
- message: [['insert', { id: messageData.id }]],
- };
- })
- ]],
- });
- this.update({
- fetchedMessages: [['link', messages]],
- isAllHistoryLoaded: messagesData.length < MESSAGE_FETCH_LIMIT,
- isLoaded: true,
- isLoading: false,
- isLoadingMore: false,
- });
- for (const threadView of this.thread.threadViews) {
- threadView.handleThreadCacheLoaded(this);
- }
+ this._loadMessages();
}
}
@@ -329,6 +310,19 @@ function factory(dependencies) {
compute: '_computeFetchedMessages',
dependencies: ['threadMessages'],
}),
+ /**
+ * Determines whether `this` should load initial messages.
+ */
+ hasToLoadMessages: attr({
+ compute: '_computeHasToLoadMessages',
+ dependencies: [
+ 'isLoaded',
+ 'isLoading',
+ 'thread',
+ 'threadIsTemporary',
+ 'threadViews',
+ ],
+ }),
isAllHistoryLoaded: attr({
default: false,
}),
@@ -370,6 +364,17 @@ function factory(dependencies) {
],
}),
/**
+ * Loads initial messages from `this`.
+ * This is not a "real" field, its compute function is used to trigger
+ * the load of messages at the right time.
+ */
+ onHasToLoadMessagesChanged: attr({
+ compute: '_onHasToLoadMessagesChanged',
+ dependencies: [
+ 'hasToLoadMessages',
+ ],
+ }),
+ /**
* Ordered list of messages that have been fetched by this cache.
*
* This DOES NOT necessarily includes all messages linked to this thread
@@ -393,9 +398,21 @@ function factory(dependencies) {
thread: many2one('mail.thread', {
inverse: 'caches',
}),
+ /**
+ * Serves as compute dependency.
+ */
+ threadIsTemporary: attr({
+ related: 'thread.isTemporary',
+ }),
threadMessages: many2many('mail.message', {
related: 'thread.messages',
}),
+ /**
+ * States the 'mail.thread_view' that are currently displaying `this`.
+ */
+ threadViews: one2many('mail.thread_view', {
+ inverse: 'threadCache',
+ }),
uncheckedMessages: many2many('mail.message', {
compute: '_computeUncheckedMessages',
dependencies: [
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 f0075314561..d7ea1508067 100644
--- a/addons/mail/static/src/models/thread_view/thread_view.js
+++ b/addons/mail/static/src/models/thread_view/thread_view.js
@@ -13,7 +13,7 @@ function factory(dependencies) {
* @override
*/
_willDelete() {
- this._stopLoading();
+ this.env.browser.clearTimeout(this._loaderTimeout);
return super._willDelete(...arguments);
}
@@ -41,16 +41,6 @@ function factory(dependencies) {
}
/**
- * @param {mail.thread_cache} threadCache
- */
- handleThreadCacheLoaded(threadCache) {
- if (threadCache !== this.threadCache) {
- return;
- }
- this._stopLoading();
- }
-
- /**
* @param {Object} hint
*/
markComponentHintProcessed(hint) {
@@ -77,33 +67,19 @@ function factory(dependencies) {
}
}
- /**
- * @param {string} scrollTop
- */
- saveThreadCacheScrollPositionsAsInitial(scrollTop) {
- if (!this.threadCache) {
- return;
- }
- this.update({
- threadCacheInitialScrollPositions: Object.assign({}, this.threadCacheInitialScrollPositions, {
- [this.threadCache.localId]: scrollTop,
- }),
- });
- }
-
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
- * @returns {mail.thread_cache|undefined}
+ * @returns {integer|undefined}
*/
- _computeThreadCache() {
- if (!this.thread) {
- return [];
+ _computeThreadCacheInitialScrollPosition() {
+ if (!this.threadCache) {
+ return;
}
- return [['link', this.thread.cache(this.stringifiedDomain)]];
+ return this.threadCacheInitialScrollPositions[this.threadCache.localId];
}
/**
@@ -131,75 +107,37 @@ function factory(dependencies) {
/**
* @private
- * @returns {integer|undefined}
- */
- _computeThreadCacheInitialScrollPosition() {
- if (!this.threadCache) {
- return undefined;
- }
- return this.threadCacheInitialScrollPositions[this.threadCache.localId];
- }
-
- /**
- * @private
*/
- _prepareLoading() {
- this._isPreparingLoading = true;
- this._loaderTimeout = setTimeout(() => {
- this.isShowingLoading = true;
- this._isPreparingLoading = false;
- }, 400);
+ _onThreadCacheChanged() {
+ this.addComponentHint('change-of-thread-cache');
}
/**
* @private
*/
- _stopLoading() {
- clearTimeout(this._loaderTimeout);
- this._loaderTimeout = null;
- this.isShowingLoading = false;
- this._isPreparingLoading = false;
- }
-
- /**
- * @override
- */
- _updateAfter(previous) {
- if (this.thread && this.thread !== previous.thread) {
- this._stopLoading();
- if (!this.threadCache.isLoaded && !this.threadCache.isLoading) {
- this.threadCache.loadMessages();
+ _onThreadCacheIsLoadingChanged() {
+ if (this.threadCache && this.threadCache.isLoading) {
+ if (!this.isLoading && !this.isPreparingLoading) {
+ this.update({ isPreparingLoading: true });
+ this.async(() =>
+ new Promise(resolve => {
+ this._loaderTimeout = this.env.browser.setTimeout(resolve, 400);
+ }
+ )).then(() => {
+ const isLoading = this.threadCache
+ ? this.threadCache.isLoading
+ : false;
+ this.update({ isLoading, isPreparingLoading: false });
+ });
}
+ return;
}
- if (this.threadCache !== previous.threadCache) {
- this._stopLoading();
- this.addComponentHint('change-of-thread-cache');
- }
-
- if (
- this.thread && this.threadCache && this.threadCache.isLoading &&
- !this.isShowingLoading && !this._isPreparingLoading
- ) {
- this._prepareLoading();
- }
- }
-
- /**
- * @override
- */
- _updateBefore() {
- return {
- thread: this.thread,
- threadCache: this.threadCache,
- };
+ this.env.browser.clearTimeout(this._loaderTimeout);
+ this.update({ isLoading: false, isPreparingLoading: false });
}
-
}
ThreadView.fields = {
- chatWindow: one2one('mail.chat_window', {
- inverse: 'threadView',
- }),
checkedMessages: many2many('mail.message', {
related: 'threadCache.checkedMessages',
}),
@@ -230,6 +168,31 @@ function factory(dependencies) {
hasComposerFocus: attr({
related: 'composer.hasFocus',
}),
+ /**
+ * States whether `this.threadCache` is currently loading messages.
+ *
+ * This field is related to `this.threadCache.isLoading` but with a
+ * delay on its update to avoid flickering on the UI.
+ *
+ * It is computed through `_onThreadCacheIsLoadingChanged` and it should
+ * otherwise be considered read-only.
+ */
+ isLoading: attr({
+ default: false,
+ }),
+ /**
+ * States whether `this` is aware of `this.threadCache` currently
+ * loading messages, but `this` is not yet ready to display that loading
+ * on the UI.
+ *
+ * This field is computed through `_onThreadCacheIsLoadingChanged` and
+ * it should otherwise be considered read-only.
+ *
+ * @see `this.isLoading`
+ */
+ isPreparingLoading: attr({
+ default: false,
+ }),
lastMessage: many2one('mail.message', {
related: 'thread.lastMessage',
}),
@@ -241,19 +204,48 @@ function factory(dependencies) {
messages: many2many('mail.message', {
related: 'threadCache.messages',
}),
+ /**
+ * Not a real field, used to trigger `_onThreadCacheChanged` when one of
+ * the dependencies changes.
+ */
+ onThreadCacheChanged: attr({
+ compute: '_onThreadCacheChanged',
+ dependencies: [
+ 'threadCache'
+ ],
+ }),
+ /**
+ * Not a real field, used to trigger `_onThreadCacheIsLoadingChanged`
+ * when one of the dependencies changes.
+ *
+ * @see `this.isLoading`
+ */
+ onThreadCacheIsLoadingChanged: attr({
+ compute: '_onThreadCacheIsLoadingChanged',
+ dependencies: [
+ 'threadCache',
+ 'threadCacheIsLoading',
+ ],
+ }),
+ /**
+ * Determines the domain to apply when fetching messages for `this.thread`.
+ */
stringifiedDomain: attr({
- default: '[]',
+ related: 'threadViewer.stringifiedDomain',
}),
+ /**
+ * Determines the `mail.thread` currently displayed by `this`.
+ */
thread: many2one('mail.thread', {
inverse: 'threadViews',
+ related: 'threadViewer.thread',
}),
+ /**
+ * States the `mail.thread_cache` currently displayed by `this`.
+ */
threadCache: many2one('mail.thread_cache', {
- compute: '_computeThreadCache',
- dependencies: [
- 'stringifiedDomain',
- 'thread',
- 'threadCaches',
- ],
+ inverse: 'threadViews',
+ related: 'threadViewer.threadCache',
}),
threadCacheInitialScrollPosition: attr({
compute: '_computeThreadCacheInitialScrollPosition',
@@ -263,17 +255,17 @@ function factory(dependencies) {
],
}),
/**
+ * Serves as compute dependency.
+ */
+ threadCacheIsLoading: attr({
+ related: 'threadCache.isLoading',
+ }),
+ /**
* List of saved initial scroll positions of thread caches.
- * Useful to restore scroll position on changing back to this
- * thread cache. Note that this is only applied when opening
- * the thread cache, because scroll position may change fast so
- * save is already throttled
*/
threadCacheInitialScrollPositions: attr({
default: {},
- }),
- threadCaches: many2many('mail.thread_cache', {
- related: 'thread.caches',
+ related: 'threadViewer.threadCacheInitialScrollPositions',
}),
/**
* Not a real field, used to trigger `thread.markAsSeen` when one of
@@ -288,6 +280,12 @@ function factory(dependencies) {
'threadCache',
],
}),
+ /**
+ * Determines the `mail.thread_viewer` currently managing `this`.
+ */
+ threadViewer: one2one('mail.thread_viewer', {
+ inverse: 'threadView',
+ }),
uncheckedMessages: many2many('mail.message', {
related: 'threadCache.uncheckedMessages',
}),
diff --git a/addons/mail/static/src/models/thread_view/thread_viewer.js b/addons/mail/static/src/models/thread_view/thread_viewer.js
new file mode 100644
index 00000000000..da546338ca3
--- /dev/null
+++ b/addons/mail/static/src/models/thread_view/thread_viewer.js
@@ -0,0 +1,257 @@
+odoo.define('mail/static/src/models/thread_viewer/thread_viewer.js', function (require) {
+'use strict';
+
+const { registerNewModel } = require('mail/static/src/model/model_core.js');
+const { attr, many2one, one2one } = require('mail/static/src/model/model_field.js');
+
+function factory(dependencies) {
+
+ class ThreadViewer extends dependencies['mail.model'] {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @param {string} scrollTop
+ */
+ saveThreadCacheScrollPositionsAsInitial(scrollTop) {
+ if (!this.threadCache) {
+ return;
+ }
+ this.update({
+ threadCacheInitialScrollPositions: Object.assign({}, this.threadCacheInitialScrollPositions, {
+ [this.threadCache.localId]: scrollTop,
+ }),
+ });
+ }
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @private
+ * @returns {boolean}
+ */
+ _computeHasThreadView() {
+ if (this.chatter) {
+ return this.chatter.hasThreadView;
+ }
+ if (this.chatWindow) {
+ return this.chatWindow.hasThreadView;
+ }
+ if (this.discuss) {
+ return this.discuss.hasThreadView;
+ }
+ return this.hasThreadView;
+ }
+
+ /**
+ * @private
+ * @returns {string}
+ */
+ _computeStringifiedDomain() {
+ if (this.chatter) {
+ return '[]';
+ }
+ if (this.chatWindow) {
+ return '[]';
+ }
+ if (this.discuss) {
+ return this.discuss.stringifiedDomain;
+ }
+ return this.stringifiedDomain;
+ }
+
+ /**
+ * @private
+ * @returns {mail.thread|undefined}
+ */
+ _computeThread() {
+ if (this.chatter) {
+ if (!this.chatter.thread) {
+ return [['unlink']];
+ }
+ return [['link', this.chatter.thread]];
+ }
+ if (this.chatWindow) {
+ if (!this.chatWindow.thread) {
+ return [['unlink']];
+ }
+ return [['link', this.chatWindow.thread]];
+ }
+ if (this.discuss) {
+ if (!this.discuss.thread) {
+ return [['unlink']];
+ }
+ return [['link', this.discuss.thread]];
+ }
+ return [];
+ }
+
+ /**
+ * @private
+ * @returns {mail.thread_cache|undefined}
+ */
+ _computeThreadCache() {
+ if (!this.thread) {
+ return [['unlink']];
+ }
+ return [['link', this.thread.cache(this.stringifiedDomain)]];
+ }
+
+ /**
+ * @private
+ * @returns {mail.thread_viewer|undefined}
+ */
+ _computeThreadView() {
+ if (!this.hasThreadView) {
+ return [['unlink']];
+ }
+ if (this.threadView) {
+ return [];
+ }
+ return [['create']];
+ }
+
+ }
+
+ ThreadViewer.fields = {
+ /**
+ * States the `mail.chatter` managing `this`. This field is computed
+ * through the inverse relation and should be considered read-only.
+ */
+ chatter: one2one('mail.chatter', {
+ inverse: 'threadViewer',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ chatterHasThreadView: attr({
+ related: 'chatter.hasThreadView',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ chatterThread: many2one('mail.thread', {
+ related: 'chatter.thread',
+ }),
+ /**
+ * States the `mail.chat_window` managing `this`. This field is computed
+ * through the inverse relation and should be considered read-only.
+ */
+ chatWindow: one2one('mail.chat_window', {
+ inverse: 'threadViewer',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ chatWindowHasThreadView: attr({
+ related: 'chatWindow.hasThreadView',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ chatWindowThread: many2one('mail.thread', {
+ related: 'chatWindow.thread',
+ }),
+ /**
+ * States the `mail.discuss` managing `this`. This field is computed
+ * through the inverse relation and should be considered read-only.
+ */
+ discuss: one2one('mail.discuss', {
+ inverse: 'threadViewer',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ discussHasThreadView: attr({
+ related: 'discuss.hasThreadView',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ discussStringifiedDomain: attr({
+ related: 'discuss.stringifiedDomain',
+ }),
+ /**
+ * Serves as compute dependency.
+ */
+ discussThread: many2one('mail.thread', {
+ related: 'discuss.thread',
+ }),
+ /**
+ * Determines whether `this.thread` should be displayed.
+ */
+ hasThreadView: attr({
+ compute: '_computeHasThreadView',
+ default: false,
+ dependencies: [
+ 'chatterHasThreadView',
+ 'chatWindowHasThreadView',
+ 'discussHasThreadView',
+ ],
+ }),
+ /**
+ * Determines the domain to apply when fetching messages for `this.thread`.
+ */
+ stringifiedDomain: attr({
+ compute: '_computeStringifiedDomain',
+ default: '[]',
+ dependencies: [
+ 'discussStringifiedDomain',
+ ],
+ }),
+ /**
+ * Determines the `mail.thread` that should be displayed by `this`.
+ */
+ thread: many2one('mail.thread', {
+ compute: '_computeThread',
+ dependencies: [
+ 'chatterThread',
+ 'chatWindowThread',
+ 'discussThread',
+ ],
+ }),
+ /**
+ * States the `mail.thread_cache` that should be displayed by `this`.
+ */
+ threadCache: many2one('mail.thread_cache', {
+ compute: '_computeThreadCache',
+ dependencies: [
+ 'stringifiedDomain',
+ 'thread',
+ ],
+ }),
+ /**
+ * Determines the initial scroll positions of thread caches.
+ * Useful to restore scroll position on changing back to this
+ * thread cache. Note that this is only applied when opening
+ * the thread cache, because scroll position may change fast so
+ * save is already throttled.
+ */
+ threadCacheInitialScrollPositions: attr({
+ default: {},
+ }),
+ /**
+ * States the `mail.thread_view` currently displayed and managed by `this`.
+ */
+ threadView: one2one('mail.thread_view', {
+ compute: '_computeThreadView',
+ dependencies: [
+ 'hasThreadView',
+ ],
+ inverse: 'threadViewer',
+ isCausal: true,
+ }),
+ };
+
+ ThreadViewer.modelName = 'mail.thread_viewer';
+
+ return ThreadViewer;
+}
+
+registerNewModel('mail.thread_viewer', factory);
+
+});
diff --git a/addons/mail/static/src/widgets/discuss/discuss.js b/addons/mail/static/src/widgets/discuss/discuss.js
index bf389ee69bc..53ed2f73b00 100644
--- a/addons/mail/static/src/widgets/discuss/discuss.js
+++ b/addons/mail/static/src/widgets/discuss/discuss.js
@@ -186,6 +186,7 @@ const DiscussWidget = AbstractAction.extend({
}
// Mark All Read
if (
+ this.discuss.threadView &&
this.discuss.thread &&
this.discuss.thread === this.env.messaging.inbox
) {
@@ -200,6 +201,7 @@ const DiscussWidget = AbstractAction.extend({
}
// Unstar All
if (
+ this.discuss.threadView &&
this.discuss.thread &&
this.discuss.thread === this.env.messaging.starred
) {
@@ -243,8 +245,11 @@ const DiscussWidget = AbstractAction.extend({
const $unselectAll = this.$buttons.find('.o_widget_Discuss_controlPanelButtonUnselectAll');
if (
- this.discuss.threadView.checkedMessages.length > 0 ||
- this.discuss.threadView.uncheckedMessages.length > 0
+ this.discuss.threadView &&
+ (
+ this.discuss.threadView.checkedMessages.length > 0 ||
+ this.discuss.threadView.uncheckedMessages.length > 0
+ )
) {
$selectAll.removeClass('o_hidden');
$selectAll.toggleClass('disabled', this.discuss.threadView.uncheckedMessages.length === 0);
@@ -260,6 +265,7 @@ const DiscussWidget = AbstractAction.extend({
// Moderation Actions
const $moderationButtons = this.$buttons.find('.o_widget_Discuss_controlPanelButtonModeration');
if (
+ this.discuss.threadView &&
this.discuss.threadView.checkedMessages.length > 0 &&
this.discuss.threadView.checkedMessages.filter(
message => !message.isModeratedByCurrentPartner
@@ -371,7 +377,7 @@ const DiscussWidget = AbstractAction.extend({
* @param {Object} searchQuery
*/
_onSearch: function (searchQuery) {
- this.discuss.threadView.update({
+ this.discuss.update({
stringifiedDomain: JSON.stringify(searchQuery.domain),
});
},
diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
index 266d8c60f38..375f036addd 100644
--- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js
+++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
@@ -102,7 +102,7 @@ FormRenderer.include({
followerIds,
hasActivities: this.chatterFields.hasActivityIds,
hasFollowers: this.chatterFields.hasMessageFollowerIds,
- hasThread: this.chatterFields.hasMessageIds,
+ hasMessageList: this.chatterFields.hasMessageIds,
isAttachmentBoxVisible: this.chatterFields.isAttachmentBoxVisible,
messageIds,
threadAttachmentCount,
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 235c95fb19d..cffd73b252f 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
@@ -468,9 +468,17 @@ QUnit.test('chatter updating', async function (assert) {
{ display_name: "first partner", id: 11 },
{ display_name: "second partner", id: 12 }
);
+ const messageFetchChannelDef = makeDeferred();
await this.createView({
data: this.data,
hasView: true,
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (route.includes('message_fetch')) {
+ messageFetchChannelDef.resolve();
+ }
+ return res;
+ },
// View params
View: FormView,
model: 'res.partner',
@@ -501,8 +509,10 @@ QUnit.test('chatter updating', async function (assert) {
"there should be no message"
);
- await afterNextRender(() => {
+ await afterNextRender(async () => {
document.querySelector('.o_pager_next').click();
+ // wait until messages are fetched, ignore other renders that are too early
+ await messageFetchChannelDef;
});
assert.containsOnce(
document.body,
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index 0cc23dabc79..68f22c4f7f5 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -1,6 +1,8 @@
odoo.define('mail.MockServer', function (require) {
"use strict";
+const { nextAnimationFrame } = require('mail/static/src/utils/test_utils.js');
+
const MockServer = require('web.MockServer');
MockServer.include({
@@ -197,6 +199,9 @@ MockServer.include({
return this._mockMailMessageMarkAllAsRead(domain);
}
if (args.model === 'mail.message' && args.method === 'message_fetch') {
+ // TODO FIXME delay RPC until next potential render as a workaround
+ // to issue https://github.com/odoo/owl/pull/724
+ await nextAnimationFrame();
const domain = args.args[0] || args.kwargs.domain;
const limit = args.args[1] || args.kwargs.limit;
const moderated_channel_ids = args.args[2] || args.kwargs.moderated_channel_ids;
diff --git a/addons/mail/views/assets.xml b/addons/mail/views/assets.xml
index fce138c7ca4..b560d870313 100644
--- a/addons/mail/views/assets.xml
+++ b/addons/mail/views/assets.xml
@@ -151,6 +151,7 @@
<script type="text/javascript" src="/mail/static/src/models/thread_cache/thread_cache.js"></script>
<script type="text/javascript" src="/mail/static/src/models/thread_partner_seen_info/thread_partner_seen_info.js"></script>
<script type="text/javascript" src="/mail/static/src/models/thread_view/thread_view.js"></script>
+ <script type="text/javascript" src="/mail/static/src/models/thread_view/thread_viewer.js"></script>
<script type="text/javascript" src="/mail/static/src/models/user/user.js"></script>
<script type="text/javascript" src="/mail/static/src/services/chat_window_service/chat_window_service.js"></script>
<script type="text/javascript" src="/mail/static/src/services/dialog_service/dialog_service.js"></script>