/* @odoo-module */ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; patch(MockServer.prototype, { async _performRPC(route, args) { if (args.model === "mail.message" && args.method === "mark_all_as_read") { const domain = args.args[0] || args.kwargs.domain; return this._mockMailMessageMarkAllAsRead(domain); } if (args.model === "mail.message" && args.method === "message_format") { const ids = args.args[0]; return this._mockMailMessageMessageFormat(ids); } if (args.model === "mail.message" && args.method === "set_message_done") { const ids = args.args[0]; return this._mockMailMessageSetMessageDone(ids); } if (args.model === "mail.message" && args.method === "toggle_message_starred") { const ids = args.args[0]; return this._mockMailMessageToggleMessageStarred(ids); } if (args.model === "mail.message" && args.method === "unstar_all") { return this._mockMailMessageUnstarAll(); } return super._performRPC(route, args); }, /** * Simulates `_bus_notification_target` on `mail.message`. * * @param {number} messageId */ _mockMailMessage__busNotificationTarget(messageId) { const [message] = this.pyEnv["mail.message"].searchRead([["id", "=", messageId]]); if (message.model === "discuss.channel") { return this.pyEnv["discuss.channel"].searchRead([["id", "=", message.res_id]])[0]; } if (this.pyEnv.currentUser?._is_public()) { this._mockMailGuest__getGuestFromContext(); } return this.pyEnv.currentPartner; }, /** * Simulates `_message_reaction` on `mail.message`. */ _mockMailMessage_messageReaction(messageId, content, action) { const [reaction] = this.pyEnv["mail.message.reaction"].searchRead([ ["content", "=", content], ["message_id", "=", messageId], ["partner_id", "=", this.pyEnv.currentPartnerId], ]); if (action === "add" && !reaction) { this.pyEnv["mail.message.reaction"].create({ content, message_id: messageId, partner_id: this.pyEnv.currentPartnerId, }); } if (action === "remove" && reaction) { this.pyEnv["mail.message.reaction"].unlink(reaction.id); } const reactions = this.pyEnv["mail.message.reaction"].search([ ["message_id", "=", messageId], ["content", "=", content], ]); const guest = this._mockMailGuest__getGuestFromContext(); const result = { id: messageId, reactions: [ [ reactions.length > 0 ? "ADD" : "DELETE", { content, count: reactions.length, message: { id: messageId }, personas: [ [ action === "add" ? "ADD" : "DELETE", { id: guest ? guest.id : this.pyEnv.currentPartnerId, name: guest ? guest.name : this.pyEnv.currentPartner.name, type: guest ? "guest" : "partner", }, ], ], }, ], ], }; this.pyEnv["bus.bus"]._sendone( this._mockMailMessage__busNotificationTarget(messageId), "mail.record/insert", { Message: result } ); }, /** * Simulates `mark_all_as_read` on `mail.message`. * * @private * @param {Array[]} [domain] * @returns {integer[]} */ _mockMailMessageMarkAllAsRead(domain) { const notifDomain = [ ["res_partner_id", "=", this.pyEnv.currentPartnerId], ["is_read", "=", false], ]; if (domain) { const messages = this.getRecords("mail.message", domain); const ids = messages.map((messages) => messages.id); this._mockMailMessageSetMessageDone(ids); return ids; } const notifications = this.getRecords("mail.notification", notifDomain); this.pyEnv["mail.notification"].write( notifications.map((notification) => notification.id), { is_read: true } ); const messageIds = []; for (const notification of notifications) { if (!messageIds.includes(notification.mail_message_id)) { messageIds.push(notification.mail_message_id); } } const messages = this.getRecords("mail.message", [["id", "in", messageIds]]); // simulate compute that should be done based on notifications for (const message of messages) { this.pyEnv["mail.message"].write([message.id], { needaction: false, needaction_partner_ids: message.needaction_partner_ids.filter( (partnerId) => partnerId !== this.pyEnv.currentPartnerId ), }); } this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/mark_as_read", { message_ids: messageIds, needaction_inbox_counter: this._mockResPartner_GetNeedactionCount( this.pyEnv.currentPartnerId ), }); return messageIds; }, /** * Simulates `_message_fetch` on `mail.message`. * * @private * @param {Array[]} domain * @param {integer} [before] * @param {integer} [after] * @param {integer} [limit=30] * @returns {Object[]} */ _mockMailMessage_MessageFetch(domain, search_term, before, after, around, limit = 30) { const res = {}; if (search_term) { search_term = search_term.replace(" ", "%"); domain.push(["body", "ilike", search_term]); res.count = this.pyEnv["mail.message"].searchCount(domain); } if (around) { const messagesBefore = this.getRecords( "mail.message", domain.concat([["id", "<=", around]]) ).sort((m1, m2) => m2.id - m1.id); messagesBefore.length = Math.min(messagesBefore.length, limit / 2); const messagesAfter = this.getRecords( "mail.message", domain.concat([["id", ">", around]]) ).sort((m1, m2) => m1.id - m2.id); messagesAfter.length = Math.min(messagesAfter.length, limit / 2); return { ...res, messages: messagesAfter.concat(messagesBefore.reverse()) }; } if (before) { domain.push(["id", "<", before]); } 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) messages.sort(function (m1, m2) { return m1.id < m2.id ? 1 : -1; }); // pick at most 'limit' messages messages.length = Math.min(messages.length, limit); res.messages = messages; return res; }, /** * Simulates `message_format` on `mail.message`. * * @private * @returns {integer[]} ids * @returns {Object[]} */ _mockMailMessageMessageFormat(ids) { const messages = this.getRecords("mail.message", [["id", "in", ids]]); // sorted from highest ID to lowest ID (i.e. from most to least recent) messages.sort(function (m1, m2) { return m1.id < m2.id ? 1 : -1; }); return messages.map((message) => { const thread = message.model && this.getRecords(message.model, [["id", "=", message.res_id]])[0]; let author; if (message.author_id) { const [partner] = this.getRecords("res.partner", [["id", "=", message.author_id]], { active_test: false, }); const [user] = this.getRecords("res.users", [ ["partner_id", "=", message.author_id], ]); author = { id: partner.id, is_company: partner.is_company, name: partner.name, type: "partner", }; if (user) { author["user"] = { id: user.id, isInternalUser: !user.share }; } } else { author = false; } const attachments = this.getRecords("ir.attachment", [ ["id", "in", message.attachment_ids], ]); const formattedAttachments = this._mockIrAttachment_attachmentFormat( attachments.map((attachment) => attachment.id) ).sort((a1, a2) => (a1.id < a2.id ? -1 : 1)); // sort attachments from oldest to most recent const allNotifications = this.getRecords("mail.notification", [ ["mail_message_id", "=", message.id], ]); const historyPartnerIds = allNotifications .filter((notification) => notification.is_read) .map((notification) => notification.res_partner_id); const needactionPartnerIds = allNotifications .filter((notification) => !notification.is_read) .map((notification) => notification.res_partner_id); let notifications = this._mockMailNotification_FilteredForWebClient( allNotifications.map((notification) => notification.id) ); notifications = this._mockMailNotification_NotificationFormat( notifications.map((notification) => notification.id) ); const trackingValueIds = this.getRecords("mail.tracking.value", [ ["id", "in", message.tracking_value_ids], ]); const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds); const partners = this.getRecords("res.partner", [["id", "in", message.partner_ids]]); const linkPreviews = this.getRecords("mail.link.preview", [ ["id", "in", message.link_preview_ids], ]); const linkPreviewsFormatted = linkPreviews.map((linkPreview) => this._mockMailLinkPreviewFormat(linkPreview) ); const reactionsPerContent = {}; for (const reactionId of message.reaction_ids ?? []) { const [reaction] = this.getRecords("mail.message.reaction", [ ["id", "=", reactionId], ]); if (reactionsPerContent[reaction.content]) { reactionsPerContent[reaction.content].push(reaction); } else { reactionsPerContent[reaction.content] = [reaction]; } } const reactionGroups = []; for (const content in reactionsPerContent) { const reactions = reactionsPerContent[content]; const guests = reactions .map( (reaction) => this.getRecords("mail.guest", [["id", "=", reaction.guest_id]])[0] ) .filter((guest) => !!guest); const partners = reactions .map( (reaction) => this.getRecords("res.partner", [["id", "=", reaction.partner_id]])[0] ) .filter((partner) => !!partner); reactionGroups.push({ content: content, count: reactionsPerContent[content].length, message: { id: message.id }, personas: guests .map((guest) => ({ id: guest.id, name: guest.name, type: "guests" })) .concat( partners.map((partner) => ({ id: partner.id, name: partner.name, type: "partner", })) ), }); } const response = Object.assign({}, message, { attachments: formattedAttachments, author, history_partner_ids: historyPartnerIds, default_subject: message.model && message.res_id && this.mockMailThread_MessageComputeSubject(message.model, [message.res_id]).get( message.res_id ), linkPreviews: linkPreviewsFormatted, reactions: reactionGroups, needaction_partner_ids: needactionPartnerIds, notifications, parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false, recipients: partners.map((p) => ({ id: p.id, name: p.name, type: "partner" })), record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name), trackingValues: formattedTrackingValues, pinned_at: message.pinned_at, }); delete response["author_id"]; if (message.subtype_id) { const subtype = this.getRecords("mail.message.subtype", [ ["id", "=", message.subtype_id], ])[0]; response.subtype_description = subtype.description; } let guestAuthor; if (message.author_guest_id) { const [guest] = this.pyEnv["mail.guest"].searchRead([ ["id", "=", message.author_guest_id], ]); guestAuthor = { id: guest.id, name: guest.name, type: "guest" }; } response.author = author || guestAuthor; response["module_icon"] = "/base/static/description/icon.png"; return response; }); }, /** * Simulate `_message_format_personalize` on `mail.message` for the current partner. * * @private * @returns {integer[]} ids * @returns {Object[]} */ _mockMailMessageFormatPersonalize(ids) { const messages = this._mockMailMessageMessageFormat(ids); messages.forEach((message) => { let user_follower_id = false; if (message.model && message.res_id) { const follower = this.getRecords("mail.followers", [ ["res_model", "=", message.model], ["res_id", "=", message.res_id], ["partner_id", "=", this.pyEnv.currentPartnerId], ]); if (follower.length !== 0) { user_follower_id = follower[0].id; } } message.user_follower_id = user_follower_id; }); return messages; }, /** * Simulates `_message_notification_format` on `mail.message`. * * @private * @returns {integer[]} ids * @returns {Object[]} */ _mockMailMessage_MessageNotificationFormat(ids) { const messages = this.getRecords("mail.message", [["id", "in", ids]]); return messages.map((message) => { let notifications = this.getRecords("mail.notification", [ ["mail_message_id", "=", message.id], ]); notifications = this._mockMailNotification_FilteredForWebClient( notifications.map((notification) => notification.id) ); notifications = this._mockMailNotification_NotificationFormat( notifications.map((notification) => notification.id) ); return { author: message.author_id ? { id: message.author_id, type: "partner" } : false, body: message.body, date: message.date, id: message.id, message_type: message.message_type, model: message.model, notifications: notifications, res_id: message.res_id, res_model_name: message.res_model_name, }; }); }, /** * Simulates `set_message_done` on `mail.message`, which turns provided * needaction message to non-needaction (i.e. they are marked as read from * from the Inbox mailbox). Also notify on the longpoll bus that the * messages have been marked as read, so that UI is updated. * * @private * @param {integer[]} ids */ _mockMailMessageSetMessageDone(ids) { const messages = this.getRecords("mail.message", [["id", "in", ids]]); const notifications = this.getRecords("mail.notification", [ ["res_partner_id", "=", this.pyEnv.currentPartnerId], ["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 } ); // simulate compute that should be done based on notifications for (const message of messages) { this.pyEnv["mail.message"].write([message.id], { needaction: false, needaction_partner_ids: message.needaction_partner_ids.filter( (partnerId) => partnerId !== this.pyEnv.currentPartnerId ), }); this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/mark_as_read", { message_ids: [message.id], needaction_inbox_counter: this._mockResPartner_GetNeedactionCount( this.pyEnv.currentPartnerId ), }); } }, /** * Simulates `toggle_message_starred` on `mail.message`. * * @private * @returns {integer[]} ids */ _mockMailMessageToggleMessageStarred(ids) { const messages = this.getRecords("mail.message", [["id", "in", ids]]); for (const message of messages) { const wasStared = message.starred_partner_ids.includes(this.pyEnv.currentPartnerId); this.pyEnv["mail.message"].write([message.id], { starred_partner_ids: [[wasStared ? 3 : 4, this.pyEnv.currentPartnerId]], }); this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/toggle_star", { message_ids: [message.id], starred: !wasStared, }); } }, /** * Simulates `unstar_all` on `mail.message`. * * @private */ _mockMailMessageUnstarAll() { const messages = this.getRecords("mail.message", [ ["starred_partner_ids", "in", this.pyEnv.currentPartnerId], ]); this.pyEnv["mail.message"].write( messages.map((message) => message.id), { starred_partner_ids: [[3, this.pyEnv.currentPartnerId]] } ); this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/toggle_star", { message_ids: messages.map((message) => message.id), starred: false, }); }, });