diff options
| author | Xavier Dubuc <xdu@odoo.com> | 2020-11-02 11:31:13 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-11-02 11:31:13 +0000 |
| commit | 03481fd3df9bc53a17ba884ad4029ae46d7e54bf (patch) | |
| tree | 4e374aad0a7a6c845ee605b77cbe4e282efb7391 | |
| parent | b3e31f2680534382b048324cfcb8a6f6c457553f (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>
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}"`); }, |
