diff --git a/addons/im_livechat/static/src/chat_window/chat_window_patch.js b/addons/im_livechat/static/src/chat_window/chat_window_patch.js index 1b8cff8186a..3525bc9a276 100644 --- a/addons/im_livechat/static/src/chat_window/chat_window_patch.js +++ b/addons/im_livechat/static/src/chat_window/chat_window_patch.js @@ -8,8 +8,11 @@ patch(ChatWindow.prototype, { async close(options) { const thread = this.thread; await super.close(options); - if (thread?.type === "livechat" && thread.isLoaded && thread.messages.length === 0) { - this.threadService.unpin(thread); + if (thread?.type === "livechat") { + await thread?.isLoadedDeferred; + if (thread.messages.length === 0) { + this.threadService.unpin(thread); + } } }, }); diff --git a/addons/im_livechat/static/src/core/@types/models.d.ts b/addons/im_livechat/static/src/core/@types/models.d.ts index 3a831742636..f7bed8977b6 100644 --- a/addons/im_livechat/static/src/core/@types/models.d.ts +++ b/addons/im_livechat/static/src/core/@types/models.d.ts @@ -2,4 +2,7 @@ declare module "models" { export interface DiscussApp { livechat: DiscussAppCategory, } + export interface Thread { + operator: Persona, + } } diff --git a/addons/im_livechat/static/src/core/discuss_app_model_patch.js b/addons/im_livechat/static/src/core/discuss_app_model_patch.js index 0f7d73b5fff..e88b11ddaa5 100644 --- a/addons/im_livechat/static/src/core/discuss_app_model_patch.js +++ b/addons/im_livechat/static/src/core/discuss_app_model_patch.js @@ -9,7 +9,7 @@ import { patch } from "@web/core/utils/patch"; patch(DiscussApp, { new(data) { const res = super.new(data); - res.livechat = this.store.DiscussAppCategory.insert({ + res.livechat = { extraClass: "o-mail-DiscussSidebarCategory-livechat", id: "livechat", name: _t("Livechat"), @@ -17,7 +17,7 @@ patch(DiscussApp, { canView: false, canAdd: false, serverStateKey: "is_discuss_sidebar_category_livechat_open", - }); + }; return res; }, }); diff --git a/addons/im_livechat/static/src/core/thread_model_patch.js b/addons/im_livechat/static/src/core/thread_model_patch.js index a44f745c7d1..e2c9e41a667 100644 --- a/addons/im_livechat/static/src/core/thread_model_patch.js +++ b/addons/im_livechat/static/src/core/thread_model_patch.js @@ -1,6 +1,7 @@ /* @odoo-module */ import { DEFAULT_AVATAR } from "@mail/core/common/persona_service"; +import { Record } from "@mail/core/common/record"; import { Thread } from "@mail/core/common/thread_model"; import { assignDefined } from "@mail/utils/common/misc"; @@ -8,29 +9,30 @@ import { patch } from "@web/core/utils/patch"; patch(Thread, { insert(data) { - const isUnknown = !this.get(data); const thread = super.insert(data); if (thread.type === "livechat") { if (data?.channel) { assignDefined(thread, data.channel, ["anonymous_name"]); } if (data?.operator_pid) { - thread.operator = this.store.Persona.insert({ + thread.operator = { type: "partner", id: data.operator_pid[0], displayName: data.operator_pid[1], - }); - } - if (isUnknown) { - this.store.discuss.livechat.threads.push(thread); - this.env.services["mail.thread"].sortChannels(); + }; } + this.store.discuss.livechat.threads.add(thread); + this.env.services["mail.thread"].sortChannels(); } return thread; }, }); patch(Thread.prototype, { + setup() { + super.setup(); + this.operator = Record.one("Persona"); + }, get typesAllowingCalls() { return super.typesAllowingCalls.concat(["livechat"]); }, diff --git a/addons/im_livechat/static/src/embed/chatbot/chatbot_service.js b/addons/im_livechat/static/src/embed/chatbot/chatbot_service.js index 58dde3c44ee..f993a3e929a 100644 --- a/addons/im_livechat/static/src/embed/chatbot/chatbot_service.js +++ b/addons/im_livechat/static/src/embed/chatbot/chatbot_service.js @@ -131,9 +131,7 @@ export class ChatBotService { channel_uuid: this.livechatService.thread.uuid, chatbot_script_id: this.chatbot.scriptId, }); - this.livechatService.thread?.messages.push( - this.store.Message.insert({ ...message, body: markup(message.body) }) - ); + this.livechatService.thread?.messages.push({ ...message, body: markup(message.body) }); this.currentStep = null; this.start(); } @@ -147,11 +145,10 @@ export class ChatBotService { chatbot_script_id: this.chatbot.scriptId, }); for (const rawMessage of rawMessages) { - const message = this.store.Message.insert({ + this.livechatService.thread?.messages.add({ ...rawMessage, body: markup(rawMessage.body), }); - this.livechatService.thread?.messages.add(message); } this.hasPostedWelcomeSteps = true; } @@ -180,11 +177,10 @@ export class ChatBotService { return; } if (stepMessage) { - const message = this.store.Message.insert({ + this.livechatService.thread?.messages.add({ ...stepMessage, body: markup(stepMessage.body), }); - this.livechatService.thread?.messages.add(message); } this.currentStep = step; if ( @@ -281,10 +277,8 @@ export class ChatBotService { channel_uuid: this.livechatService.thread.uuid, }); this.currentStep.isEmailValid = success; - if (msg && !this.livechatService.thread.messages.some((m) => m.id === msg.id)) { - this.livechatService.thread.messages.push( - this.store.Message.insert({ ...msg, body: markup(msg.body) }) - ); + if (msg) { + this.livechatService.thread.messages.add({ ...msg, body: markup(msg.body) }); } } diff --git a/addons/im_livechat/static/src/embed/core/@types/models.d.ts b/addons/im_livechat/static/src/embed/core/@types/models.d.ts new file mode 100644 index 00000000000..2a32d997e96 --- /dev/null +++ b/addons/im_livechat/static/src/embed/core/@types/models.d.ts @@ -0,0 +1,6 @@ +declare module "models" { + export interface Thread { + chatbotTypingMessage: Message, + livechatWelcomeMessage: Message, + } +} diff --git a/addons/im_livechat/static/src/embed/core/messaging_service_patch.js b/addons/im_livechat/static/src/embed/core/messaging_service_patch.js index 8a315177ce2..a27ba46728e 100644 --- a/addons/im_livechat/static/src/embed/core/messaging_service_patch.js +++ b/addons/im_livechat/static/src/embed/core/messaging_service_patch.js @@ -11,10 +11,10 @@ patch(Messaging.prototype, { return super.initialize(); } if (session.livechatData?.options.current_partner_id) { - this.store.user = this.store.Persona.insert({ + this.store.user = { type: "partner", id: session.livechatData.options.current_partner_id, - }); + }; } this.store.isMessagingReady = true; this.isReady.resolve({ diff --git a/addons/im_livechat/static/src/embed/core/thread_model_patch.js b/addons/im_livechat/static/src/embed/core/thread_model_patch.js index 4bb852c80d7..6aa726ed367 100644 --- a/addons/im_livechat/static/src/embed/core/thread_model_patch.js +++ b/addons/im_livechat/static/src/embed/core/thread_model_patch.js @@ -1,5 +1,6 @@ /* @odoo-module */ +import { Record } from "@mail/core/common/record"; import { Thread } from "@mail/core/common/thread_model"; import { onChange } from "@mail/utils/common/misc"; @@ -25,20 +26,20 @@ patch(Thread, { } }); if (chatbotService.isChatbotThread(thread)) { - thread.chatbotTypingMessage = this.store.Message.insert({ + thread.chatbotTypingMessage = { id: messageService.getNextTemporaryId(), res_id: thread.id, model: thread.model, author: thread.operator, - }); + }; } else { - thread.livechatWelcomeMessage = this.store.Message.insert({ + thread.livechatWelcomeMessage = { id: messageService.getNextTemporaryId(), body: livechatService.options.default_message, res_id: thread.id, model: thread.model, author: thread.operator, - }); + }; } } return thread; @@ -48,14 +49,19 @@ patch(Thread, { patch(Thread.prototype, { chatbotScriptId: null, + setup() { + super.setup(); + this.chatbotTypingMessage = Record.one("Message"); + this.livechatWelcomeMessage = Record.one("Message"); + }, update(data) { super.update(...arguments); if (data.operator_pid) { - this.operator = this._store.Persona.insert({ + this.operator = { type: "partner", id: data.operator_pid[0], name: data.operator_pid[1], - }); + }; } }, diff --git a/addons/im_livechat/static/src/embed/core/thread_service_patch.js b/addons/im_livechat/static/src/embed/core/thread_service_patch.js index 1a10f179613..5580cd74285 100644 --- a/addons/im_livechat/static/src/embed/core/thread_service_patch.js +++ b/addons/im_livechat/static/src/embed/core/thread_service_patch.js @@ -64,7 +64,7 @@ patch(ThreadService.prototype, { }, avatarUrl(author, thread) { - if (thread.type !== "livechat") { + if (thread?.type !== "livechat") { return super.avatarUrl(...arguments); } const isFromOperator = diff --git a/addons/im_livechat/static/tests/embed/livechat_session_tests.js b/addons/im_livechat/static/tests/embed/livechat_session_tests.js index ceeb032f272..1c0092a1b3b 100644 --- a/addons/im_livechat/static/tests/embed/livechat_session_tests.js +++ b/addons/im_livechat/static/tests/embed/livechat_session_tests.js @@ -5,7 +5,7 @@ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; import { loadDefaultConfig, start } from "@im_livechat/../tests/embed/helper/test_utils"; import { LivechatButton } from "@im_livechat/embed/core_ui/livechat_button"; -import { mockTimeout, triggerHotkey } from "@web/../tests/helpers/utils"; +import { mockTimeout, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; import { click, contains, insertText } from "@web/../tests/utils"; QUnit.module("livechat session"); @@ -57,6 +57,7 @@ QUnit.test("Seen message is saved on the session", async (assert) => { await insertText(".o-mail-Composer-input", "Hello World!"); triggerHotkey("Enter"); await contains(".o-mail-Message", { count: 2 }); + await nextTick(); // wait for message seen assert.strictEqual( env.services["im_livechat.livechat"].sessionCookie.seen_message_id, env.services["im_livechat.livechat"].thread.newestMessage.id diff --git a/addons/mail/static/src/core/common/attachment_model.js b/addons/mail/static/src/core/common/attachment_model.js index 6d46c8b3de5..7f4a576a004 100644 --- a/addons/mail/static/src/core/common/attachment_model.js +++ b/addons/mail/static/src/core/common/attachment_model.js @@ -11,10 +11,6 @@ export class Attachment extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Attachment} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Attachment} */ static get(data) { return super.get(data); } @@ -26,7 +22,8 @@ export class Attachment extends Record { if (!("id" in data)) { throw new Error("Cannot insert attachment: id is missing in data"); } - const attachment = this.get(data) ?? this.new(data); + /** @type {import("models").Attachment} */ + const attachment = this.preinsert(data); Object.assign(attachment, { id: data.id }); attachment.update(data); return attachment; @@ -55,10 +52,10 @@ export class Attachment extends Record { const threadData = Array.isArray(data.originThread) ? data.originThread[0][1] : data.originThread; - this.originThread = this._store.Thread.insert({ + this.originThread = { model: threadData.model, id: threadData.id, - }); + }; const thread = this.originThread; thread.attachments.add(this); thread.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1)); diff --git a/addons/mail/static/src/core/common/autoresize_input.js b/addons/mail/static/src/core/common/autoresize_input.js index 10e14b93967..f65038fae7c 100644 --- a/addons/mail/static/src/core/common/autoresize_input.js +++ b/addons/mail/static/src/core/common/autoresize_input.js @@ -14,7 +14,7 @@ export class AutoresizeInput extends Component { enabled: { optional: true }, onValidate: { type: Function, optional: true }, placeholder: { type: String, optional: true }, - value: { type: String }, + value: { type: String, optional: true }, }; static defaultProps = { autofocus: false, diff --git a/addons/mail/static/src/core/common/canned_response_model.js b/addons/mail/static/src/core/common/canned_response_model.js index d92c98eee64..a7cabcec2dd 100644 --- a/addons/mail/static/src/core/common/canned_response_model.js +++ b/addons/mail/static/src/core/common/canned_response_model.js @@ -7,10 +7,6 @@ export class CannedResponse extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").CannedResponse} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").CannedResponse} */ static get(data) { return super.get(data); } @@ -19,7 +15,8 @@ export class CannedResponse extends Record { * @returns {import("models").CannedResponse} */ static insert(data) { - const cannedResponse = this.get(data) ?? this.new(data); + /** @type {import("models").CannedResponse} */ + const cannedResponse = this.preinsert(data); Object.assign(cannedResponse, { id: data.id, name: data.source, diff --git a/addons/mail/static/src/core/common/channel_member_model.js b/addons/mail/static/src/core/common/channel_member_model.js index 7f1fe2057eb..bd938e68dcd 100644 --- a/addons/mail/static/src/core/common/channel_member_model.js +++ b/addons/mail/static/src/core/common/channel_member_model.js @@ -14,10 +14,6 @@ export class ChannelMember extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").ChannelMember} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").ChannelMember} */ static get(data) { return super.get(data); } @@ -27,7 +23,8 @@ export class ChannelMember extends Record { */ static insert(data) { const memberData = Array.isArray(data) ? data[1] : data; - const member = this.get(memberData) ?? this.new(memberData); + /** @type {import("models").ChannelMember} */ + const member = this.preinsert(memberData); member.update(data); return member; } @@ -36,19 +33,19 @@ export class ChannelMember extends Record { const [command, memberData] = Array.isArray(data) ? data : ["ADD", data]; this.id = memberData.id; if ("persona" in memberData) { - this.persona = this._store.Persona.insert({ + this.persona = { ...(memberData.persona.partner ?? memberData.persona.guest), type: memberData.persona.guest ? "guest" : "partner", country: memberData.persona.partner?.country, channelId: memberData.persona.guest ? memberData.channel.id : null, - }); + }; } let thread = memberData.thread ?? this.thread; if (!thread && memberData.channel?.id) { - thread = this._store.Thread.insert({ + thread = { id: memberData.channel.id, model: "discuss.channel", - }); + }; } if (thread && !this.thread) { this.thread = thread; diff --git a/addons/mail/static/src/core/common/chat_window_model.js b/addons/mail/static/src/core/common/chat_window_model.js index f4bd2cb688a..1368c423f9f 100644 --- a/addons/mail/static/src/core/common/chat_window_model.js +++ b/addons/mail/static/src/core/common/chat_window_model.js @@ -12,10 +12,6 @@ export class ChatWindow extends Record { /** @type {Object c.thread?.eq(data.thread)); if (!chatWindow) { - const chatWindow = this.new(data); + /** @type {import("models").ChatWindow} */ + const chatWindow = this.preinsert(data); Object.assign(chatWindow, { thread: data.thread }); assignDefined(chatWindow, data); let index; @@ -54,7 +51,7 @@ export class ChatWindow extends Record { data.replaceNewMessageChatWindow ? 1 : 0, chatWindow ); - return this.store.discuss.chatWindows[index]; // return reactive version + return chatWindow; // return reactive version } if (chatWindow.hidden) { this.env.services["mail.chat_window"].makeVisible(chatWindow); diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index 780dd90673c..b98a1568b40 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -9,10 +9,6 @@ import { OR, Record } from "@mail/core/common/record"; export class Composer extends Record { static id = OR("thread", "message"); /** @returns {import("models").Composer} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Composer} */ static get(data) { return super.get(data); } @@ -27,7 +23,8 @@ export class Composer extends Record { } let composer = (thread ?? message)?.composer; if (!composer) { - composer = this.new(data); + /** @type {import("models").Composer} */ + composer = this.preinsert(data); const { message, thread } = data; if (thread) { composer.thread = thread; diff --git a/addons/mail/static/src/core/common/discuss_app_category_model.js b/addons/mail/static/src/core/common/discuss_app_category_model.js index 28aecc1b0ac..0ec6ca23532 100644 --- a/addons/mail/static/src/core/common/discuss_app_category_model.js +++ b/addons/mail/static/src/core/common/discuss_app_category_model.js @@ -6,16 +6,13 @@ import { Record } from "./record"; export class DiscussAppCategory extends Record { static id = "id"; /** @returns {import("models").DiscussAppCategory} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").DiscussAppCategory} */ static get(data) { return super.get(data); } /** @returns {import("models").DiscussAppCategory} */ static insert(data) { - const category = this.get(data) ?? this.new(data); + /** @type {import("models").DiscussAppCategory} */ + const category = this.preinsert(data); assignDefined(category, data); return category; } diff --git a/addons/mail/static/src/core/common/discuss_app_model.js b/addons/mail/static/src/core/common/discuss_app_model.js index c24d0e3f348..a11ff6cfd08 100644 --- a/addons/mail/static/src/core/common/discuss_app_model.js +++ b/addons/mail/static/src/core/common/discuss_app_model.js @@ -4,11 +4,11 @@ import { _t } from "@web/core/l10n/translation"; import { Record } from "./record"; export class DiscussApp extends Record { - /** @returns {import("models").DiscussApp} */ static new(data) { + /** @type {import("models").DiscussApp} */ const res = super.new(data); Object.assign(res, { - channels: this.store.DiscussAppCategory.insert({ + channels: { extraClass: "o-mail-DiscussSidebarCategory-channel", id: "channels", name: _t("Channels"), @@ -18,8 +18,8 @@ export class DiscussApp extends Record { serverStateKey: "is_discuss_sidebar_category_channel_open", addTitle: _t("Add or join a channel"), addHotkey: "c", - }), - chats: this.store.DiscussAppCategory.insert({ + }, + chats: { extraClass: "o-mail-DiscussSidebarCategory-chat", id: "chats", name: _t("Direct messages"), @@ -29,7 +29,7 @@ export class DiscussApp extends Record { serverStateKey: "is_discuss_sidebar_category_chat_open", addTitle: _t("Start a conversation"), addHotkey: "d", - }), + }, }); return res; } @@ -38,9 +38,8 @@ export class DiscussApp extends Record { return super.get(data); } /** @returns {import("models").DiscussApp} */ - static insert() { - const app = this.get() ?? this.new(); - return app; + static insert(data) { + return super.insert(data); } /** @type {'mailbox'|'all'|'channel'|'chat'|'livechat'} */ diff --git a/addons/mail/static/src/core/common/follower_model.js b/addons/mail/static/src/core/common/follower_model.js index 0de80782d8d..896b3989a8e 100644 --- a/addons/mail/static/src/core/common/follower_model.js +++ b/addons/mail/static/src/core/common/follower_model.js @@ -15,10 +15,6 @@ export class Follower extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Follower} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Follower} */ static get(data) { return super.get(data); } @@ -27,12 +23,13 @@ export class Follower extends Record { * @returns {import("models").Follower} */ static insert(data) { - const follower = this.get(data) ?? this.new(data); + /** @type {import("models").Follower} */ + const follower = this.preinsert(data); Object.assign(follower, { followedThread: data.followedThread, id: data.id, isActive: data.is_active, - partner: this.store.Persona.insert({ ...data.partner, type: "partner" }), + partner: { ...data.partner, type: "partner" }, }); return follower; } diff --git a/addons/mail/static/src/core/common/link_preview_model.js b/addons/mail/static/src/core/common/link_preview_model.js index d42ee2d3ab9..66290aa9f68 100644 --- a/addons/mail/static/src/core/common/link_preview_model.js +++ b/addons/mail/static/src/core/common/link_preview_model.js @@ -5,10 +5,6 @@ import { Record } from "@mail/core/common/record"; export class LinkPreview extends Record { static id = "id"; /** @returns {import("models").LinkPreview} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").LinkPreview} */ static get(data) { return super.get(data); } @@ -20,13 +16,10 @@ export class LinkPreview extends Record { const message = this.store.Message.get(data.message_id); data.message = message; delete data.message_id; - let linkPreview = message?.linkPreviews.find((lp) => lp.id === data.id); - if (linkPreview) { - return Object.assign(linkPreview, data); - } - linkPreview = this.new(data); + /** @type {import("models").LinkPreview} */ + const linkPreview = this.preinsert(data); Object.assign(linkPreview, data); - message?.linkPreviews.push(linkPreview); + message?.linkPreviews.add(linkPreview); return linkPreview; } diff --git a/addons/mail/static/src/core/common/message_model.js b/addons/mail/static/src/core/common/message_model.js index fb698df1354..f4c57434002 100644 --- a/addons/mail/static/src/core/common/message_model.js +++ b/addons/mail/static/src/core/common/message_model.js @@ -1,7 +1,6 @@ /* @odoo-module */ import { Record } from "@mail/core/common/record"; -import { replaceArrayWithCompare } from "@mail/utils/common/arrays"; import { htmlToTextContentInline } from "@mail/utils/common/format"; import { assignDefined } from "@mail/utils/common/misc"; @@ -19,10 +18,6 @@ export class Message extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Message} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Message} */ static get(data) { return super.get(data); } @@ -37,7 +32,8 @@ export class Message extends Record { id: data.res_id, }); } - const message = this.get(data) ?? this.new(data); + /** @type {import("models").Message} */ + const message = this.preinsert(data); message.update(data); return message; } @@ -72,7 +68,7 @@ export class Message extends Record { ? this.starred_partner_ids.includes(this._store.user.id) : false, isTransient, - parentMessage: parentMessage ? this._store.Message.insert(parentMessage) : undefined, + parentMessage: parentMessage || undefined, resId, resModel, subtypeDescription, @@ -89,48 +85,23 @@ export class Message extends Record { : record_name || undefined, }); } - replaceArrayWithCompare( - this.attachments, - attachments.map((attachment) => - this._store.Attachment.insert({ message: this, ...attachment }) - ) - ); + this.attachments = attachments.map((attachment) => ({ message: this, ...attachment })); if (data.author?.id) { - this.author = this._store.Persona.insert({ - ...data.author, - type: "partner", - }); + this.author = { ...data.author, type: "partner" }; } if (data.guestAuthor?.id) { - this.author = this._store.Persona.insert({ - ...data.guestAuthor, - type: "guest", - channelId: this.originThread.id, - }); + this.author = { ...data.guestAuthor, type: "guest", channelId: this.originThread.id }; } - replaceArrayWithCompare( - this.linkPreviews, - linkPreviews.map((data) => this._store.LinkPreview.insert({ ...data, message: this })) - ); - replaceArrayWithCompare( - this.notifications, - notifications.map((notification) => - this._store.Notification.insert({ ...notification, message: this }) - ) - ); - replaceArrayWithCompare( - this.recipients, - recipients.map((recipient) => - this._store.Persona.insert({ ...recipient, type: "partner" }) - ) - ); + this.linkPreviews = linkPreviews.map((data) => ({ ...data, message: this })); + this.notifications = notifications.map((notif) => ({ ...notif, message: this })); + this.recipients = recipients.map((recipient) => ({ ...recipient, type: "partner" })); if ("user_follower_id" in data && data.user_follower_id && this._store.self) { - this.originThread.selfFollower = this._store.Follower.insert({ + this.originThread.selfFollower = { followedThread: this.originThread, id: data.user_follower_id, isActive: true, partner: this._store.self, - }); + }; } if (data.messageReactionGroups) { const reactionContentToUnlink = new Set(); diff --git a/addons/mail/static/src/core/common/message_reactions_model.js b/addons/mail/static/src/core/common/message_reactions_model.js index 159f20a53f5..4110fc7331e 100644 --- a/addons/mail/static/src/core/common/message_reactions_model.js +++ b/addons/mail/static/src/core/common/message_reactions_model.js @@ -5,10 +5,6 @@ import { AND, Record } from "@mail/core/common/record"; export class MessageReactions extends Record { static id = AND("message", "content"); /** @returns {import("models").MessageReactions} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").MessageReactions} */ static get(data) { return super.get(data); } @@ -21,7 +17,8 @@ export class MessageReactions extends Record { ({ content }) => content === data.content ); if (!reaction) { - reaction = this.new(data); + /** @type {import("models").MessageReactions} */ + reaction = this.preinsert(data); } const personasToUnlink = new Set(); const alreadyKnownPersonaIds = new Set(reaction.personas.map((p) => p.localId)); @@ -48,7 +45,7 @@ export class MessageReactions extends Record { Object.assign(reaction, { count: data.count, content: data.content, - message: this.store.Message.insert(data.message), + message: data.message, personas: reaction.personas.filter((p) => !personasToUnlink.has(p)), }); return reaction; diff --git a/addons/mail/static/src/core/common/messaging_service.js b/addons/mail/static/src/core/common/messaging_service.js index 940ea460d63..32da5fc7f43 100644 --- a/addons/mail/static/src/core/common/messaging_service.js +++ b/addons/mail/static/src/core/common/messaging_service.js @@ -30,26 +30,26 @@ export class Messaging { this.store.Persona.insert({ id: user.partnerId, type: "partner", isAdmin: user.isAdmin }); this.registeredImStatusPartners = reactive([], () => this.updateImStatusRegistration()); this.store.registeredImStatusPartners = this.registeredImStatusPartners; - this.store.discuss.inbox = this.store.Thread.insert({ + this.store.discuss.inbox = { id: "inbox", model: "mail.box", name: _t("Inbox"), type: "mailbox", - }); - this.store.discuss.starred = this.store.Thread.insert({ + }; + this.store.discuss.starred = { id: "starred", model: "mail.box", name: _t("Starred"), type: "mailbox", counter: 0, - }); - this.store.discuss.history = this.store.Thread.insert({ + }; + this.store.discuss.history = { id: "history", model: "mail.box", name: _t("History"), type: "mailbox", counter: 0, - }); + }; this.updateImStatusRegistration(); } @@ -64,22 +64,16 @@ export class Messaging { initMessagingCallback(data) { if (data.current_partner) { - this.store.user = this.store.Persona.insert({ - ...data.current_partner, - type: "partner", - }); + this.store.user = { ...data.current_partner, type: "partner" }; } if (data.currentGuest) { - this.store.guest = this.store.Persona.insert({ + this.store.guest = { ...data.currentGuest, type: "guest", channelId: data.channels[0]?.id, - }); + }; } - this.store.odoobot = this.store.Persona.insert({ - ...data.odoobot, - type: "partner", - }); + this.store.odoobot = { ...data.odoobot, type: "partner" }; const settings = data.current_user_settings; this.userSettingsService.updateFromCommands(settings); this.userSettingsService.id = settings.id; diff --git a/addons/mail/static/src/core/common/notification_group_model.js b/addons/mail/static/src/core/common/notification_group_model.js index 606532aec36..d0a55f1fa2a 100644 --- a/addons/mail/static/src/core/common/notification_group_model.js +++ b/addons/mail/static/src/core/common/notification_group_model.js @@ -10,10 +10,6 @@ export class NotificationGroup extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").NotificationGroup} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").NotificationGroup} */ static get(data) { return super.get(data); } @@ -31,7 +27,8 @@ export class NotificationGroup extends Record { }); if (!group) { const id = nextId++; - group = this.new({ id }); + /** @type {import("models").NotificationGroup} */ + group = this.preinsert({ id }); Object.assign(group, { id }); this.store.discuss.notificationGroups.add(group); } diff --git a/addons/mail/static/src/core/common/notification_model.js b/addons/mail/static/src/core/common/notification_model.js index cbfabe5aeda..9fc365df21d 100644 --- a/addons/mail/static/src/core/common/notification_model.js +++ b/addons/mail/static/src/core/common/notification_model.js @@ -9,10 +9,6 @@ export class Notification extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Notification} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Notification} */ static get(data) { return super.get(data); } @@ -21,7 +17,8 @@ export class Notification extends Record { * @returns {import("models").Notification} */ static insert(data) { - const notification = this.get(data) ?? this.new(data); + /** @type {import("models").Notification} */ + const notification = this.preinsert(data); Object.assign(notification, { id: data.id }); notification.update(data); return notification; @@ -34,11 +31,11 @@ export class Notification extends Record { notification_type: data.notification_type, failure_type: data.failure_type, persona: data.res_partner_id - ? this._store.Persona.insert({ + ? { id: data.res_partner_id[0], displayName: data.res_partner_id[1], type: "partner", - }) + } : undefined, }); if (!this.message.author?.eq(this._store.self)) { diff --git a/addons/mail/static/src/core/common/persona_model.js b/addons/mail/static/src/core/common/persona_model.js index ff5af347430..f6bb8de1a17 100644 --- a/addons/mail/static/src/core/common/persona_model.js +++ b/addons/mail/static/src/core/common/persona_model.js @@ -18,10 +18,6 @@ export class Persona extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Persona} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Persona} */ static get(data) { return super.get(data); } @@ -30,10 +26,7 @@ export class Persona extends Record { * @returns {import("models").Persona} */ static insert(data) { - const persona = this.get(data) ?? this.new(data); - persona.update(data); - // return reactive version - return persona; + return super.insert(data); } update(data) { diff --git a/addons/mail/static/src/core/common/record.js b/addons/mail/static/src/core/common/record.js index d764d970b94..706c17c2ae6 100644 --- a/addons/mail/static/src/core/common/record.js +++ b/addons/mail/static/src/core/common/record.js @@ -17,6 +17,29 @@ export function OR(...args) { return [OR_SYM, ...args]; } +/** + * @param {R|any} val + * @param {Record} record + * @param {string} fname + * @param {(R) => void} fn + */ +export function preinsert(val, record, fname, fn) { + /** @type {R} */ + let r3; + if (!(val instanceof Record)) { + const { targetModel } = record.Model.__rels__.get(fname); + r3 = record.Model.store[targetModel].preinsert(val); + } else { + r3 = val; + } + fn(r3); + if (!(val instanceof Record)) { + // was preinserted, fully insert now + const { targetModel } = record.Model.__rels__.get(fname); + record.Model.store[targetModel].insert(val); + } +} + export class RecordInverses { /** * Track the inverse of a record. Each record contains this map. @@ -102,16 +125,16 @@ export class RecordList extends Array { if (typeof name !== "symbol" && !window.isNaN(parseInt(name))) { // support for "array[index] = r3" syntax const index = parseInt(name); - /** @type {R} */ - const r3 = val; - const r2 = receiver[index]; - if (r2 && r2.notEq(r3)) { - receiver.__deleteInverse__(r2); - } - receiver.__list__[index] = r3?.localId; - if (r3) { - receiver.__addInverse__(r3); - } + receiver._preinsert(val, (r3) => { + const r2 = receiver[index]; + if (r2 && r2.notEq(r3)) { + receiver.__deleteInverse__(r2); + } + receiver.__list__[index] = r3?.localId; + if (r3) { + receiver.__addInverse__(r3); + } + }); } else if (name === "length") { const newLength = parseInt(val); if (newLength < receiver.length) { @@ -125,7 +148,13 @@ export class RecordList extends Array { }, }); } - + /** + * @param {R|any} val + * @param {(R) => void} fn + */ + _preinsert(val, fn) { + preinsert(val, this.owner, this.name, fn); + } /** * @param {number} index * @returns {R} @@ -135,11 +164,11 @@ export class RecordList extends Array { } /** @param {R[]} records */ push(...records) { - this.__list__.push(...records.map((r3) => r3.localId)); - for (const r3 of records) { - if (r3) { + for (const val of records) { + this._preinsert(val, (r3) => { + this.__list__.push(r3.localId); this.__addInverse__(r3); - } + }); } return this.__list__.length; } @@ -161,11 +190,11 @@ export class RecordList extends Array { } /** @param {R[]} records */ unshift(...records) { - this.__list__.unshift(...records.map((r3) => r3.localId)); - for (const r3 of records) { - if (r3) { + for (const val of records) { + this._preinsert(val, (r3) => { + this.__list__.unshift(r3.localId); this.__addInverse__(r3); - } + }); } return this.__list__.length; } @@ -251,20 +280,31 @@ export class RecordList extends Array { .map((localId) => this.__store__.get(localId)) .concat(...collections.map((c) => [...c])); } - /** @param {R} */ - add(r) { - if (this.indexOf(r) !== -1) { - return; + /** @param {...R} */ + add(...records) { + for (const val of records) { + this._preinsert(val, (r) => { + if (this.indexOf(r) === -1) { + this.push(r); + } + }); } - this.push(r); } - /** @param {R} */ - delete(r) { - const index = this.indexOf(r); - if (index === -1) { - return; + /** @param {...R} */ + delete(...records) { + for (const val of records) { + this._preinsert(val, (r) => { + const index = this.indexOf(r); + if (index !== -1) { + this.splice(index, 1); + } + }); + } + } + clear() { + while (this.__list__.length > 0) { + this.pop(); } - this.splice(index, 1); } /** @yields {R} */ *[Symbol.iterator]() { @@ -306,7 +346,7 @@ export class Record { } static _localId(expr, data, { brackets = false } = {}) { if (!Array.isArray(expr)) { - if (this.Class.__rels__.has(expr)) { + if (this.__rels__.has(expr)) { // relational field (note: optional when OR) return `(${data[expr]?.localId})`; } @@ -322,6 +362,35 @@ export class Record { } return res; } + static _retrieveIdFromData(data) { + const res = {}; + function _deepRetrieve(expr2) { + if (typeof expr2 === "string") { + return Object.assign(res, { [expr2]: data[expr2] }); + } + if (expr2 instanceof Array) { + for (const expr of this.id) { + if (typeof expr === "symbol") { + continue; + } + _deepRetrieve(expr); + } + } + } + if (this.id === undefined) { + return res; + } + if (typeof this.id === "string") { + return { [this.id]: data[this.id] }; + } + for (const expr of this.id) { + if (typeof expr === "symbol") { + continue; + } + _deepRetrieve(expr); + } + return res; + } /** * Technical attribute, DO NOT USE in business code. * This class is almost equivalent to current class of model, @@ -340,7 +409,8 @@ export class Record { */ static new(data) { const obj = new this.Class(); - let record = Object.assign(obj, { localId: this.localId(data), Model: this }); + const ids = this._retrieveIdFromData(data); + let record = Object.assign(obj, { Model: this, localId: this.localId(data), ...ids }); Object.assign(record, { _store: this.store }); this.records[record.localId] = record; // return reactive version @@ -353,7 +423,7 @@ export class Record { * @returns {import("models").Models[M]} */ static one(modelName) { - return ONE_SYM; + return [ONE_SYM, modelName]; } /** * @template {keyof import("model").Models} M @@ -361,13 +431,24 @@ export class Record { * @returns {import("models").Models[M][]} */ static many(modelName) { - return MANY_SYM; + return [MANY_SYM, modelName]; } /** * @param {Object} data * @returns {Record} */ - static insert(data) {} + static insert(data) { + const res = this.preinsert(data); + res.update(data); + return res; + } + /** + * @param {Object} data + * @returns {Record} + */ + static preinsert(data) { + return this.get(data) ?? this.new(data); + } /** * Raw relational values of the record, each of which contains object id(s) @@ -377,7 +458,7 @@ export class Record { * @type {Map} */ __rels__ = new Map(); - /** Track inverse relations of current record. */ + /** @type {Map} */ __invs__ = new RecordInverses(); /** @type {import("@mail/core/common/store_service").Store} */ _store; @@ -405,14 +486,12 @@ export class Record { setup() {} + update(data) {} + delete() { const r1 = this; - for (const [name, l1] of r1.__rels__.entries()) { - if (l1 instanceof RecordList) { - r1[name] = []; - } else { - r1[name] = undefined; - } + for (const name of r1.__rels__.keys()) { + r1[name] = undefined; } for (const [localId, names] of r1.__invs__.__map__.entries()) { for (const [name2, count] of names.entries()) { diff --git a/addons/mail/static/src/core/common/store_service.js b/addons/mail/static/src/core/common/store_service.js index f5c13443875..674715bdd45 100644 --- a/addons/mail/static/src/core/common/store_service.js +++ b/addons/mail/static/src/core/common/store_service.js @@ -6,12 +6,12 @@ import { reactive } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; -import { modelRegistry, Record, RecordInverses, RecordList } from "./record"; +import { modelRegistry, preinsert, Record, RecordInverses, RecordList } from "./record"; export class Store extends Record { /** @returns {import("models").Store} */ static insert() { - return this.get() ?? this.new(); + return super.insert(); } /** @type {typeof import("@mail/core/web/activity_model").Activity} */ @@ -50,18 +50,10 @@ export class Store extends Record { Thread; lastChannelSubscription = ""; - /** - * This is the current logged partner - * - * @type {import("models").Persona} - */ - user = null; - /** - * This is the current logged guest - * - * @type {import("models").Persona} - */ - guest = null; + /** This is the current logged partner */ + user = Record.one("Persona"); + /** This is the current logged guest */ + guest = Record.one("Persona"); /** * The last id of bus notification at the time for fetch init_messaging. * When receiving a notification: @@ -77,8 +69,7 @@ export class Store extends Record { */ inPublicPage = false; companyName = ""; - /** @type {import("models").Persona} */ - odoobot = null; + odoobot = Record.one("Persona"); odoobotOnboarding; users = {}; internalUserGroupId = null; @@ -145,9 +136,9 @@ export const storeService = { }, }; const Models = {}; - for (const [name, _Model] of modelRegistry.getEntries()) { + for (const [name, _OgClass] of modelRegistry.getEntries()) { /** @type {typeof Record} */ - const Model = _Model; + const OgClass = _OgClass; if (res.store[name]) { throw new Error( `There must be no duplicated Model Names (duplicate found: ${name})` @@ -156,27 +147,22 @@ export const storeService = { // classes cannot be made reactive because they are functions and they are not supported. // work-around: make an object whose prototype is the class, so that static props become // instance props. - const entry = Object.assign(Object.create(Model), { env, store: res.store }); + const Model = Object.assign(Object.create(OgClass), { env, store: res.store }); // Produce another class with changed prototype, so that there are automatic get/set on relational fields - let detecting = true; const Class = { - [Model.name]: class extends Model { - static __rels__ = new Set(); + [OgClass.name]: class extends OgClass { constructor() { super(); - if (detecting) { - return; - } - for (const name of this.constructor.__rels__) { + for (const name of Model.__rels__.keys()) { // Relational fields contain symbols for detection in original class. // This constructor is called on genuine records: // - 'one' fields => undefined // - 'many' fields => RecordList let newVal; - if (this[name] === Record.one()) { + if (this[name]?.[0] === Record.one()[0]) { newVal = undefined; } - if (this[name] === Record.many()) { + if (this[name]?.[0] === Record.many()[0]) { newVal = new RecordList(); newVal.__store__ = res.store; newVal.name = name; @@ -213,63 +199,68 @@ export const storeService = { }, /** @param {Record} receiver */ set(target, name, val, receiver) { - if (receiver.__rels__.has(name)) { - const oldVal = receiver.__rels__.get(name); - if (oldVal instanceof RecordList) { - const r1 = receiver; - /** @type {RecordList} */ - const l1 = r1.__rels__.get(name); - /** @type {Record[]|Set|RecordList} */ - const collection = val; - const oldRecords = l1.slice(); - l1.__list__ = []; - for (const r2 of oldRecords) { - r2.__invs__.delete(r1.localId, name); - } - for (const r3 of collection) { + if (!receiver.__rels__.has(name)) { + Reflect.set(target, name, val, receiver); + return true; + } + const oldVal = receiver.__rels__.get(name); + if (oldVal instanceof RecordList) { + // [Record.many] = + const r1 = receiver; + /** @type {RecordList} */ + const l1 = r1.__rels__.get(name); + /** @type {Record[]|Set|RecordList} */ + const collection = val; + const oldRecords = l1.slice(); + for (const r2 of oldRecords) { + r2.__invs__.delete(r1.localId, name); + } + l1.clear(); + if ([null, false, undefined].includes(val)) { + return true; + } + for (const v of collection) { + preinsert(v, r1, name, (r3) => { l1.__list__.push(r3.localId); r3.__invs__.add(r1.localId, name); - } - } else { - const r1 = receiver; - const l1 = r1.__rels__.get(name); - const r2 = res.store.get(l1); - /** @type {Record} */ - const r3 = val; - if (r2 && r2.notEq(r3)) { - r2.__invs__.delete(r1.localId, name); - } - if (r3) { - r1.__rels__.set(name, r3?.localId); - if (!(r3 instanceof Record)) { - return true; // not a record, ignored - } - r3.__invs__.add(r1.localId, name); - } else { - delete r1[name]; - } + }); } } else { - Reflect.set(target, name, val, receiver); + // [Record.one] = + const r1 = receiver; + const l1 = r1.__rels__.get(name); + const r2 = res.store.get(l1); + if (r2) { + r2.__invs__.delete(r1.localId, name); + } + if ([null, false, undefined].includes(val)) { + delete receiver[name]; + return true; + } + preinsert(val, r1, name, (r3) => { + r1.__rels__.set(name, r3?.localId); + }); } return true; }, }); } }, - }[Model.name]; - entry.Class = Class; - entry.records = JSON.parse(JSON.stringify(Model.records)); - Models[name] = entry; - res.store[name] = entry; + }[OgClass.name]; + Object.assign(Model, { + Class, + records: JSON.parse(JSON.stringify(OgClass.records)), + __rels__: new Map(), + }); + Models[name] = Model; + res.store[name] = Model; // Detect relational fields with a dummy record and setup getter/setters on them - const obj = new Model(); - detecting = false; + const obj = new OgClass(); for (const [name, val] of Object.entries(obj)) { - if (![Record.one(), Record.many()].includes(val)) { + if (![Record.one()[0], Record.many()[0]].includes(val?.[0])) { continue; } - Class.__rels__.add(name); + Model.__rels__.set(name, { targetModel: val[1] }); } } // Make true store (as a model) @@ -280,7 +271,7 @@ export const storeService = { res.store[Model.name] = Model; } const store = res.store; - store.discuss = store.DiscussApp.insert(); + store.discuss = {}; store.discuss.activeTab = env.services.ui.isSmall ? "mailbox" : "all"; onChange(store.Thread, "records", () => store.updateBusSubscription()); services.ui.bus.addEventListener("resize", () => { diff --git a/addons/mail/static/src/core/common/thread.js b/addons/mail/static/src/core/common/thread.js index bf17e7d1d6f..e19357a1658 100644 --- a/addons/mail/static/src/core/common/thread.js +++ b/addons/mail/static/src/core/common/thread.js @@ -9,15 +9,7 @@ import { useVisible, } from "@mail/utils/common/hooks"; -import { - Component, - onMounted, - onWillStart, - onWillUpdateProps, - useEffect, - useRef, - useState, -} from "@odoo/owl"; +import { Component, onMounted, onWillUpdateProps, useEffect, useRef, useState } from "@odoo/owl"; import { Transition } from "@web/core/transition"; import { useBus, useService } from "@web/core/utils/hooks"; @@ -57,7 +49,11 @@ export class Thread extends Component { setup() { this.escape = escape; this.store = useState(useService("mail.store")); - this.state = useState({ isReplyingTo: false, showJumpPresent: false }); + this.state = useState({ + isReplyingTo: false, + mountedAndLoaded: false, + showJumpPresent: false, + }); this.threadService = useState(useService("mail.thread")); if (!this.env.inChatter || !this.props.hasScrollAdjust) { useAutoScroll("messages", () => { @@ -140,16 +136,23 @@ export class Thread extends Component { }, () => [this.props.jumpPresent] ); - onMounted(() => { - this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id; - if (!this.env.inChatter || !this.props.hasScrollAdjust) { - this.scrollPosition.restore(); - this.updateShowJumpPresent(); - } - }); - onWillStart(() => { + useEffect( + () => { + if (!this.state.mountedAndLoaded) { + return; + } + this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id; + if (!this.env.inChatter || !this.props.hasScrollAdjust) { + this.scrollPosition.restore(); + this.updateShowJumpPresent(); + } + }, + () => [this.state.mountedAndLoaded] + ); + onMounted(async () => { this.lastJumpPresent = this.props.jumpPresent; - this.threadService.fetchNewMessages(this.props.thread); + await this.threadService.fetchNewMessages(this.props.thread); + this.state.mountedAndLoaded = true; }); useBus(this.env.bus, "MAIL:RELOAD-THREAD", ({ detail }) => { const { model, id } = this.props.thread; diff --git a/addons/mail/static/src/core/common/thread_model.js b/addons/mail/static/src/core/common/thread_model.js index 32edb4db565..b0ce74a9999 100644 --- a/addons/mail/static/src/core/common/thread_model.js +++ b/addons/mail/static/src/core/common/thread_model.js @@ -2,7 +2,6 @@ import { AND, Record } from "@mail/core/common/record"; import { ScrollPosition } from "@mail/core/common/scroll_position"; -import { replaceArrayWithCompare } from "@mail/utils/common/arrays"; import { assignDefined, onChange } from "@mail/utils/common/misc"; import { deserializeDateTime } from "@web/core/l10n/dates"; @@ -27,13 +26,15 @@ export class Thread extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Thread} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Thread} */ static get(data) { return super.get(data); } + static new(data) { + /** @type {import("models").Thread} */ + const thread = super.new(data); + this.store.Composer.insert({ thread }); + return thread; + } /** * @param {string} localId * @returns {string} @@ -61,7 +62,8 @@ export class Thread extends Record { thread.update(data); return thread; } - thread = this.new(data); + /** @type {import("models").Thread} */ + thread = this.preinsert(data); Object.assign(thread, { id: data.id, model: data.model, @@ -76,11 +78,10 @@ export class Thread extends Record { onChange(thread, "channelMembers", () => this.store.updateBusSubscription()); onChange(thread, "is_pinned", () => { if (!thread.is_pinned && thread.eq(this.store.discuss.thread)) { - delete this.store.discuss.thread; + this.store.discuss.thread = undefined; } }); thread.update(data); - this.store.Composer.insert({ thread }); // return reactive version. return thread; } @@ -90,12 +91,7 @@ export class Thread extends Record { const { id, name, attachments: attachmentsData, description, ...serverData } = data; assignDefined(this, { id, name, description }); if (attachmentsData) { - replaceArrayWithCompare( - this.attachments, - attachmentsData - .map((attachmentData) => this._store.Attachment.insert(attachmentData)) - .sort((a1, a2) => a2.id - a1.id) - ); + this.attachments = attachmentsData.sort((a1, a2) => a2.id - a1.id); } if (serverData) { assignDefined(this, serverData, [ @@ -124,9 +120,7 @@ export class Thread extends Record { } const lastServerMessageId = serverData.last_message_id ?? this.lastServerMessage?.id; if (this.lastServerMessage?.id !== lastServerMessageId) { - this.lastServerMessage = this._store.Message.insert({ - id: lastServerMessageId, - }); + this.lastServerMessage = { id: lastServerMessageId }; } if (this.model === "discuss.channel" && serverData.channel) { this.channel = assignDefined(this.channel ?? {}, serverData.channel); @@ -165,15 +159,13 @@ export class Thread extends Record { case "ADD": if (members) { for (const member of members) { - const record = this._store.ChannelMember.insert(member); - this.invitedMembers.add(record); + this.invitedMembers.add(member); } } break; case "DELETE": for (const member of members) { - const record = this._store.ChannelMember.insert(member); - this.invitedMembers.delete(record); + this.invitedMembers.delete(member); } break; } @@ -183,15 +175,10 @@ export class Thread extends Record { ({ fetched_message_id, partner_id, seen_message_id }) => { return { lastFetchedMessage: fetched_message_id - ? this._store.Message.insert({ id: fetched_message_id }) + ? { id: fetched_message_id } : undefined, - lastSeenMessage: seen_message_id - ? this._store.Message.insert({ id: seen_message_id }) - : undefined, - partner: this._store.Persona.insert({ - id: partner_id, - type: "partner", - }), + lastSeenMessage: seen_message_id ? { id: seen_message_id } : undefined, + partner: { id: partner_id, type: "partner" }, }; } ); diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js index 30d3a203805..93c8707a491 100644 --- a/addons/mail/static/src/core/common/thread_service.js +++ b/addons/mail/static/src/core/common/thread_service.js @@ -81,7 +81,7 @@ export class ThreadService { * @param {import("models").Thread} thread */ async markAsRead(thread) { - if (!thread.isLoaded && thread.status === "loading") { + if (!thread.isLoaded) { await thread.isLoadedDeferred; await new Promise(setTimeout); } diff --git a/addons/mail/static/src/core/web/activity_model.js b/addons/mail/static/src/core/web/activity_model.js index dd73e299aa6..7df41d72d6b 100644 --- a/addons/mail/static/src/core/web/activity_model.js +++ b/addons/mail/static/src/core/web/activity_model.js @@ -38,10 +38,6 @@ export class Activity extends Record { /** @type {Object.} */ static records = {}; /** @returns {import("models").Activity} */ - static new(data) { - return super.new(data); - } - /** @returns {import("models").Activity} */ static get(data) { return super.get(data); } @@ -52,7 +48,8 @@ export class Activity extends Record { * @returns {import("models").Activity} */ static insert(data, { broadcast = true } = {}) { - const activity = this.get(data) ?? this.new(data); + /** @type {import("models").Activity} */ + const activity = this.preinsert(data); Object.assign(activity, { id: data.id }); if (data.request_partner_id) { data.request_partner_id = data.request_partner_id[0]; diff --git a/addons/mail/static/src/core/web/chatter.js b/addons/mail/static/src/core/web/chatter.js index 7cd4a67338c..b294c945c39 100644 --- a/addons/mail/static/src/core/web/chatter.js +++ b/addons/mail/static/src/core/web/chatter.js @@ -17,7 +17,6 @@ import { markup, onMounted, onPatched, - onWillStart, onWillUpdateProps, useChildSubEnv, useEffect, @@ -135,9 +134,7 @@ export class Chatter extends Component { "o-mail-Chatter-dropzone" ); - onMounted(this.scrollPosition.restore); - onPatched(this.scrollPosition.restore); - onWillStart(() => { + onMounted(async () => { if (this.props.threadId) { this.state.thread = this.store.Thread.insert({ id: this.props.threadId, @@ -145,12 +142,14 @@ export class Chatter extends Component { name: this.props.webRecord?.data?.display_name || undefined, }); } - return this.load(this.props.threadId, [ + await this.load(this.props.threadId, [ "followers", "attachments", "suggestedRecipients", ]); + this.scrollPosition.restore(); }); + onPatched(this.scrollPosition.restore); onWillUpdateProps((nextProps) => { this.load(nextProps.threadId, ["followers", "attachments", "suggestedRecipients"]); if (nextProps.threadId === false) { @@ -168,22 +167,33 @@ export class Chatter extends Component { }); useEffect( () => { - if (this.attachments.length === 0) { + if ( + this.state.thread && + !["new", "loading"].includes(this.state.thread.status) && + this.attachments.length === 0 + ) { this.state.isAttachmentBoxOpened = false; } }, - () => [this.attachments] + () => [this.state.thread?.status, this.attachments] ); useEffect( () => { - if (this.state.scrollToAttachments > 0) { + if ( + this.state.thread && + !["new", "loading"].includes(this.state.thread.status) && + this.state.scrollToAttachments > 0 + ) { this.attachmentBox.el.scrollIntoView({ block: "center" }); } }, - () => [this.state.scrollToAttachments] + () => [this.state.thread?.status, this.state.scrollToAttachments] ); useEffect( () => { + if (!this.state.thread) { + return; + } browser.clearTimeout(this.loadingAttachmentTimeout); if (this.state.thread?.isLoadingAttachments) { this.loadingAttachmentTimeout = browser.setTimeout( @@ -195,7 +205,7 @@ export class Chatter extends Component { } return () => browser.clearTimeout(this.loadingAttachmentTimeout); }, - () => [this.state.thread?.isLoadingAttachments] + () => [this.state.thread, this.state.thread?.isLoadingAttachments] ); } @@ -203,7 +213,7 @@ export class Chatter extends Component { * @returns {import("models").Activity[]} */ get activities() { - return this.state.thread.activities; + return this.state.thread?.activities ?? []; } get followerButtonLabel() { @@ -218,7 +228,7 @@ export class Chatter extends Component { * @returns {boolean} */ get isDisabled() { - return !this.props.threadId || !this.state.thread.hasReadAccess; + return !this.props.threadId || !this.state.thread?.hasReadAccess; } get attachments() { @@ -229,17 +239,19 @@ export class Chatter extends Component { * @returns {string} */ get toRecipientsText() { - const recipients = [...this.state.thread.recipients].slice(0, 5).map(({ partner }) => { - const text = partner.email ? partner.emailWithoutDomain : partner.name; - return `${escapeHTML( - text - )}`; - }); + const recipients = [...(this.state.thread?.recipients ?? [])] + .slice(0, 5) + .map(({ partner }) => { + const text = partner.email ? partner.emailWithoutDomain : partner.name; + return `${escapeHTML( + text + )}`; + }); const formatter = new Intl.ListFormat( this.store.env.services["user"].lang?.replace("_", "-"), { type: "unit" } ); - if (this.state.thread.recipients.length > 5) { + if (this.state.thread && this.state.thread.recipients.length > 5) { recipients.push("…"); } return markup(formatter.format(recipients)); @@ -255,7 +267,7 @@ export class Chatter extends Component { ) { const { threadModel } = this.props; this.state.thread = this.threadService.getThread(threadModel, threadId); - this.scrollPosition.model = this.state.thread.scrollPosition; + this.scrollPosition.model = this.state.thread?.scrollPosition; if (!threadId) { return; } diff --git a/addons/mail/static/src/core/web/chatter.xml b/addons/mail/static/src/core/web/chatter.xml index 8f45ed8f1b6..c659dda6561 100644 --- a/addons/mail/static/src/core/web/chatter.xml +++ b/addons/mail/static/src/core/web/chatter.xml @@ -2,7 +2,7 @@ -
+