summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Dubuc <xdu@odoo.com>2020-11-02 11:31:13 +0000
committerSébastien Theys <seb@odoo.com>2020-11-02 11:31:13 +0000
commit03481fd3df9bc53a17ba884ad4029ae46d7e54bf (patch)
tree4e374aad0a7a6c845ee605b77cbe4e282efb7391
parentb3e31f2680534382b048324cfcb8a6f6c457553f (diff)
[FIX] mail: handle transient messages with (un)seen notions
task-2343887 closes odoo/odoo#60906 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js1
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js11
-rw-r--r--addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml2
-rw-r--r--addons/mail/static/src/components/message_list/message_list.xml15
-rw-r--r--addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js4
-rw-r--r--addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml4
-rw-r--r--addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js53
-rw-r--r--addons/mail/static/src/components/thread_preview/thread_preview.js4
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js74
-rw-r--r--addons/mail/static/src/models/composer/composer.js37
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js9
-rw-r--r--addons/mail/static/src/models/thread/thread.js257
-rw-r--r--addons/mail/static/src/models/thread_view/thread_view.js40
-rw-r--r--addons/mail/static/tests/helpers/mock_server.js21
14 files changed, 424 insertions, 108 deletions
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 b5abc5b2236..b70f61475dd 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
@@ -1930,7 +1930,6 @@ QUnit.test('new message separator is shown in a chat window of a chat on receivi
is_minimized: true,
is_pinned: false,
members: [this.data.currentPartnerId, 10],
- message_unread_counter: 0,
uuid: 'channel-10-uuid',
},
];
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 77f7e875e42..91a578e7b1b 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -1890,7 +1890,16 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
QUnit.test('restore thread scroll position', async function (assert) {
assert.expect(6);
// 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 });
+ 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++) {
this.data['mail.message'].records.push({
body: "not empty",
diff --git a/addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml b/addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml
index de8992e4b8f..eddc2b01c39 100644
--- a/addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml
+++ b/addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml
@@ -7,7 +7,7 @@
<t t-if="message.author and message.author === env.messaging.currentPartner">
<i class="o_MessageAuthorPrefixIcon fa fa-mail-reply"/>You:
</t>
- <t t-elif="thread and thread.channel_type === 'channel'">
+ <t t-elif="thread and message.author !== thread.correspondent">
<t t-esc="message.author.nameOrDisplayName"/>:
</t>
</t>
diff --git a/addons/mail/static/src/components/message_list/message_list.xml b/addons/mail/static/src/components/message_list/message_list.xml
index fbd44eb5aba..432cdc5059a 100644
--- a/addons/mail/static/src/components/message_list/message_list.xml
+++ b/addons/mail/static/src/components/message_list/message_list.xml
@@ -43,12 +43,12 @@
<!-- MESSAGES -->
<t t-set="current_day" t-value="0"/>
<t t-set="prev_message" t-value="0"/>
- <t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and !threadView.thread.lastSeenByCurrentPartnerMessageId">
- <div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
- <hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
- </div>
- </t>
<t t-foreach="orderedMessages" t-as="message" t-key="message.localId">
+ <t t-if="message === threadView.thread.messageAfterNewMessageSeparator">
+ <div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
+ <hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
+ </div>
+ </t>
<t t-if="!message.isEmpty">
<t t-set="message_day" t-value="getDateDay(message)"/>
<t t-if="current_day !== message_day">
@@ -76,11 +76,6 @@
t-ref="{{ message.localId }}"
/>
<t t-set="prev_message" t-value="message"/>
- <t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and threadView.thread.lastSeenByCurrentPartnerMessageId === message.id">
- <div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
- <hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
- </div>
- </t>
</t>
</t>
<!-- LOADING (if order desc)-->
diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js b/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
index 483597a7796..8224c088611 100644
--- a/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
+++ b/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
@@ -289,6 +289,10 @@ QUnit.test('basic rendering', async function (assert) {
QUnit.test('counter is taking into account failure notification', async function (assert) {
assert.expect(2);
+ this.data['mail.channel'].records.push({
+ id: 31,
+ seen_message_id: 11,
+ });
// message that is expected to have a failure
this.data['mail.message'].records.push({
id: 11, // random unique id, will be used to link failure to message
diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml
index b6a5c896174..3047003f292 100644
--- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml
+++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml
@@ -39,9 +39,9 @@
</div>
<div class="o_ThreadNeedactionPreview_core">
<span class="o_ThreadNeedactionPreview_coreItem o_ThreadNeedactionPreview_inlineText" t-att-class="{ 'o-empty': inlineLastNeedactionMessageBody.length === 0 }">
- <t t-if="thread.lastMessage and thread.lastMessage.author">
+ <t t-if="thread.lastNeedactionMessage and thread.lastNeedactionMessage.author">
<MessageAuthorPrefix
- messageLocalId="thread.lastMessage.localId"
+ messageLocalId="thread.lastNeedactionMessage.localId"
threadLocalId="thread.localId"
/>
</t>
diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js
index 9e2f19ffbaf..fe01b8f7c7d 100644
--- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js
+++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js
@@ -292,6 +292,59 @@ QUnit.test('[technical] opening a non-channel chat window should not call channe
);
});
+QUnit.test('preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread', async function (assert) {
+ assert.expect(2);
+
+ this.data['res.partner'].records.push({
+ id: 11,
+ name: "Stranger",
+ });
+ this.data['mail.message'].records.push({
+ author_id: 11,
+ body: "I am the oldest but needaction",
+ id: 21,
+ model: 'res.partner',
+ needaction: true,
+ needaction_partner_ids: [this.data.currentPartnerId],
+ res_id: 11,
+ });
+ this.data['mail.message'].records.push({
+ author_id: this.data.currentPartnerId,
+ body: "I am more recent",
+ id: 22,
+ model: 'res.partner',
+ res_id: 11,
+ });
+ this.data['mail.notification'].records.push({
+ mail_message_id: 21,
+ notification_status: 'sent',
+ notification_type: 'inbox',
+ res_partner_id: this.data.currentPartnerId,
+ });
+ await this.start({
+ hasChatWindow: true,
+ hasMessagingMenu: true,
+ });
+ await afterNextRender(() => this.afterEvent({
+ eventName: 'o-thread-cache-loaded-messages',
+ func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
+ message: "should wait until inbox loaded initial needaction messages",
+ predicate: ({ threadCache }) => {
+ return threadCache.thread.model === 'mail.box' && threadCache.thread.id === 'inbox';
+ },
+ }));
+ assert.containsOnce(
+ document.body,
+ '.o_ThreadNeedactionPreview_inlineText',
+ "should have a preview from the last message"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ThreadNeedactionPreview_inlineText').textContent,
+ 'Stranger: I am the oldest but needaction',
+ "the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread"
+ );
+});
+
});
});
});
diff --git a/addons/mail/static/src/components/thread_preview/thread_preview.js b/addons/mail/static/src/components/thread_preview/thread_preview.js
index b10e8ee2586..92fcb6101d1 100644
--- a/addons/mail/static/src/components/thread_preview/thread_preview.js
+++ b/addons/mail/static/src/components/thread_preview/thread_preview.js
@@ -108,8 +108,8 @@ class ThreadPreview extends Component {
* @param {MouseEvent} ev
*/
_onClickMarkAsRead(ev) {
- if (this.thread.lastMessage) {
- this.thread.markAsSeen(this.thread.lastMessage.id);
+ if (this.thread.lastNonTransientMessage) {
+ this.thread.markAsSeen(this.thread.lastNonTransientMessage);
}
}
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 7a73f587e1b..89ae34a0f8e 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
@@ -1367,6 +1367,80 @@ QUnit.test('[technical] message list with a full page of empty messages should s
);
});
+QUnit.test('first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]', async function (assert) {
+ // The goal of removing the focus is to ensure the thread is not marked as seen automatically.
+ // Indeed that would trigger channel_seen no matter what, which is already covered by other tests.
+ // The goal of this test is to cover the conditions specific to transient messages,
+ // and the conditions from focus would otherwise shadow them.
+ assert.expect(3);
+
+ this.data['mail.channel_command'].records.push({ name: 'who' });
+ // Needed partner & user to allow simulation of message reception
+ this.data['res.partner'].records.push({
+ id: 11,
+ name: "Foreigner partner",
+ });
+ this.data['res.users'].records.push({
+ id: 42,
+ name: "Foreigner user",
+ partner_id: 11,
+ });
+ this.data['mail.channel'].records = [{
+ channel_type: 'channel',
+ id: 20,
+ is_pinned: true,
+ name: "General",
+ uuid: 'channel20uuid',
+ }];
+ 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 });
+ // send a command that leads to receiving a transient message
+ document.querySelector('.o_ComposerTextInput_textarea').focus();
+ await afterNextRender(() => document.execCommand('insertText', false, "/who"));
+ await afterNextRender(() => {
+ document.querySelector('.o_Composer_buttonSend').focus();
+ document.querySelector('.o_Composer_buttonSend').click();
+ });
+
+ // simulate receiving a message
+ await afterNextRender(() => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ context: {
+ mockedUserId: 42,
+ },
+ uuid: 'channel20uuid',
+ message_content: "test",
+ },
+ }));
+ assert.containsN(
+ document.body,
+ '.o_Message',
+ 2,
+ "should display 2 messages (the transient & the received message), after posting a command"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_MessageList_separatorNewMessages',
+ "separator should be shown as a message has been received"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Message[data-message-local-id="${
+ this.env.models['mail.message'].find(m => m.isTransient).localId
+ }"] + .o_MessageList_separatorNewMessages`,
+ "separator should be shown just after transient message"
+ );
+});
+
});
});
});
diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js
index a18c78d3a2e..b81e5b165ab 100644
--- a/addons/mail/static/src/models/composer/composer.js
+++ b/addons/mail/static/src/models/composer/composer.js
@@ -269,25 +269,34 @@ function factory(dependencies) {
if (thread.model === 'mail.channel') {
const command = this._getCommandFromText(body);
Object.assign(postData, {
- command: command ? command.name : undefined,
- subtype_xmlid: 'mail.mt_comment'
+ subtype_xmlid: 'mail.mt_comment',
});
- messageId = await this.async(() => this.env.services.rpc({
- model: 'mail.channel',
- method: command ? 'execute_command' : 'message_post',
- args: [thread.id],
- kwargs: postData,
- }));
+ if (command) {
+ messageId = await this.async(() => this.env.models['mail.thread'].performRpcExecuteCommand({
+ channelId: thread.id,
+ command: command.name,
+ postData,
+ }));
+ } else {
+ messageId = await this.async(() =>
+ this.env.models['mail.thread'].performRpcMessagePost({
+ postData,
+ threadId: thread.id,
+ threadModel: thread.model,
+ })
+ );
+ }
} else {
Object.assign(postData, {
subtype_xmlid: this.isLog ? 'mail.mt_note' : 'mail.mt_comment',
});
- messageId = await this.async(() => this.env.services.rpc({
- model: thread.model,
- method: 'message_post',
- args: [thread.id],
- kwargs: postData,
- }));
+ messageId = await this.async(() =>
+ this.env.models['mail.thread'].performRpcMessagePost({
+ postData,
+ threadId: thread.id,
+ threadModel: thread.model,
+ })
+ );
const [messageData] = await this.async(() => this.env.services.rpc({
model: 'mail.message',
method: 'message_format',
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 affa3c05a30..ac08419f590 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
@@ -655,14 +655,19 @@ function factory(dependencies) {
*/
_handleNotificationPartnerTransientMessage(data) {
const convertedData = this.env.models['mail.message'].convertData(data);
- const messageIds = this.env.models['mail.message'].all().map(message => message.id);
+ const lastMessageId = this.env.models['mail.message'].all().reduce(
+ (lastMessageId, message) => Math.max(lastMessageId, message.id),
+ 0
+ );
const partnerRoot = this.env.messaging.partnerRoot;
const message = this.env.models['mail.message'].create(Object.assign(convertedData, {
author: [['link', partnerRoot]],
- id: (messageIds ? Math.max(...messageIds) : 0) + 0.01,
+ id: lastMessageId + 0.01,
isTransient: true,
}));
this._notifyThreadViewsMessageReceived(message);
+ // manually force recompute of counter
+ this.messaging.messagingMenu.update();
}
/**
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index 4de44f8b3ec..c5d1371187b 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -197,7 +197,7 @@ function factory(dependencies) {
data2.public = data.public;
}
if ('seen_message_id' in data) {
- data2.lastSeenByCurrentPartnerMessageId = data.seen_message_id;
+ data2.lastSeenByCurrentPartnerMessageId = data.seen_message_id || 0;
}
if ('uuid' in data) {
data2.uuid = data.uuid;
@@ -475,17 +475,35 @@ function factory(dependencies) {
*
* @static
* @param {Object} param0
- * @param {string} param0.command the command to execute
- * @param {integer[]} param0.ids list of id of channels
+ * @param {integer} param0.channelId
+ * @param {string} param0.command
+ * @param {Object} [param0.postData={}]
*/
- static async performRpcExecuteCommand({ command, ids }) {
+ static async performRpcExecuteCommand({ channelId, command, postData = {} }) {
return this.env.services.rpc({
model: 'mail.channel',
method: 'execute_command',
- args: [ids],
- kwargs: {
- command,
- }
+ args: [[channelId]],
+ kwargs: Object.assign({ command }, postData),
+ });
+ }
+
+ /**
+ * Performs the `message_post` RPC on given threadModel.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.postData
+ * @param {integer} param0.threadId
+ * @param {string} param0.threadModel
+ * @return {integer} the posted message id
+ */
+ static async performRpcMessagePost({ postData, threadId, threadModel }) {
+ return this.env.services.rpc({
+ model: threadModel,
+ method: 'message_post',
+ args: [threadId],
+ kwargs: postData,
});
}
@@ -615,26 +633,25 @@ function factory(dependencies) {
/**
* Mark the specified conversation as read/seen.
*
- * @param {integer} messageId the message to be considered as last seen
+ * @param {mail.message} message the message to be considered as last seen.
*/
- async markAsSeen(messageId) {
+ async markAsSeen(message) {
if (this.model !== 'mail.channel') {
return;
}
- if (this.pendingSeenMessageId && messageId <= this.pendingSeenMessageId) {
+ if (this.pendingSeenMessageId && message.id <= this.pendingSeenMessageId) {
return;
}
if (
this.lastSeenByCurrentPartnerMessageId &&
- messageId <= this.lastSeenByCurrentPartnerMessageId
+ message.id <= this.lastSeenByCurrentPartnerMessageId
) {
return;
}
- this.update({ pendingSeenMessageId: messageId });
+ this.update({ pendingSeenMessageId: message.id });
return this.env.models['mail.thread'].performRpcChannelSeen({
ids: [this.id],
- // commands have fake message id that is not integer
- lastMessageId: Math.floor(messageId),
+ lastMessageId: message.id,
});
}
@@ -677,9 +694,9 @@ function factory(dependencies) {
uuid: this.uuid,
});
} else {
- await this.env.models['mail.thread'].performRpcExecuteCommand({
+ this.env.models['mail.thread'].performRpcExecuteCommand({
+ channelId: this.id,
command: 'leave',
- ids: [this.id],
});
}
}
@@ -1106,9 +1123,6 @@ function factory(dependencies) {
* @returns {mail.message}
*/
_computeLastCurrentPartnerMessageSeenByEveryone() {
- if (!this.partnerSeenInfos || !this.orderedMessages) {
- return [['unlink-all']];
- }
const otherPartnerSeenInfos =
this.partnerSeenInfos.filter(partnerSeenInfo =>
partnerSeenInfo.partner !== this.messagingCurrentPartner);
@@ -1127,7 +1141,7 @@ function factory(dependencies) {
...otherPartnersLastSeenMessageIds
);
const currentPartnerOrderedSeenMessages =
- this.orderedMessages.filter(message =>
+ this.orderedNonTransientMessages.filter(message =>
message.author === this.messagingCurrentPartner &&
message.id <= lastMessageSeenByAllId);
@@ -1159,6 +1173,56 @@ function factory(dependencies) {
* @private
* @returns {mail.message|undefined}
*/
+ _computeLastNonTransientMessage() {
+ const {
+ length: l,
+ [l - 1]: lastMessage,
+ } = this.orderedNonTransientMessages;
+ if (lastMessage) {
+ return [['link', lastMessage]];
+ }
+ return [['unlink']];
+ }
+
+ /**
+ * Adjusts the last seen message received from the server to consider
+ * the following messages also as read if they are either transient
+ * messages or messages from the current partner.
+ *
+ * @private
+ * @returns {integer}
+ */
+ _computeLastSeenByCurrentPartnerMessageId() {
+ const firstMessage = this.orderedMessages[0];
+ if (
+ firstMessage &&
+ this.lastSeenByCurrentPartnerMessageId &&
+ this.lastSeenByCurrentPartnerMessageId < firstMessage.id
+ ) {
+ // no deduction can be made if there is a gap
+ return this.lastSeenByCurrentPartnerMessageId;
+ }
+ let lastSeenByCurrentPartnerMessageId = this.lastSeenByCurrentPartnerMessageId;
+ for (const message of this.orderedMessages) {
+ if (message.id <= this.lastSeenByCurrentPartnerMessageId) {
+ continue;
+ }
+ if (
+ message.author === this.env.messaging.currentPartner ||
+ message.isTransient
+ ) {
+ lastSeenByCurrentPartnerMessageId = message.id;
+ continue;
+ }
+ return lastSeenByCurrentPartnerMessageId;
+ }
+ return lastSeenByCurrentPartnerMessageId;
+ }
+
+ /**
+ * @private
+ * @returns {mail.message|undefined}
+ */
_computeLastNeedactionMessage() {
const orderedNeedactionMessages = this.needactionMessages.sort(
(m1, m2) => m1.id < m2.id ? -1 : 1
@@ -1186,41 +1250,31 @@ function factory(dependencies) {
* @returns {integer}
*/
_computeLocalMessageUnreadCounter() {
- // the only situations where serverMessageUnreadCounter can/have to be
- // trusted are:
- // - we have no last message (and then no messages at all)
- // - the message it used to compute is the last message we know
- if (this.orderedMessages.length === 0) {
- return this.serverMessageUnreadCounter;
- }
- // from here serverLastMessageId is not undefined because
- // orderedMessages contain at least one message.
- if (!this.lastSeenByCurrentPartnerMessageId) {
- return this.serverMessageUnreadCounter;
- }
- // if server knows more messages than the client knows then we just
- // need to trust him
- if (this.serverLastMessageId > this.lastSeenByCurrentPartnerMessageId) {
- return this.serverMessageUnreadCounter;
- }
+ // By default trust the server up to the last message it used
+ // because it's not possible to do better.
+ let baseCounter = this.serverMessageUnreadCounter;
+ let countFromId = this.serverLastMessageId;
+ // But if the client knows the last seen message that the server
+ // returned (and by assumption all the messages that come after),
+ // the counter can be computed fully locally, ignoring potentially
+ // obsolete values from the server.
const firstMessage = this.orderedMessages[0];
- // if the lastSeenByCurrentPartnerMessageId is not known (not fetched), then we
- // need to rely on server value to determine the amount of unread
- // messages until the last message it knew when computing the
- // serverMessageUnreadCounter
- if (this.lastSeenByCurrentPartnerMessageId < firstMessage.id) {
- const fetchedNotSeenMessages = this.orderedMessages.filter(message =>
- message.id > this.serverLastMessageId && message.author !== this.env.messaging.currentPartner
- );
- return this.serverMessageUnreadCounter + fetchedNotSeenMessages.length;
- }
- // lastSeenByCurrentPartnerMessageId is a known message,
- // then we can forget serverMessageUnreadCounter
- const maxId = this.lastSeenByCurrentPartnerMessageId;
- return this.orderedMessages.reduce(
- (acc, message) => acc + ((message.id > maxId && message.author !== this.env.messaging.currentPartner) ? 1 : 0),
- 0
- );
+ if (
+ firstMessage &&
+ this.lastSeenByCurrentPartnerMessageId &&
+ this.lastSeenByCurrentPartnerMessageId >= firstMessage.id
+ ) {
+ baseCounter = 0;
+ countFromId = this.lastSeenByCurrentPartnerMessageId;
+ }
+ // Include all the messages that are known locally but the server
+ // didn't take into account.
+ return this.orderedMessages.reduce((total, message) => {
+ if (message.id <= countFromId) {
+ return total;
+ }
+ return total + 1;
+ }, baseCounter);
}
/**
@@ -1241,6 +1295,27 @@ function factory(dependencies) {
/**
* @private
+ * @returns {mail.message|undefined}
+ */
+ _computeMessageAfterNewMessageSeparator() {
+ if (this.model !== 'mail.channel') {
+ return [['unlink']];
+ }
+ if (this.localMessageUnreadCounter === 0) {
+ return [['unlink']];
+ }
+ const index = this.orderedMessages.findIndex(message =>
+ message.id === this.lastSeenByCurrentPartnerMessageId
+ );
+ const message = this.orderedMessages[index + 1];
+ if (!message) {
+ return [['unlink']];
+ }
+ return [['link', message]];
+ }
+
+ /**
+ * @private
* @returns {mail.message[]}
*/
_computeOrderedMessages() {
@@ -1249,6 +1324,14 @@ function factory(dependencies) {
/**
* @private
+ * @returns {mail.message[]}
+ */
+ _computeOrderedNonTransientMessages() {
+ return [['replace', this.orderedMessages.filter(m => !m.isTransient)]];
+ }
+
+ /**
+ * @private
* @returns {mail.partner[]}
*/
_computeOrderedOtherTypingMembers() {
@@ -1639,11 +1722,14 @@ function factory(dependencies) {
lastCurrentPartnerMessageSeenByEveryone: many2one('mail.message', {
compute: '_computeLastCurrentPartnerMessageSeenByEveryone',
dependencies: [
- 'partnerSeenInfos',
- 'orderedMessages',
'messagingCurrentPartner',
+ 'orderedNonTransientMessages',
+ 'partnerSeenInfos',
],
}),
+ /**
+ * Last message of the thread, could be a transient one.
+ */
lastMessage: many2one('mail.message', {
compute: '_computeLastMessage',
dependencies: ['orderedMessages'],
@@ -1653,17 +1739,30 @@ function factory(dependencies) {
dependencies: ['needactionMessages'],
}),
/**
+ * Last non-transient message.
+ */
+ lastNonTransientMessage: many2one('mail.message', {
+ compute: '_computeLastNonTransientMessage',
+ dependencies: ['orderedNonTransientMessages'],
+ }),
+ /**
* Last seen message id of the channel by current partner.
*
- * If there is a pending seen message id change, it is immediately applied
- * on the interface to avoid a feeling of unresponsiveness. Otherwise the
- * last known message id of the server is used.
- *
* Also, it needs to be kept as an id because it's considered like a "date" and could stay
* even if corresponding message is deleted. It is basically used to know which
* messages are before or after it.
*/
- lastSeenByCurrentPartnerMessageId: attr(),
+ lastSeenByCurrentPartnerMessageId: attr({
+ compute: '_computeLastSeenByCurrentPartnerMessageId',
+ default: 0,
+ dependencies: [
+ 'lastSeenByCurrentPartnerMessageId',
+ 'messagingCurrentPartner',
+ 'orderedMessages',
+ 'orderedMessagesIsTransient',
+ // FIXME missing dependency 'orderedMessages.author', (task-2261221)
+ ],
+ }),
/**
* Local value of message unread counter, that means it is based on initial server value and
* updated with interface updates.
@@ -1671,7 +1770,6 @@ function factory(dependencies) {
localMessageUnreadCounter: attr({
compute: '_computeLocalMessageUnreadCounter',
dependencies: [
- 'lastMessage',
'lastSeenByCurrentPartnerMessageId',
'messagingCurrentPartner',
'orderedMessages',
@@ -1688,6 +1786,19 @@ function factory(dependencies) {
members: many2many('mail.partner', {
inverse: 'memberThreads',
}),
+ /**
+ * Determines the message before which the "new message" separator must
+ * be positioned, if any.
+ */
+ messageAfterNewMessageSeparator: many2one('mail.message', {
+ compute: '_computeMessageAfterNewMessageSeparator',
+ dependencies: [
+ 'lastSeenByCurrentPartnerMessageId',
+ 'localMessageUnreadCounter',
+ 'model',
+ 'orderedMessages',
+ ],
+ }),
message_needaction_counter: attr({
default: 0,
}),
@@ -1753,11 +1864,31 @@ function factory(dependencies) {
'serverFoldState',
],
}),
+ /**
+ * All messages ordered like they are displayed.
+ */
orderedMessages: many2many('mail.message', {
compute: '_computeOrderedMessages',
dependencies: ['messages'],
}),
/**
+ * Serves as compute dependency. (task-2261221)
+ */
+ orderedMessagesIsTransient: attr({
+ related: 'orderedMessages.isTransient',
+ }),
+ /**
+ * All messages ordered like they are displayed. This field does not
+ * contain transient messages which are not "real" records.
+ */
+ orderedNonTransientMessages: many2many('mail.message', {
+ compute: '_computeOrderedNonTransientMessages',
+ dependencies: [
+ 'orderedMessages',
+ 'orderedMessagesIsTransient',
+ ],
+ }),
+ /**
* Ordered typing members on this thread, excluding the current partner.
*/
orderedOtherTypingMembers: many2many('mail.partner', {
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 895732f8872..5c65e19c0f8 100644
--- a/addons/mail/static/src/models/thread_view/thread_view.js
+++ b/addons/mail/static/src/models/thread_view/thread_view.js
@@ -114,18 +114,29 @@ function factory(dependencies) {
* @returns {boolean}
*/
_computeThreadShouldBeSetAsSeen() {
- // FIXME condition should not be on "composer is focused" but "threadView is active"
- // See task-2277543
- const lastMessageIsVisible = this.lastVisibleMessage &&
- this.lastVisibleMessage === this.lastMessage;
- if (lastMessageIsVisible && this.hasComposerFocus && this.thread) {
- this.thread.markAsSeen(this.lastMessage.id).catch(e => {
- // prevent crash when executing compute during destroy
- if (!(e instanceof RecordDeletedError)) {
- throw e;
- }
- });
+ if (!this.thread) {
+ return;
+ }
+ if (!this.thread.lastNonTransientMessage) {
+ return;
+ }
+ if (!this.lastVisibleMessage) {
+ return;
+ }
+ if (this.lastVisibleMessage !== this.lastMessage) {
+ return;
}
+ if (!this.hasComposerFocus) {
+ // FIXME condition should not be on "composer is focused" but "threadView is active"
+ // See task-2277543
+ return;
+ }
+ this.thread.markAsSeen(this.thread.lastNonTransientMessage).catch(e => {
+ // prevent crash when executing compute during destroy
+ if (!(e instanceof RecordDeletedError)) {
+ throw e;
+ }
+ });
}
/**
@@ -233,6 +244,12 @@ function factory(dependencies) {
related: 'thread.lastMessage',
}),
/**
+ * Serves as compute dependency.
+ */
+ lastNonTransientMessage: many2one('mail.message', {
+ related: 'thread.lastNonTransientMessage',
+ }),
+ /**
* Most recent message in this ThreadView that has been shown to the
* current partner in the currently displayed thread cache.
*/
@@ -329,6 +346,7 @@ function factory(dependencies) {
dependencies: [
'hasComposerFocus',
'lastMessage',
+ 'lastNonTransientMessage',
'lastVisibleMessage',
'threadCache',
],
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index 80e3947cef1..9f39f0eb9f1 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -851,7 +851,8 @@ MockServer.include({
* @private
*/
_mockMailChannelExecuteCommand(args) {
- const [ids, commandName] = args.args;
+ const ids = args.args[0];
+ const commandName = args.kwargs.command || args.args[1];
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
if (commandName === 'leave') {
for (const channel of channels) {
@@ -866,6 +867,24 @@ MockServer.include({
this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmUnpin]);
}
return;
+ } else if (commandName === 'who') {
+ for (const channel of channels) {
+ const members = channel.members.map(memberId => this._getRecords('res.partner', [['id', '=', memberId]])[0].name);
+ let message = "You are alone in this channel.";
+ if (members.length > 0) {
+ message = `Users in this channel: ${members.join(', ')} and you`
+ }
+ const notification = [
+ ["dbName", 'res.partner', this.currentPartnerId],
+ {
+ 'body': `<span class="o_mail_notification">${message}</span>`,
+ 'channel_ids': [channel.id],
+ 'info': 'transient_message',
+ }
+ ];
+ this._widget.call('bus_service', 'trigger', 'notification', [notification]);
+ }
+ return;
}
throw new Error(`mail/mock_server: the route execute_command doesn't implement the command "${commandName}"`);
},