diff --git a/addons/im_livechat/static/src/core/thread_service_patch.js b/addons/im_livechat/static/src/core/thread_service_patch.js index 028eee00899..b66bc7cfbdc 100644 --- a/addons/im_livechat/static/src/core/thread_service_patch.js +++ b/addons/im_livechat/static/src/core/thread_service_patch.js @@ -44,14 +44,14 @@ patch(ThreadService.prototype, "im_livechat", { canUnpin(thread) { if (thread.type === "livechat") { - return this.localMessageUnreadCounter(thread) === 0; + return thread.message_unread_counter === 0; } return this._super(thread); }, getCounter(thread) { if (thread.type === "livechat") { - return this.localMessageUnreadCounter(thread); + return thread.message_needaction_counter; } return this._super(thread); }, diff --git a/addons/mail/controllers/discuss.py b/addons/mail/controllers/discuss.py index 9376166e95e..64a4231e08d 100644 --- a/addons/mail/controllers/discuss.py +++ b/addons/mail/controllers/discuss.py @@ -213,16 +213,16 @@ class DiscussController(http.Controller): # -------------------------------------------------------------------------- @http.route('/mail/inbox/messages', methods=['POST'], type='json', auth='user') - def discuss_inbox_messages(self, max_id=None, min_id=None, limit=30, **kwargs): - return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], max_id=max_id, min_id=min_id, limit=limit).message_format() + def discuss_inbox_messages(self, before=None, after=None, limit=30, **kwargs): + return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], before=before, after=after, limit=limit).message_format() @http.route('/mail/history/messages', methods=['POST'], type='json', auth='user') - def discuss_history_messages(self, max_id=None, min_id=None, limit=30, **kwargs): - return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], max_id=max_id, min_id=min_id, limit=limit).message_format() + def discuss_history_messages(self, before=None, after=None, limit=30, **kwargs): + return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], before=before, after=after, limit=limit).message_format() @http.route('/mail/starred/messages', methods=['POST'], type='json', auth='user') - def discuss_starred_messages(self, max_id=None, min_id=None, limit=30, **kwargs): - return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], max_id=max_id, min_id=min_id, limit=limit).message_format() + def discuss_starred_messages(self, before=None, after=None, limit=30, **kwargs): + return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], before=before, after=after, limit=limit).message_format() # -------------------------------------------------------------------------- # Thread API (channel/chatter common) @@ -406,13 +406,13 @@ class DiscussController(http.Controller): raise NotFound() @http.route('/mail/channel/messages', methods=['POST'], type='json', auth='public') - def mail_channel_messages(self, channel_id, max_id=None, min_id=None, limit=30, **kwargs): + def mail_channel_messages(self, channel_id, before=None, after=None, limit=30, **kwargs): channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id)) messages = channel_member_sudo.env['mail.message']._message_fetch(domain=[ ('res_id', '=', channel_id), ('model', '=', 'mail.channel'), ('message_type', '!=', 'user_notification'), - ], max_id=max_id, min_id=min_id, limit=limit) + ], before=before, after=after, limit=limit) if not request.env.user._is_public(): messages.set_message_done() return messages.message_format() @@ -451,12 +451,12 @@ class DiscussController(http.Controller): return thread._get_mail_thread_data(request_list) @http.route('/mail/thread/messages', methods=['POST'], type='json', auth='user') - def mail_thread_messages(self, thread_model, thread_id, max_id=None, min_id=None, limit=30, **kwargs): + def mail_thread_messages(self, thread_model, thread_id, before=None, after=None, limit=30, **kwargs): messages = request.env['mail.message']._message_fetch(domain=[ ('res_id', '=', int(thread_id)), ('model', '=', thread_model), ('message_type', '!=', 'user_notification'), - ], max_id=max_id, min_id=min_id, limit=limit) + ], before=before, after=after, limit=limit) if not request.env.user._is_public(): messages.set_message_done() return messages.message_format() diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index be993ad1061..38cb666a8d0 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -732,8 +732,6 @@ class Channel(models.Model): 'message': message_format, } notifications.append((channel, 'mail.channel/new_message', payload)) - if self.env.user.partner_id == message.author_id: - self._channel_seen(last_message_id=message.id) return notifications # ------------------------------------------------------------ @@ -808,7 +806,7 @@ class Channel(models.Model): if member: channel_data['channelMembers'] = [('insert', list(member._mail_channel_member_format().values()))] info['state'] = member.fold_state or 'open' - channel_data['serverMessageUnreadCounter'] = member.message_unread_counter + channel_data['message_unread_counter'] = member.message_unread_counter info['is_minimized'] = member.is_minimized info['seen_message_id'] = member.seen_message_id.id channel_data['custom_channel_name'] = member.custom_channel_name diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index b6df0852c87..38a5150700b 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -917,18 +917,18 @@ class Message(models.Model): return vals_list @api.model - def _message_fetch(self, domain, max_id=None, min_id=None, limit=30): + def _message_fetch(self, domain, before=None, after=None, limit=30): """ Get a limited amount of formatted messages with provided domain. :param domain: the domain to filter messages; - :param min_id: messages must be more recent than this id - :param max_id: message must be less recent than this id + :param after: messages must be more recent than this id + :param before: message must be less recent than this id :param limit: the maximum amount of messages to get; :returns: record set of mail.message """ - if max_id: - domain = expression.AND([domain, [('id', '<', max_id)]]) - if min_id: - domain = expression.AND([domain, [('id', '>', min_id)]]) + if before: + domain = expression.AND([domain, [('id', '<', before)]]) + if after: + domain = expression.AND([domain, [('id', '>', after)]]) return self.search(domain, limit=limit) def message_format(self, format_reply=True, msg_vals=None): diff --git a/addons/mail/static/src/core/message_model.js b/addons/mail/static/src/core/message_model.js index e5f5ac5dd2e..f3fec94a7f4 100644 --- a/addons/mail/static/src/core/message_model.js +++ b/addons/mail/static/src/core/message_model.js @@ -74,9 +74,6 @@ export class Message { * @returns {boolean} */ get editable() { - if (this.isEmpty) { - return false; - } if (!this._store.user?.isAdmin && !this.isSelfAuthored) { return false; } diff --git a/addons/mail/static/src/core/message_service.js b/addons/mail/static/src/core/message_service.js index dff1877bd6a..348a502da84 100644 --- a/addons/mail/static/src/core/message_service.js +++ b/addons/mail/static/src/core/message_service.js @@ -125,7 +125,7 @@ export class MessageService { createTransient(data) { const { body, res_id, model } = data; const lastMessageId = this.getLastMessageId(); - this.insert({ + return this.insert({ author: this.store.odoobot, body, id: lastMessageId + 0.01, @@ -177,17 +177,15 @@ export class MessageService { updateStarred(message, isStarred) { message.isStarred = isStarred; + const starred = this.store.discuss.starred; if (isStarred) { - this.store.discuss.starred.counter++; - if (this.store.discuss.starred.messages.length > 0) { - this.store.discuss.starred.messages.push(message); + starred.counter++; + if (!starred.messages.includes(message)) { + starred.messages.push(message); } } else { - this.store.discuss.starred.counter--; - removeFromArrayWithPredicate( - this.store.discuss.starred.messages, - ({ id }) => id === message.id - ); + starred.counter--; + removeFromArrayWithPredicate(starred.messages, ({ id }) => id === message.id); } } @@ -287,28 +285,6 @@ export class MessageService { message.originThread.modelName = data.res_model_name; } this._updateReactions(message, data.messageReactionGroups); - if (message.originThread && !message.originThread.messages.includes(message)) { - message.originThread.messages.push(message); - this.sortMessages(message.originThread); - } - if (message.isNeedaction && !this.store.discuss.inbox.messages.includes(message)) { - if (!fromFetch) { - this.store.discuss.inbox.counter++; - if (message.originThread) { - message.originThread.message_needaction_counter++; - } - } - this.store.discuss.inbox.messages.push(message); - this.sortMessages(this.store.discuss.inbox); - } - if (message.isStarred && !this.store.discuss.starred.messages.includes(message)) { - this.store.discuss.starred.messages.push(message); - this.sortMessages(this.store.discuss.starred); - } - if (message.isHistory && !this.store.discuss.history.messages.includes(message)) { - this.store.discuss.history.messages.push(message); - this.sortMessages(this.store.discuss.history); - } } updateNotifications(message) { @@ -474,15 +450,6 @@ export class MessageService { group.resIds.add(data.resId); } - /** - * @param {import("@mail/core/thread_model").Thread} thread - */ - sortMessages(thread) { - thread.messages.sort((msg1, msg2) => { - return msg1.id - msg2.id; - }); - } - scheduledDateSimple(message) { return message.scheduledDate.toLocaleString(DateTime.TIME_SIMPLE, { locale: this.userService.lang.replace("_", "-"), diff --git a/addons/mail/static/src/core/messaging_service.js b/addons/mail/static/src/core/messaging_service.js index 89fc0e97974..8444d69d08d 100644 --- a/addons/mail/static/src/core/messaging_service.js +++ b/addons/mail/static/src/core/messaging_service.js @@ -2,7 +2,6 @@ import { markup, reactive } from "@odoo/owl"; import { Deferred } from "@web/core/utils/concurrency"; -import { memoize } from "@web/core/utils/functions"; import { cleanTerm } from "@mail/utils/format"; import { removeFromArray, removeFromArrayWithPredicate } from "@mail/utils/arrays"; import { LinkPreview } from "./link_preview_model"; @@ -251,10 +250,15 @@ export class Messaging { type: notif.payload.channel.channel_type, }); break; - case "mail.channel/transient_message": - return this.messageService.createTransient( + case "mail.channel/transient_message": { + const channel = + this.store.threads[createLocalId("mail.channel", notif.payload.res_id)]; + const message = this.messageService.createTransient( Object.assign(notif.payload, { body: markup(notif.payload.body) }) ); + channel.messages.push(message); + break; + } case "mail.link.preview/delete": { const { id, message_id } = notif.payload; @@ -266,7 +270,12 @@ export class Messaging { break; case "mail.message/inbox": { const data = Object.assign(notif.payload, { body: markup(notif.payload.body) }); - this.messageService.insert(data); + const message = this.messageService.insert(data); + const inbox = this.store.discuss.inbox; + if (!inbox.messages.includes(message)) { + inbox.messages.push(message); + inbox.counter++; + } break; } case "mail.message/delete": { @@ -295,12 +304,22 @@ export class Messaging { message.originThread.messages, ({ id }) => id === message.id ); + if (message.isNeedaction) { + removeFromArrayWithPredicate( + message.originThread.needactionMessages, + ({ id }) => id === message.id + ); + } + if (message.id > message.originThread.seen_message_id) { + message.originThread.message_unread_counter--; + } } } break; } case "mail.message/mark_as_read": { const { message_ids: messageIds, needaction_inbox_counter } = notif.payload; + const inbox = this.store.discuss.inbox; for (const messageId of messageIds) { // We need to ignore all not yet known messages because we don't want them // to be shown partially as they would be linked directly to cache. @@ -315,25 +334,25 @@ export class Messaging { const originThread = message.originThread; if (originThread && message.isNeedaction) { originThread.message_needaction_counter--; + removeFromArrayWithPredicate( + originThread.needactionMessages, + ({ id }) => id === messageId + ); } // move messages from Inbox to history const partnerIndex = message.needaction_partner_ids.find( (p) => p === this.store.user?.id ); removeFromArray(message.needaction_partner_ids, partnerIndex); - removeFromArrayWithPredicate( - this.store.discuss.inbox.messages, - ({ id }) => id === messageId - ); - if (this.store.discuss.history.messages.length > 0) { - this.store.discuss.history.messages.push(message); + removeFromArrayWithPredicate(inbox.messages, ({ id }) => id === messageId); + const history = this.store.discuss.history; + if (!history.messages.includes(message)) { + history.messages.push(message); } } - this.store.discuss.inbox.counter = needaction_inbox_counter; - if ( - this.store.discuss.inbox.counter > this.store.discuss.inbox.messages.length - ) { - this.threadService.fetchMessages(this.store.discuss.inbox); + inbox.counter = needaction_inbox_counter; + if (inbox.counter > inbox.messages.length) { + this.threadService.fetchMoreMessages(inbox); } break; } @@ -342,7 +361,6 @@ export class Messaging { for (const messageId of messageIds) { const message = this.messageService.insert({ id: messageId }); this.messageService.updateStarred(message, starred); - this.messageService.sortMessages(this.store.discuss.starred); } break; } @@ -354,8 +372,8 @@ export class Messaging { // knowledge of the channel continue; } - if (partner_id && this.store.user?.id === partner_id) { - channel.serverLastSeenMsgBySelf = last_message_id; + if (partner_id && partner_id === this.store.user?.id) { + this.threadService.updateSeen(channel, last_message_id); } const seenInfo = channel.seenInfos.find( (seenInfo) => seenInfo.partner.id === partner_id @@ -490,6 +508,25 @@ export class Messaging { res_id: channel.id, model: channel.model, }); + if (!channel.messages.includes(message)) { + channel.messages.push(message); + if (message.isSelfAuthored) { + channel.seen_message_id = message.id; + } else { + channel.message_unread_counter++; + if (message.isNeedaction) { + const inbox = this.store.discuss.inbox; + if (!inbox.messages.includes(message)) { + inbox.messages.push(message); + inbox.counter++; + } + if (!channel.needactionMessages.includes(message)) { + channel.needactionMessages.push(message); + channel.message_needaction_counter++; + } + } + } + } if (channel.chatPartnerId !== this.store.odoobot?.id) { if (!this.presence.isOdooFocused() && channel.isChatChannel) { this.outOfFocusService.notify(message, channel); @@ -504,9 +541,9 @@ export class Messaging { if ( !message.isSelfAuthored && channel.composer.isFocused && - channel.mostRecentNonTransientMessage && + channel.newestPersistentMessage && !this.store.guest && - channel.mostRecentNonTransientMessage === channel.mostRecentMsg + channel.newestPersistentMessage === channel.newestMessage ) { this.threadService.markAsRead(channel); } @@ -618,25 +655,6 @@ export class Messaging { // actions that can be performed on the messaging system // ------------------------------------------------------------------------- - fetchPreviews = memoize(async () => { - const ids = []; - for (const thread of Object.values(this.store.threads)) { - if (["channel", "group", "chat"].includes(thread.type)) { - ids.push(thread.id); - } - } - if (ids.length) { - const previews = await this.orm.call("mail.channel", "channel_fetch_preview", [ids]); - for (const preview of previews) { - const thread = this.store.threads[createLocalId("mail.channel", preview.id)]; - const data = Object.assign(preview.last_message, { - body: markup(preview.last_message.body), - }); - this.messageService.insert({ ...data, res_id: thread.id, model: thread.model }); - } - } - }); - async searchPartners(searchStr = "", limit = 10) { let partners = []; const searchTerm = cleanTerm(searchStr); diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index 476d3b93476..d0506d07137 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -63,12 +63,32 @@ export class Thread { memberCount = 0; message_needaction_counter = 0; message_unread_counter = 0; - /** @type {import("@mail/core/message_model").Message[]} */ + /** + * Contains continuous sequence of messages to show in message list. + * There should not be any hole in this list: there can be unknown + * messages before start and after end, but there should not be any + * unknown in-between messages. + * + * Content should be fetched and inserted in a controlled way. + * + * @type {import("@mail/core/message_model").Message[]} + */ messages = []; + /** + * Contains continuous sequence of needaction messages to show in messaging menu. + * There should not be any hole in this list: there can be unknown + * messages before start and after end, but there should not be any + * unknown in-between messages. + * + * Content should be fetched and inserted in a controlled way. + * + * @type {import("@mail/core/message_model").Message[]} + */ + needactionMessages = []; /** @type {string} */ name; /** @type {number|false} */ - serverLastSeenMsgBySelf; + seen_message_id; /** @type {'opened' | 'folded' | 'closed'} */ state; status = "new"; @@ -82,7 +102,6 @@ export class Thread { defaultDisplayMode; /** @type {SeenInfo[]} */ seenInfos = []; - serverMessageUnreadCounter = 0; /** @type {SuggestedRecipient[]} */ suggestedRecipients = []; hasLoadingFailed = false; @@ -241,7 +260,7 @@ export class Thread { } get lastEditableMessageOfSelf() { - const editableMessagesBySelf = this.messages.filter( + const editableMessagesBySelf = this.nonEmptyMessages.filter( (message) => message.isSelfAuthored && message.editable ); if (editableMessagesBySelf.length > 0) { @@ -254,41 +273,25 @@ export class Thread { return createLocalId(this.model, this.id); } - get needactionMessages() { - return this.messages.filter(({ isNeedaction }) => isNeedaction); - } - /** @returns {import("@mail/core/message_model").Message | undefined} */ - get mostRecentMsg() { - if (this.messages.length === 0) { - return undefined; - } - return this._store.messages[Math.max(...this.nonEmptyMessages.map((m) => m.id))]; + get newestMessage() { + return [...this.messages].reverse().find((msg) => !msg.isEmpty); } - get mostRecentNeedactionMsg() { - const mostRecentNeedactionMsgId = this.mostRecentNeedactionMsgId; - if (!mostRecentNeedactionMsgId) { - return undefined; - } - return this._store.messages[mostRecentNeedactionMsgId]; + get newestNeedactionMessage() { + return this.needactionMessages[this.needactionMessages.length - 1]; } - get mostRecentNeedactionMsgId() { - const needactionMessages = this.needactionMessages; - return needactionMessages.length > 0 - ? Math.max(...needactionMessages.map(({ id }) => id)) - : undefined; + get oldestNeedactionMessage() { + return this.needactionMessages[0]; } - get mostRecentNonTransientMessage() { - if (this.messages.length === 0) { - return undefined; - } - const oldestNonTransientMessage = [...this.messages] - .reverse() - .find((message) => Number.isInteger(message.id)); - return oldestNonTransientMessage; + get newestPersistentMessage() { + return [...this.messages].reverse().find((msg) => Number.isInteger(msg.id)); + } + + get oldestPersistentMessage() { + return this.messages.find((msg) => Number.isInteger(msg.id)); } get hasSelfAsMember() { @@ -318,21 +321,11 @@ export class Thread { return orderedOnlineMembers.sort((m1, m2) => (m1.persona.name < m2.persona.name ? -1 : 1)); } - get oldestNonTransientMessage() { - if (this.messages.length === 0) { - return undefined; - } - const oldestNonTransientMessage = this.messages.find((message) => - Number.isInteger(message.id) - ); - return oldestNonTransientMessage; - } - get nonEmptyMessages() { return this.messages.filter((message) => !message.isEmpty); } - get nonTransientMessages() { + get persistentMessages() { return this.messages.filter((message) => !message.isTransient); } @@ -350,7 +343,7 @@ export class Thread { return false; } const lastMessageSeenByAllId = Math.min(...otherLastSeenMessageIds); - const orderedSelfSeenMessages = this.nonTransientMessages.filter((message) => { + const orderedSelfSeenMessages = this.persistentMessages.filter((message) => { return message.author === this._store.self && message.id <= lastMessageSeenByAllId; }); if (!orderedSelfSeenMessages || orderedSelfSeenMessages.length === 0) { diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index 96f407e7795..c3552151fc6 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -13,11 +13,12 @@ import { Composer } from "../composer/composer_model"; import { prettifyMessageContent } from "../utils/format"; import { registry } from "@web/core/registry"; import { url } from "@web/core/utils/urls"; +import { memoize } from "@web/core/utils/functions"; import { DEFAULT_AVATAR } from "@mail/core/persona_service"; import { loadEmoji } from "@mail/emoji_picker/emoji_picker"; import { browser } from "@web/core/browser/browser"; -const FETCH_MSG_LIMIT = 30; +const FETCH_LIMIT = 30; export class ThreadService { nextId = 0; @@ -104,18 +105,16 @@ export class ThreadService { async markAsRead(thread) { if (!thread.isLoaded && thread.status === "loading") { await thread.isLoadedDeferred; + await new Promise(setTimeout); } - const mostRecentNonTransientMessage = thread.mostRecentNonTransientMessage; - if ( - this.isUnread(thread) && - thread.allowSetLastSeenMessage && - mostRecentNonTransientMessage - ) { + const newestPersistentMessage = thread.newestPersistentMessage; + thread.seen_message_id = thread.newestPersistentMessage?.id ?? false; + if (this.isUnread(thread) && thread.allowSetLastSeenMessage && newestPersistentMessage) { this.rpc("/mail/channel/set_last_seen_message", { channel_id: thread.id, - last_message_id: mostRecentNonTransientMessage.id, + last_message_id: newestPersistentMessage.id, }).then(() => { - this.update(thread, { serverLastSeenMsgBySelf: mostRecentNonTransientMessage.id }); + this.updateSeen(thread, newestPersistentMessage.id); }); } if (thread.hasNeedactionMessages) { @@ -123,13 +122,38 @@ export class ThreadService { } } - markAllMessagesAsRead(thread) { - return this.orm.silent.call("mail.message", "mark_all_as_read", [ + updateSeen(thread, lastSeenId = thread.newestPersistentMessage?.id) { + const lastReadIndex = thread.messages.findIndex((message) => message.id === lastSeenId); + let newNeedactionCounter = 0; + let newUnreadCounter = 0; + for (const message of thread.messages.slice(lastReadIndex + 1)) { + if (message.isNeedaction) { + newNeedactionCounter++; + } + if (Number.isInteger(message.id)) { + newUnreadCounter++; + } + } + this.update(thread, { + seen_message_id: lastSeenId, + message_needaction_counter: newNeedactionCounter, + message_unread_counter: newUnreadCounter, + }); + } + + async markAllMessagesAsRead(thread) { + await this.orm.silent.call("mail.message", "mark_all_as_read", [ [ ["model", "=", thread.model], ["res_id", "=", thread.id], ], ]); + Object.assign(thread, { + needactionMessages: [], + message_unread_counter: 0, + message_needaction_counter: 0, + seen_message_id: thread.newestPersistentMessage?.id, + }); } /** @@ -141,9 +165,9 @@ export class ThreadService { /** * @param {Thread} thread - * @param {{min: Number, max: Number}} + * @param {{after: Number, before: Number}} */ - async fetchMessages(thread, { min, max } = {}) { + async fetchMessages(thread, { after, before } = {}) { thread.status = "loading"; if (thread.type === "chatter" && !thread.id) { return []; @@ -174,11 +198,12 @@ export class ThreadService { return {}; })(); try { + // ordered messages received: newest to oldest const rawMessages = await this.rpc(route, { ...params, - limit: FETCH_MSG_LIMIT, - max_id: max, - min_id: min, + limit: FETCH_LIMIT, + after, + before, }); const messages = rawMessages.reverse().map((data) => { if (data.parentMessage) { @@ -205,17 +230,60 @@ export class ThreadService { * @param {Thread} thread */ async fetchNewMessages(thread) { - if (thread.status === "loading" || (thread.isLoaded && thread.model === "mail.channel")) { + if ( + thread.status === "loading" || + (thread.isLoaded && ["mail.channel", "mail.box"].includes(thread.model)) + ) { return; } - const min = thread.isLoaded ? thread.mostRecentNonTransientMessage?.id : undefined; + const after = thread.isLoaded ? thread.newestPersistentMessage?.id : undefined; try { - const fetchedMsgs = await this.fetchMessages(thread, { min }); + const fetched = await this.fetchMessages(thread, { after }); + // feed messages + // could have received a new message as notification during fetch + // filter out already fetched (e.g. received as notification in the meantime) + const startIndex = + after === undefined + ? 0 + : thread.messages.findIndex((message) => message.id === after); + const alreadyKnownMessages = new Set(thread.messages.map((m) => m.id)); + const filtered = fetched.filter( + (message) => + !alreadyKnownMessages.has(message.id) && + (thread.persistentMessages.length === 0 || + message.id < thread.oldestPersistentMessage.id || + message.id > thread.newestPersistentMessage.id) + ); + thread.messages.splice(startIndex, 0, ...filtered); + // feed needactions + // same for needaction messages, special case for mailbox: + // kinda "fetch new/more" with needactions on many origin threads at once + if (thread === this.store.discuss.inbox) { + for (const message of fetched) { + const thread = message.originThread; + if (!thread.needactionMessages.includes(message)) { + thread.needactionMessages.unshift(message); + } + } + } else { + const startNeedactionIndex = + after === undefined + ? 0 + : thread.messages.findIndex((message) => message.id === after); + const filteredNeedaction = fetched.filter( + (message) => + message.isNeedaction && + (thread.needactionMessages.length === 0 || + message.id < thread.oldestNeedactionMessage.id || + message.id > thread.newestNeedactionMessage.id) + ); + thread.needactionMessages.splice(startNeedactionIndex, 0, ...filteredNeedaction); + } Object.assign(thread, { loadMore: - min === undefined && fetchedMsgs.length === FETCH_MSG_LIMIT + after === undefined && fetched.length === FETCH_LIMIT ? true - : min === undefined && fetchedMsgs.length !== FETCH_MSG_LIMIT + : after === undefined && fetched.length !== FETCH_LIMIT ? false : thread.loadMore, }); @@ -224,6 +292,39 @@ export class ThreadService { } } + // This function is like fetchNewMessages but just for a single message at most on all pinned threads + fetchPreviews = memoize(async () => { + const ids = []; + for (const thread of Object.values(this.store.threads)) { + if (["channel", "group", "chat"].includes(thread.type)) { + ids.push(thread.id); + } + } + if (ids.length) { + const previews = await this.orm.call("mail.channel", "channel_fetch_preview", [ids]); + for (const preview of previews) { + const thread = this.store.threads[createLocalId("mail.channel", preview.id)]; + const data = Object.assign(preview.last_message, { + body: markup(preview.last_message.body), + }); + const message = this.messageService.insert({ + ...data, + res_id: thread.id, + model: thread.model, + }); + if (!thread.isLoaded) { + thread.messages.push(message); + if (message.isNeedaction && !thread.needactionMessages.includes(message)) { + thread.needactionMessages.push(message); + } + } + thread.isLoaded = true; + thread.loadMore = true; + thread.status = "ready"; + } + } + }); + /** * @param {Thread} thread */ @@ -232,10 +333,11 @@ export class ThreadService { return; } try { - const fetchedMsgs = await this.fetchMessages(thread, { - max: thread.oldestNonTransientMessage?.id, + const fetched = await this.fetchMessages(thread, { + before: thread.oldestPersistentMessage?.id, }); - if (fetchedMsgs.length < FETCH_MSG_LIMIT) { + thread.messages.unshift(...fetched); + if (fetched.length < FETCH_LIMIT) { thread.loadMore = false; } } catch { @@ -484,23 +586,21 @@ export class ThreadService { "is_pinned", "message_needaction_counter", "name", + "seen_message_id", "state", "group_based_subscription", "last_interest_dt", "defaultDisplayMode", ]); + if (serverData.channel && "message_unread_counter" in serverData.channel) { + thread.message_unread_counter = serverData.channel.message_unread_counter; + } thread.lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessageId; if (thread.model === "mail.channel" && serverData.channel) { thread.channel = assignDefined(thread.channel ?? {}, serverData.channel); } thread.memberCount = serverData.channel?.memberCount ?? thread.memberCount; - if (serverData.channel && "serverMessageUnreadCounter" in serverData.channel) { - thread.serverMessageUnreadCounter = serverData.channel.serverMessageUnreadCounter; - } - if ("seen_message_id" in serverData) { - thread.serverLastSeenMsgBySelf = serverData.seen_message_id; - } if ("rtc_inviting_session" in serverData) { this.env.bus.trigger("THREAD-SERVICE:UPDATE_RTC_SESSIONS", { thread, @@ -744,6 +844,8 @@ export class ThreadService { model: thread.model, temporary_id: tmpId, }); + thread.messages.push(tmpMsg); + thread.seen_message_id = tmpMsg.id; } const data = await this.rpc("/mail/message/post", params); if (data.parentMessage) { @@ -757,6 +859,9 @@ export class ThreadService { const message = this.messageService.insert( Object.assign(data, { body: markup(data.body) }) ); + if (!thread.messages.some(({ id }) => id === message.id)) { + thread.messages.push(message); + } if (!message.isEmpty && this.store.hasLinkPreviewFeature) { this.rpc("/mail/link_preview", { message_id: data.id }, { silent: true }); } @@ -771,7 +876,7 @@ export class ThreadService { * @param {Thread} thread */ isUnread(thread) { - return this.localMessageUnreadCounter(thread) > 0; + return thread.message_unread_counter > 0; } /** @@ -800,63 +905,18 @@ export class ThreadService { return thread.counter; } if (thread.type === "chat" || thread.type === "group") { - return this.localMessageUnreadCounter(thread); + return thread.message_unread_counter || thread.message_needaction_counter; } return thread.message_needaction_counter; } - /** - * @param {Thread} thread - */ - localMessageUnreadCounter(thread) { - let baseCounter = thread.serverMessageUnreadCounter; - let countFromId = thread.lastServerMessageId ? thread.lastServerMessageId : 0; - const lastSeenMessageId = this.lastSeenBySelfMessageId(thread); - const firstMessage = thread.messages[0]; - if (firstMessage && (lastSeenMessageId === false || lastSeenMessageId >= firstMessage.id)) { - baseCounter = 0; - countFromId = lastSeenMessageId || 0; - } - return thread.messages.reduce((total, message) => { - if (message.id <= countFromId || message.temporary_id) { - return total; - } - return total + 1; - }, baseCounter); - } - - /** - * @param {Thread} thread - */ - lastSeenBySelfMessageId(thread) { - if (thread.model !== "mail.channel") { - return null; - } - const firstMessage = thread.messages[0]; - if (firstMessage && thread.serverLastSeenMsgBySelf < firstMessage.id) { - return thread.serverLastSeenMsgBySelf; - } - let lastSeenMessageId = thread.serverLastSeenMsgBySelf; - for (const message of thread.messages) { - if (message.id <= thread.serverLastSeenMsgBySelf) { - continue; - } - if (message.temporary_id || message.isTransient) { - lastSeenMessageId = message.id; - continue; - } - return lastSeenMessageId; - } - return lastSeenMessageId; - } - getDiscussCategoryCounter(categoryId) { return this.store.discuss[categoryId].threads.reduce((acc, threadLocalId) => { const channel = this.store.threads[threadLocalId]; if (categoryId === "channels") { return channel.message_needaction_counter > 0 ? acc + 1 : acc; } else { - return this.localMessageUnreadCounter(channel) > 0 ? acc + 1 : acc; + return this.isUnread(channel) > 0 ? acc + 1 : acc; } }, 0); } diff --git a/addons/mail/static/src/core_ui/message.js b/addons/mail/static/src/core_ui/message.js index 798a74dc08c..25ba56082b5 100644 --- a/addons/mail/static/src/core_ui/message.js +++ b/addons/mail/static/src/core_ui/message.js @@ -336,7 +336,7 @@ export class Message extends Component { onClickMarkAsUnread() { const previousMessageId = this.message.originThread.getPreviousMessage(this.message)?.id ?? false; - if (this.threadService.lastSeenBySelfMessageId(this.props.thread) === previousMessageId) { + if (this.props.thread.seen_message_id === previousMessageId) { return; } return this.rpc("/mail/channel/set_last_seen_message", { diff --git a/addons/mail/static/src/core_ui/message.xml b/addons/mail/static/src/core_ui/message.xml index 5709dc9ca98..53c6dc6e55d 100644 --- a/addons/mail/static/src/core_ui/message.xml +++ b/addons/mail/static/src/core_ui/message.xml @@ -145,7 +145,7 @@ - + diff --git a/addons/mail/static/src/core_ui/thread.js b/addons/mail/static/src/core_ui/thread.js index f048e84def0..30bf33e6199 100644 --- a/addons/mail/static/src/core_ui/thread.js +++ b/addons/mail/static/src/core_ui/thread.js @@ -62,7 +62,7 @@ export class Thread extends Component { this.threadService.fetchMoreMessages(this.props.thread); } }); - this.oldestNonTransientMessageId = null; + this.oldestPersistentMessageId = null; this.scrollPosition = useScrollPosition( "messages", this.props.thread.scrollPosition, @@ -73,8 +73,8 @@ export class Thread extends Component { onWillPatch: () => { return { hasMoreMsgsAbove: - this.props.thread.oldestNonTransientMessage?.id !== - this.oldestNonTransientMessage && this.props.order === "asc", + this.props.thread.oldestPersistentMessage?.id !== + this.oldestPersistentMessage && this.props.order === "asc", }; }, onPatched: ({ hasMoreMsgsAbove, scrollTop, scrollHeight }) => { @@ -82,13 +82,12 @@ export class Thread extends Component { if (hasMoreMsgsAbove) { el.scrollTop = scrollTop + el.scrollHeight - scrollHeight; } - this.oldestNonTransientMessage = - this.props.thread.oldestNonTransientMessage?.id; + this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id; }, }); } onMounted(() => { - this.oldestNonTransientMessage = this.props.thread.oldestNonTransientMessage?.id; + this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id; if (!this.env.inChatter || this.props.hasMessageScrollAdjustInChatter) { this.scrollPosition.restore(); } diff --git a/addons/mail/static/src/core_ui/thread.xml b/addons/mail/static/src/core_ui/thread.xml index e9f8ef96586..e6594ee992a 100644 --- a/addons/mail/static/src/core_ui/thread.xml +++ b/addons/mail/static/src/core_ui/thread.xml @@ -20,7 +20,7 @@ - +

New messages
diff --git a/addons/mail/static/src/web/chat_window/chat_window.xml b/addons/mail/static/src/web/chat_window/chat_window.xml index 3c1f7fd2ff7..1565fe25acc 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.xml +++ b/addons/mail/static/src/web/chat_window/chat_window.xml @@ -21,8 +21,8 @@ 'ms-3 me-1 my-0': !thread, }" /> -
- () +
+ ()
diff --git a/addons/mail/static/src/web/messaging_menu/messaging_menu.js b/addons/mail/static/src/web/messaging_menu/messaging_menu.js index 0b8c781d73c..e4bc9c47275 100644 --- a/addons/mail/static/src/web/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/web/messaging_menu/messaging_menu.js @@ -42,13 +42,13 @@ export class MessagingMenu extends Component { } beforeOpen() { - this.messaging.fetchPreviews(); + this.threadService.fetchPreviews(); if ( !this.store.discuss.inbox.isLoaded && this.store.discuss.inbox.status !== "loading" && this.store.discuss.inbox.counter !== this.store.discuss.inbox.messages.length ) { - this.threadService.fetchMessages(this.store.discuss.inbox); + this.threadService.fetchNewMessages(this.store.discuss.inbox); } } @@ -117,24 +117,22 @@ export class MessagingMenu extends Component { if (a.correspondent === this.store.odoobot) { return 1; } - if (!a.mostRecentNonTransientMessage?.datetime) { + if (!a.newestPersistentMessage?.datetime) { return -1; } - if (!b.mostRecentNonTransientMessage?.datetime) { + if (!b.newestPersistentMessage?.datetime) { return 1; } - return ( - b.mostRecentNonTransientMessage.datetime - a.mostRecentNonTransientMessage.datetime - ); + return b.newestPersistentMessage.datetime - a.newestPersistentMessage.datetime; }); const previews = []; for (const thread of threads) { - const { mostRecentMsg, mostRecentNeedactionMsg } = thread; + const { newestMessage, newestNeedactionMessage } = thread; if (thread.is_pinned) { - const message = mostRecentMsg; + const message = newestMessage; previews.push({ id: `preview-${thread.localId}`, - count: this.threadService.localMessageUnreadCounter(thread), + count: thread.message_unread_counter, imgUrl: thread.imgUrl, hasMarkAsReadButton: this.threadService.isUnread(thread), message, @@ -142,8 +140,8 @@ export class MessagingMenu extends Component { isNeedaction: false, }); } - if (mostRecentNeedactionMsg) { - const message = mostRecentNeedactionMsg; + if (newestNeedactionMessage) { + const message = newestNeedactionMessage; previews.push({ id: `preview-needaction-${thread.localId}`, count: thread.needactionMessages.length, diff --git a/addons/mail/static/src/web/thread_service_patch.js b/addons/mail/static/src/web/thread_service_patch.js index e2665b4a7a5..f54cf2c8ec7 100644 --- a/addons/mail/static/src/web/thread_service_patch.js +++ b/addons/mail/static/src/web/thread_service_patch.js @@ -116,7 +116,8 @@ patch(ThreadService.prototype, "mail/web", { res_id: thread.id, model: thread.model, }; - this.messageService.insert(tmpData); + const message = this.messageService.insert(tmpData); + thread.messages.push(message); } return thread; }, diff --git a/addons/mail/static/tests/core/message_model_tests.js b/addons/mail/static/tests/core/message_model_tests.js index eb2c07bd09a..9d73f0bb5d4 100644 --- a/addons/mail/static/tests/core/message_model_tests.js +++ b/addons/mail/static/tests/core/message_model_tests.js @@ -7,8 +7,6 @@ QUnit.module("message model test", {}); QUnit.test("Message model properties", async (assert) => { const { env } = await start(); - /** @type {import("@mail/core/store_service").Store} */ - const store = env.services["mail.store"]; env.services["mail.thread"].insert({ id: 3, model: "res.partner", @@ -42,8 +40,6 @@ QUnit.test("Message model properties", async (assert) => { "2019-05-05 10:00:00" ); assert.strictEqual(message.id, 4000); - assert.ok(store.discuss.inbox.messages.find((m) => m.id === message.id)); - assert.ok(store.discuss.starred.messages.find((m) => m.id === message.id)); assert.ok(message.attachments); assert.strictEqual(message.attachments[0].name, "test.txt"); diff --git a/addons/mail/static/tests/discuss/discuss_tests.js b/addons/mail/static/tests/discuss/discuss_tests.js index 8d0f615b2e9..1aaffe08d15 100644 --- a/addons/mail/static/tests/discuss/discuss_tests.js +++ b/addons/mail/static/tests/discuss/discuss_tests.js @@ -1557,7 +1557,6 @@ QUnit.test("new messages separator [REQUIRE FOCUS]", async (assert) => { // this test requires several messages so that the last message is not // visible. This is necessary in order to display 'new messages' and not // remove from DOM right away from seeing last message. - // AKU TODO: thread specific test const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" }); const userId = pyEnv["res.users"].create({ diff --git a/addons/mail/static/tests/discuss/sidebar_tests.js b/addons/mail/static/tests/discuss/sidebar_tests.js index 1cd0d8d063d..b8615ed53af 100644 --- a/addons/mail/static/tests/discuss/sidebar_tests.js +++ b/addons/mail/static/tests/discuss/sidebar_tests.js @@ -1155,7 +1155,7 @@ QUnit.test("Group unread counter up to date after mention is marked as seen", as author_id: partnerId, model: "mail.channel", res_id: channelId, - body: `@Mitchell Admin`, + body: "@Mitchell Admin", needaction: true, }); pyEnv["mail.notification"].create([ diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js index 117617725ed..585f1e6d640 100644 --- a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js +++ b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js @@ -40,8 +40,8 @@ patch(MockServer.prototype, "mail/controllers/discuss", { return this._mockRouteMailAttachmentRemove(attachment_id); } if (route === "/mail/channel/messages") { - const { channel_id, min_id, max_id, limit } = args; - return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit); + const { channel_id, after, before, limit } = args; + return this._mockRouteMailChannelMessages(channel_id, before, after, limit); } if (route === "/mail/channel/notify_typing") { const id = args.channel_id; @@ -60,15 +60,15 @@ patch(MockServer.prototype, "mail/controllers/discuss", { return this._mockMailChannelLoadMoreMembers([channel_id], known_member_ids); } if (route === "/mail/history/messages") { - const { min_id, max_id, limit } = args; - return this._mockRouteMailMessageHistory(min_id, max_id, limit); + const { after, before, limit } = args; + return this._mockRouteMailMessageHistory(after, before, limit); } if (route === "/mail/init_messaging") { return this._mockRouteMailInitMessaging(); } if (route === "/mail/inbox/messages") { - const { min_id, max_id, limit } = args; - return this._mockRouteMailMessageInbox(min_id, max_id, limit); + const { after, before, limit } = args; + return this._mockRouteMailMessageInbox(after, before, limit); } if (route === "/mail/link_preview") { return this._mockRouteMailLinkPreview(args.message_id); @@ -142,8 +142,8 @@ patch(MockServer.prototype, "mail/controllers/discuss", { return; } if (route === "/mail/starred/messages") { - const { min_id, max_id, limit } = args; - return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit); + const { after, before, limit } = args; + return this._mockRouteMailMessageStarredMessages(after, before, limit); } if (route === "/mail/thread/data") { return this._mockRouteMailThreadData( @@ -153,12 +153,12 @@ patch(MockServer.prototype, "mail/controllers/discuss", { ); } if (route === "/mail/thread/messages") { - const { min_id, max_id, limit, thread_model, thread_id } = args; + const { after, before, limit, thread_model, thread_id } = args; return this._mockRouteMailThreadFetchMessages( thread_model, thread_id, - max_id, - min_id, + before, + after, limit ); } @@ -191,17 +191,17 @@ patch(MockServer.prototype, "mail/controllers/discuss", { * @private * @param {integer} channel_id * @param {integer} limit - * @param {integer} max_id - * @param {integer} min_id + * @param {integer} before + * @param {integer} after * @returns {Object} list of messages */ - async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) { + async _mockRouteMailChannelMessages(channel_id, before = false, after = false, limit = 30) { const domain = [ ["res_id", "=", channel_id], ["model", "=", "mail.channel"], ["message_type", "!=", "user_notification"], ]; - const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); + const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit); this._mockMailMessageSetMessageDone(messages.map((message) => message.id)); return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, @@ -297,9 +297,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", { * @private * @returns {Object} */ - _mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) { + _mockRouteMailMessageHistory(after = false, before = false, limit = 30) { const domain = [["needaction", "=", false]]; - const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); + const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit); const messagesWithNotification = messages.filter((message) => { const notifs = this.pyEnv["mail.notification"].searchRead([ ["mail_message_id", "=", message.id], @@ -319,9 +319,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", { * @private * @returns {Object} */ - _mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) { + _mockRouteMailMessageInbox(after = false, before = false, limit = 30) { const domain = [["needaction", "=", true]]; - const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); + const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit); return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** @@ -330,9 +330,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", { * @private * @returns {Object} */ - _mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) { + _mockRouteMailMessageStarredMessages(after = false, before = false, limit = 30) { const domain = [["starred_partner_ids", "in", [this.currentPartnerId]]]; - const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); + const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit); return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** @@ -514,16 +514,16 @@ patch(MockServer.prototype, "mail/controllers/discuss", { * @private * @param {string} res_model * @param {integer} res_id - * @param {integer} max_id - * @param {integer} min_id + * @param {integer} before + * @param {integer} after * @param {integer} limit * @returns {Object[]} list of messages */ async _mockRouteMailThreadFetchMessages( res_model, res_id, - max_id = false, - min_id = false, + before = false, + after = false, limit = 30 ) { const domain = [ @@ -531,7 +531,7 @@ patch(MockServer.prototype, "mail/controllers/discuss", { ["model", "=", res_model], ["message_type", "!=", "user_notification"], ]; - const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); + const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit); this._mockMailMessageSetMessageDone(messages.map((message) => message.id)); return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, 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 78a768ca24e..48147388210 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 @@ -552,7 +552,7 @@ patch(MockServer.prototype, "mail/models/mail_channel", { }); Object.assign(channelData, { custom_channel_name: memberOfCurrentUser.custom_channel_name, - serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter, + message_unread_counter: memberOfCurrentUser.message_unread_counter, }); if (memberOfCurrentUser.rtc_inviting_session_id) { res["rtc_inviting_session"] = { 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 b2740820bd9..ee80556c3a4 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 @@ -134,17 +134,17 @@ patch(MockServer.prototype, "mail/models/mail_message", { * * @private * @param {Array[]} domain - * @param {integer} [max_id] - * @param {integer} [min_id] + * @param {integer} [before] + * @param {integer} [after] * @param {integer} [limit=30] * @returns {Object[]} */ - _mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) { - if (max_id) { - domain.push(["id", "<", max_id]); + _mockMailMessage_MessageFetch(domain, before, after, limit = 30) { + if (before) { + domain.push(["id", "<", before]); } - if (min_id) { - domain.push(["id", ">", min_id]); + if (after) { + domain.push(["id", ">", after]); } const messages = this.getRecords("mail.message", domain); // sorted from highest ID to lowest ID (i.e. from youngest to oldest) diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index 428995eb2c0..d1585713a9f 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -174,6 +174,11 @@ function getOpenDiscuss(webClient, { context = {}, params = {}, ...props } = {}) ); if (waitUntilMessagesLoaded) { const messagesLoadedPromise = makeDeferred(); + const store = webClient.env.services["mail.store"]; + const thread = store.threads[store.discuss.threadLocalId]; + if (thread.isLoaded) { + messagesLoadedPromise.resolve(); + } let loadMessageRoute = `/mail/${threadId}/messages`; if (Number.isInteger(threadId)) { loadMessageRoute = "/mail/channel/messages"; diff --git a/addons/mail/static/tests/message/message_tests.js b/addons/mail/static/tests/message/message_tests.js index b786086a3c4..c1173f81a7b 100644 --- a/addons/mail/static/tests/message/message_tests.js +++ b/addons/mail/static/tests/message/message_tests.js @@ -1421,12 +1421,22 @@ QUnit.test("Can reply to chatter messages from history", async (assert) => { 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({ + const channelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + name: "General", + }); + const messageId = pyEnv["mail.message"].create({ model: "mail.channel", res_id: channelId, body: "Hello World!", }); + const [memberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", channelId], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([memberId], { + seen_message_id: messageId, + }); const { openDiscuss } = await start(); await openDiscuss(channelId); await click("[title='Expand']"); diff --git a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js index 9702afcdaea..64a43d08303 100644 --- a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js +++ b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js @@ -837,7 +837,12 @@ QUnit.test( async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Partner1" }); - const channelId = pyEnv["mail.channel"].create({ name: "Test" }); + const channelId = pyEnv["mail.channel"].create({ + name: "Test", + channel_member_ids: [ + [0, 0, { message_unread_counter: 2, partner_id: pyEnv.currentPartnerId }], + ], + }); const messageId = pyEnv["mail.message"].create({ author_id: partnerId, body: "Message with needaction", diff --git a/addons/mail/static/tests/thread/thread_tests.js b/addons/mail/static/tests/thread/thread_tests.js index b2384d91450..f74d61f1988 100644 --- a/addons/mail/static/tests/thread/thread_tests.js +++ b/addons/mail/static/tests/thread/thread_tests.js @@ -880,6 +880,7 @@ QUnit.test( async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["mail.channel"].create({ name: "General" }); + const partnerId = pyEnv["res.partner"].create({ name: "Demo" }); const { env, openDiscuss } = await start({ mockRPC(route, args) { if (args.model === "mail.message" && args.method === "mark_all_as_read") { @@ -896,6 +897,7 @@ QUnit.test( await triggerEvents($(".o-mail-Composer-input")[0], null, ["blur", "focusout"]); await click("button:contains(Inbox)"); const messageId = pyEnv["mail.message"].create({ + author_id: partnerId, body: "@Mitchel Admin", needaction: true, model: "mail.channel", diff --git a/addons/test_discuss_full/tests/test_performance.py b/addons/test_discuss_full/tests/test_performance.py index f40afd30b8b..3c276ab8d96 100644 --- a/addons/test_discuss_full/tests/test_performance.py +++ b/addons/test_discuss_full/tests/test_performance.py @@ -142,7 +142,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_general.id, 'memberCount': len(self.group_user.users | self.user_root), - 'serverMessageUnreadCounter': 5, + 'message_unread_counter': 5, }, 'create_uid': self.user_root.id, 'defaultDisplayMode': False, @@ -191,7 +191,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_channel_public_1.id, 'memberCount': 5, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -240,7 +240,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_channel_public_2.id, 'memberCount': 5, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -289,7 +289,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_channel_group_1.id, 'memberCount': 5, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -338,7 +338,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_channel_group_2.id, 'memberCount': 5, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -409,7 +409,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_group_1.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -494,7 +494,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_chat_1.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -579,7 +579,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_chat_2.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -664,7 +664,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_chat_3.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -749,7 +749,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_chat_4.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -828,7 +828,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_livechat_1.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.user.id, 'defaultDisplayMode': False, @@ -907,7 +907,7 @@ class TestDiscussFullPerformance(TransactionCase): 'custom_channel_name': False, 'id': self.channel_livechat_2.id, 'memberCount': 2, - 'serverMessageUnreadCounter': 0, + 'message_unread_counter': 0, }, 'create_uid': self.env.ref('base.public_user').id, 'defaultDisplayMode': False,