From a6c7739191ba46809eed2bdc956798ef1b2d4b56 Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Wed, 22 Mar 2023 12:17:33 +0000 Subject: [PATCH] [FIX] mail: fix message unread counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes several issues with the unread counter: - Notifications being considered as unread messages (which is not the case on the server side which resulted in inconsistencies). - Counter greater than it should be when the last seen message is not loaded. - Messages not marked as read on first thread opening. task-3239957 closes odoo/odoo#116208 Signed-off-by: Alexandre Kühn (aku) --- addons/mail/static/src/core/thread_model.js | 4 +++ addons/mail/static/src/core/thread_service.js | 16 +++++++--- .../mock_server/models/mail_message.js | 3 ++ .../mail/static/tests/thread/thread_tests.js | 32 +++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index 00d50c86f3a..d5e55306aab 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -5,6 +5,7 @@ import { sprintf } from "@web/core/utils/strings"; import { ScrollPosition } from "@mail/core/scroll_position_model"; import { createLocalId } from "../utils/misc"; +import { Deferred } from "@web/core/utils/concurrency"; /** * @typedef SeenInfo @@ -55,6 +56,7 @@ export class Thread { isAdmin = false; loadMore = false; isLoadingAttachments = false; + isLoadedDeferred = new Deferred(); isLoaded = false; /** @type {import("@mail/attachments/attachment_model").Attachment} */ mainAttachment; @@ -87,6 +89,8 @@ export class Thread { canPostOnReadonly; /** @type {String} */ last_interest_dt; + /** @type {number} */ + lastServerMessageId; constructor(store, data) { Object.assign(this, { diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index 2aa76ceaabf..5173453742d 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -101,7 +101,10 @@ export class ThreadService { /** * @param {Thread} thread */ - markAsRead(thread) { + async markAsRead(thread) { + if (!thread.isLoaded && thread.status === "loading") { + await thread.isLoadedDeferred; + } const mostRecentNonTransientMessage = thread.mostRecentNonTransientMessage; if ( this.isUnread(thread) && @@ -486,6 +489,7 @@ export class ThreadService { "last_interest_dt", "defaultDisplayMode", ]); + thread.lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessageId; if (thread.model === "mail.channel" && serverData.channel) { thread.channel = assignDefined(thread.channel ?? {}, serverData.channel); } @@ -614,6 +618,7 @@ export class ThreadService { return thread; } let thread = new Thread(this.store, data); + onChange(thread, "isLoaded", () => thread.isLoadedDeferred.resolve()); onChange(thread, "channelMembers", () => this.store.updateBusSubscription()); onChange(thread, "is_pinned", () => { if (!thread.is_pinned && this.store.discuss.threadLocalId === thread.localId) { @@ -672,7 +677,8 @@ export class ThreadService { .map((recipient) => recipient.persona.id); partner_ids?.push(...recipientIds); } - const tmpId = `${thread.localId}_${new Date().valueOf()}`; + const lastMessageId = this.messageService.getLastMessageId(); + const tmpId = lastMessageId + 0.01; const params = { post_data: { body: await prettifyMessageContent(body, validMentions), @@ -794,12 +800,12 @@ export class ThreadService { */ localMessageUnreadCounter(thread) { let baseCounter = thread.serverMessageUnreadCounter; - let countFromId = thread.serverLastSeenMsgBySelf ? thread.serverLastSeenMsgBySelf : 0; + let countFromId = thread.lastServerMessageId ? thread.lastServerMessageId : 0; const lastSeenMessageId = this.lastSeenBySelfMessageId(thread); const firstMessage = thread.messages[0]; - if (firstMessage && lastSeenMessageId && lastSeenMessageId >= firstMessage.id) { + if (firstMessage && (lastSeenMessageId === false || lastSeenMessageId >= firstMessage.id)) { baseCounter = 0; - countFromId = lastSeenMessageId; + countFromId = lastSeenMessageId || 0; } return thread.messages.reduce((total, message) => { if (message.id <= countFromId || message.temporary_id) { diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_message.js b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js index 5e7279d19b6..b2740820bd9 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_message.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js @@ -334,6 +334,9 @@ patch(MockServer.prototype, "mail/models/mail_message", { ["is_read", "=", false], ["mail_message_id", "in", messages.map((messages) => messages.id)], ]); + if (notifications.length === 0) { + return; + } this.pyEnv["mail.notification"].write( notifications.map((notification) => notification.id), { is_read: true } diff --git a/addons/mail/static/tests/thread/thread_tests.js b/addons/mail/static/tests/thread/thread_tests.js index f5f48958722..b2384d91450 100644 --- a/addons/mail/static/tests/thread/thread_tests.js +++ b/addons/mail/static/tests/thread/thread_tests.js @@ -269,6 +269,7 @@ QUnit.test( assert.verifySteps(["rpc:channel_fetch"]); $(".o-mail-Composer-input")[0].focus(); + await waitUntil(".o-mail-Message"); assert.verifySteps(["rpc:set_last_seen_message"]); } ); @@ -951,3 +952,34 @@ QUnit.test( assert.verifySteps([]); } ); + +QUnit.test("can be marked as read while loading", async function (assert) { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); + const channelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { message_unread_counter: 1, partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: partnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const loadDeferred = makeDeferred(); + const { openDiscuss } = await start({ + async mockRPC(route) { + if (route === "/mail/channel/messages") { + await loadDeferred; + } + }, + }); + await openDiscuss(undefined, { waitUntilMessagesLoaded: false }); + assert.containsOnce($, ".o-mail-DiscussCategoryItem-counter:contains(1)"); + await click(".o-mail-DiscussCategoryItem:contains(Demo)"); + await afterNextRender(loadDeferred.resolve); + assert.containsNone($, ".o-mail-DiscussCategoryItem-counter"); +});