From 14b0da3d0abcc6eb57d7adb85cb9a1f06bb618c5 Mon Sep 17 00:00:00 2001 From: tsm-odoo Date: Mon, 20 Mar 2023 14:34:36 +0000 Subject: [PATCH] [IMP] mail: mark message as unread task-3232911 Part-of: odoo/odoo#116085 --- addons/mail/controllers/discuss.py | 4 +-- addons/mail/models/mail_channel.py | 23 +++++++++++------ addons/mail/static/src/core/thread_model.js | 10 +++++++- addons/mail/static/src/core/thread_service.js | 2 +- addons/mail/static/src/core_ui/message.js | 14 +++++++++++ addons/mail/static/src/core_ui/message.xml | 2 ++ addons/mail/static/src/core_ui/thread.xml | 10 ++++---- .../tests/chat_window/chat_window_tests.js | 4 +-- .../mock_server/models/mail_channel.js | 25 ++++++------------- .../static/tests/message/message_tests.js | 17 ++++++++++++- 10 files changed, 74 insertions(+), 37 deletions(-) diff --git a/addons/mail/controllers/discuss.py b/addons/mail/controllers/discuss.py index 1b5c8dba6c8..e62680394a1 100644 --- a/addons/mail/controllers/discuss.py +++ b/addons/mail/controllers/discuss.py @@ -415,9 +415,9 @@ class DiscussController(http.Controller): return messages.message_format() @http.route('/mail/channel/set_last_seen_message', methods=['POST'], type='json', auth='public') - def mail_channel_mark_as_seen(self, channel_id, last_message_id, **kwargs): + def mail_channel_mark_as_seen(self, channel_id, last_message_id, allow_older=False, **kwargs): channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id)) - return channel_member_sudo.channel_id._channel_seen(int(last_message_id)) + return channel_member_sudo.channel_id._channel_seen(last_message_id, allow_older=allow_older) @http.route('/mail/channel/notify_typing', methods=['POST'], type='json', auth='public') def mail_channel_notify_typing(self, channel_id, is_typing, **kwargs): diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 9f79a436ddc..c2d387b716f 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -962,21 +962,26 @@ class Channel(models.Model): else: self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.channel/legacy_insert', self.channel_info()[0]) - def _channel_seen(self, last_message_id=None): + def _channel_seen(self, last_message_id=None, allow_older=False): """ Mark channel as seen by updating seen message id of the current logged partner :param last_message_id: the id of the message to be marked as seen, last message of the thread by default. This param SHOULD be required, the default behaviour is DEPRECATED and kept only for compatibility reasons. + :param allow_order: whether to allow setting and older message + as the last seen message. """ self.ensure_one() domain = ["&", ("model", "=", "mail.channel"), ("res_id", "in", self.ids)] if last_message_id: - domain = expression.AND([domain, [('id', '<=', last_message_id)]]) - last_message = self.env['mail.message'].search(domain, order="id DESC", limit=1) - if not last_message: + domain = expression.AND([domain, [('id', '<=', int(last_message_id))]]) + last_message = ( + self.env["mail.message"] if last_message_id is False + else self.env['mail.message'].search(domain, order="id DESC", limit=1) + ) + if last_message_id is not False and not last_message: return - self._set_last_seen_message(last_message) + self._set_last_seen_message(last_message, allow_older=allow_older) data = { 'channel_id': self.id, 'last_message_id': last_message.id, @@ -986,22 +991,24 @@ class Channel(models.Model): self.env['bus.bus']._sendone(target, 'mail.channel.member/seen', data) return last_message.id - def _set_last_seen_message(self, last_message): + def _set_last_seen_message(self, last_message, allow_older=False): """ Set last seen message of `self` channels for the current user. :param last_message: the message to set as last seen message + :param allow_order: whether to allow setting and older message + as the last seen message. """ channel_member_domain = expression.AND([ [('channel_id', 'in', self.ids)], [('partner_id', '=', self.env.user.partner_id.id)], - expression.OR([ + [] if allow_older else expression.OR([ [('seen_message_id', '=', False)], [('seen_message_id', '<', last_message.id)] ]) ]) member = self.env['mail.channel.member'].search(channel_member_domain) member.write({ - 'fetched_message_id': last_message.id, + 'fetched_message_id': max(member.fetched_message_id.id, last_message.id), 'seen_message_id': last_message.id, 'last_seen_dt': fields.Datetime.now(), }) diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index ddde79ac4a9..00d50c86f3a 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -65,7 +65,7 @@ export class Thread { messages = []; /** @type {string} */ name; - /** @type {number} */ + /** @type {number|false} */ serverLastSeenMsgBySelf; /** @type {'opened' | 'folded' | 'closed'} */ state; @@ -403,4 +403,12 @@ export class Thread { getMemberName(persona) { return persona.name; } + + getPreviousMessage(message) { + const previousMessages = this.nonEmptyMessages.filter(({ id }) => id < message.id); + if (previousMessages.length === 0) { + return false; + } + return this._store.messages[Math.max(...previousMessages.map((m) => m.id))]; + } } diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index e6b4b04def2..baaac86ba5a 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -805,7 +805,7 @@ export class ThreadService { if (message.id <= thread.serverLastSeenMsgBySelf) { continue; } - if (message.isSelfAuthored || message.isTransient) { + if (message.isTransient) { lastSeenMessageId = message.id; continue; } diff --git a/addons/mail/static/src/core_ui/message.js b/addons/mail/static/src/core_ui/message.js index 2fe87ac3425..7b6b6cce0b2 100644 --- a/addons/mail/static/src/core_ui/message.js +++ b/addons/mail/static/src/core_ui/message.js @@ -88,6 +88,7 @@ export class Message extends Component { this.messageBody = useRef("body"); this.messaging = useMessaging(); this.store = useStore(); + this.rpc = useService("rpc"); /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useState(useService("mail.thread")); /** @type {import("@mail/core/message_service").MessageService} */ @@ -316,6 +317,19 @@ export class Message extends Component { } } + onClickMarkAsUnread() { + const previousMessageId = + this.message.originThread.getPreviousMessage(this.message)?.id ?? false; + if (this.threadService.lastSeenBySelfMessageId(this.props.thread) === previousMessageId) { + return; + } + return this.rpc("/mail/channel/set_last_seen_message", { + channel_id: this.message.originThread.id, + last_message_id: previousMessageId, + allow_older: true, + }); + } + get authorText() { return this.hasOpenChatFeature ? _t("Open chat") : ""; } diff --git a/addons/mail/static/src/core_ui/message.xml b/addons/mail/static/src/core_ui/message.xml index a9497818ef9..8575fa5db6f 100644 --- a/addons/mail/static/src/core_ui/message.xml +++ b/addons/mail/static/src/core_ui/message.xml @@ -146,6 +146,8 @@ + + +
+
New messages +
+
@@ -33,11 +38,6 @@ thread="props.thread" messageEdition="props.messageEdition" /> - -
-
New messages -
-
diff --git a/addons/mail/static/tests/chat_window/chat_window_tests.js b/addons/mail/static/tests/chat_window/chat_window_tests.js index 019165b245b..85a8437e25e 100644 --- a/addons/mail/static/tests/chat_window/chat_window_tests.js +++ b/addons/mail/static/tests/chat_window/chat_window_tests.js @@ -596,7 +596,7 @@ QUnit.test( ); QUnit.test( - "new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation", + "new message separator is shown in chat window of chat on receiving new message when there was no history", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); @@ -621,7 +621,7 @@ QUnit.test( uuid: "channel-10-uuid", }) ); - assert.containsNone($, "hr + span:contains(New messages)"); + assert.containsOnce($, "hr + span:contains(New messages)"); } ); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js index 65fea19eec3..78a768ca24e 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js @@ -644,27 +644,16 @@ patch(MockServer.prototype, "mail/models/mail_channel", { throw new Error("Should only be one channel in channel_seen mock params"); } const channel = this.getRecords("mail.channel", [["id", "=", channel_id]])[0]; - const messagesBeforeGivenLastMessage = this.getRecords("mail.message", [ - ["id", "<=", last_message_id], + const messages = this.getRecords("mail.message", [ ["model", "=", "mail.channel"], ["res_id", "=", channel.id], ]); - if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) { + if (!messages || messages.length === 0) { return; } if (!channel) { return; } - const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ - ["channel_id", "=", channel_id], - ["partner_id", "=", this.currentPartnerId], - ]); - if ( - memberOfCurrentUser.seen_message_id && - memberOfCurrentUser.seen_message_id >= last_message_id - ) { - return; - } this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id); this.pyEnv["bus.bus"]._sendone( channel.channel_type === "chat" ? channel : this.pyEnv.currentPartner, @@ -940,9 +929,11 @@ patch(MockServer.prototype, "mail/models/mail_channel", { ["channel_id", "in", ids], ["partner_id", "=", this.currentPartnerId], ]); - this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { - fetched_message_id: message_id, - seen_message_id: message_id, - }); + if (memberOfCurrentUser) { + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { + fetched_message_id: message_id, + seen_message_id: message_id, + }); + } }, }); diff --git a/addons/mail/static/tests/message/message_tests.js b/addons/mail/static/tests/message/message_tests.js index ed13e821c7c..97964fc8e44 100644 --- a/addons/mail/static/tests/message/message_tests.js +++ b/addons/mail/static/tests/message/message_tests.js @@ -515,7 +515,7 @@ QUnit.test("basic rendering of message", async (assert) => { deserializeDateTime("2019-04-20 10:00:00").toLocaleString(DateTime.DATETIME_SHORT) ); assert.containsOnce($message, ".o-mail-Message-actions"); - assert.containsN($message, ".o-mail-Message-actions i", 3); + assert.containsN($message, ".o-mail-Message-actions i", 4); assert.containsOnce($message, "i[aria-label='Add a Reaction']"); assert.containsOnce($message, "i[aria-label='Mark as Todo']"); assert.containsOnce($message, "i[aria-label='Reply']"); @@ -1385,3 +1385,18 @@ QUnit.test("Can reply to chatter messages from history", async (assert) => { await openDiscuss("mail.box_history"); assert.containsOnce($, ".o-mail-Message-actions [title='Reply']"); }); + +QUnit.test("Mark as unread", async (assert) => { + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ channel_type: "chat", name: "General" }); + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: channelId, + body: "Hello World!", + }); + const { openDiscuss } = await start(); + await openDiscuss(channelId); + await click("[title='Mark as Unread']"); + assert.containsOnce($, ".o-mail-Thread-newMessage"); + assert.containsOnce($, ".o-mail-DiscussCategoryItem .badge:contains(1)"); +});