diff --git a/addons/hr/static/src/core/web/thread_actions.js b/addons/hr/static/src/core/web/thread_actions.js index 93db4d07bd5..db9836d8c1a 100644 --- a/addons/hr/static/src/core/web/thread_actions.js +++ b/addons/hr/static/src/core/web/thread_actions.js @@ -28,10 +28,10 @@ threadActionsRegistry.add("open-hr-profile", { const orm = useService("orm"); const store = useService("mail.store"); let employeeId; - if (!component.thread?.correspondent?.employeeId && component.thread?.chatPartnerId) { + if (!component.thread?.correspondent?.employeeId && component.thread?.chatPartner) { const employees = await orm.silent.searchRead( "hr.employee", - [["user_partner_id", "=", component.thread.chatPartnerId]], + [["user_partner_id", "=", component.thread.chatPartner.id]], ["id"] ); employeeId = employees[0]?.id; diff --git a/addons/mail/static/src/core/common/attachment_model.js b/addons/mail/static/src/core/common/attachment_model.js index bf35db8d174..a78c0c45400 100644 --- a/addons/mail/static/src/core/common/attachment_model.js +++ b/addons/mail/static/src/core/common/attachment_model.js @@ -39,7 +39,7 @@ export class Attachment extends Record { id; mimetype; name; - originThreadLocalId; + originThread = Record.one("Thread"); res_name; type; /** @type {string} */ @@ -48,16 +48,10 @@ export class Attachment extends Record { url; /** @type {boolean} */ uploading; - /** @type {import("@mail/core/common/message_model").Message} */ - message; + message = Record.one("Message"); /** @type {string} */ create_date; - /** @type {import("@mail/core/common/thread_model").Thread} */ - get originThread() { - return this._store.Thread.records[this.originThreadLocalId]; - } - get isDeletable() { return true; } diff --git a/addons/mail/static/src/core/common/attachment_service.js b/addons/mail/static/src/core/common/attachment_service.js index edf4238867f..6add0e631d2 100644 --- a/addons/mail/static/src/core/common/attachment_service.js +++ b/addons/mail/static/src/core/common/attachment_service.js @@ -43,11 +43,10 @@ export class AttachmentService { const threadData = Array.isArray(data.originThread) ? data.originThread[0][1] : data.originThread; - this.store.Thread.insert({ + attachment.originThread = this.store.Thread.insert({ model: threadData.model, id: threadData.id, }); - attachment.originThreadLocalId = this.store.Thread.localId(threadData); const thread = attachment.originThread; if (attachment.notIn(thread.attachments)) { thread.attachments.push(attachment); @@ -59,7 +58,7 @@ export class AttachmentService { /** * Remove the given attachment globally. * - * @param {Attachment} attachment + * @param {import("@mail/core/common/attachment_model").Attachment} attachment */ remove(attachment) { if (attachment.tmpUrl) { 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 46c0b866ef4..1ec3dc78cf5 100644 --- a/addons/mail/static/src/core/common/channel_member_model.js +++ b/addons/mail/static/src/core/common/channel_member_model.js @@ -6,8 +6,8 @@ import { Record } from "@mail/core/common/record"; * @class ChannelMember * @typedef Data * @property {number} id - * @property {string} personaLocalId - * @property {number} threadId + * @property {import("@mail/core/common/persona_model").Persona} persona + * @property {import("@mail/core/common/thread_model").Thread} thread */ export class ChannelMember extends Record { static id = "id"; @@ -34,25 +34,9 @@ export class ChannelMember extends Record { /** @type {number} */ id; - personaLocalId; - rtcSessionId; - threadId; - - get persona() { - return this._store.Persona.records[this.personaLocalId]; - } - - set persona(persona) { - this.personaLocalId = persona?.localId; - } - - get rtcSession() { - return this._store.RtcSession.get(this.rtcSessionId); - } - - get thread() { - return this._store.Thread.get({ model: "discuss.channel", id: this.threadId }); - } + persona = Record.one("Persona"); + rtcSession = Record.one("RtcSession"); + thread = Record.one("Thread"); /** * @returns {string} diff --git a/addons/mail/static/src/core/common/channel_member_service.js b/addons/mail/static/src/core/common/channel_member_service.js index 260c6c3aafa..0886d0b77be 100644 --- a/addons/mail/static/src/core/common/channel_member_service.js +++ b/addons/mail/static/src/core/common/channel_member_service.js @@ -25,13 +25,16 @@ export class ChannelMemberService { channelId: memberData.persona.guest ? memberData.channel.id : null, }); } - member.threadId = memberData.threadId ?? member.threadId ?? memberData.channel?.id; - if (member.threadId && !member.thread) { - this.store.Thread.insert({ - id: member.threadId, + let thread = memberData.thread ?? member.thread; + if (!thread && memberData.channel?.id) { + thread = this.store.Thread.insert({ + id: memberData.channel.id, model: "discuss.channel", }); } + if (thread && !member.thread) { + member.thread = thread; + } switch (command) { case "insert": { diff --git a/addons/mail/static/src/core/common/chat_window_container.xml b/addons/mail/static/src/core/common/chat_window_container.xml index e69842193c6..ca4fe096b88 100644 --- a/addons/mail/static/src/core/common/chat_window_container.xml +++ b/addons/mail/static/src/core/common/chat_window_container.xml @@ -6,7 +6,7 @@
- + @@ -21,7 +21,7 @@ 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 2e45e6e1e98..a62df699c90 100644 --- a/addons/mail/static/src/core/common/chat_window_model.js +++ b/addons/mail/static/src/core/common/chat_window_model.js @@ -8,7 +8,7 @@ import { _t } from "@web/core/l10n/translation"; /** @typedef {{ thread?: import("@mail/core/common/thread_model").Thread, folded?: boolean, replaceNewMessageChatWindow?: boolean }} ChatWindowData */ export class ChatWindow extends Record { - static id = "threadLocalId"; + static id = "thread"; /** @type {ChatWindow[]} */ static records = []; /** @returns {ChatWindow} */ @@ -24,7 +24,7 @@ export class ChatWindow extends Record { * @returns {ChatWindow} */ static insert(data = {}) { - const chatWindow = this.records.find((c) => c.threadLocalId === data.thread?.localId); + const chatWindow = this.records.find((c) => c.thread?.eq(data.thread)); if (!chatWindow) { const chatWindow = this.new(data); Object.assign(chatWindow, { thread: data.thread }); @@ -57,20 +57,11 @@ export class ChatWindow extends Record { return chatWindow; } - /** @type {import("@mail/core/common/thread_model").Thread.localId} */ - threadLocalId; + thread = Record.one("Thread"); autofocus = 0; folded = false; hidden = false; - get thread() { - return this._store.Thread.records[this.threadLocalId]; - } - - set thread(thread) { - this.threadLocalId = thread?.localId; - } - get displayName() { return this.thread?.displayName ?? _t("New message"); } diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index 076200c2a6c..5891b0f8156 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -7,7 +7,7 @@ import { OR, Record } from "@mail/core/common/record"; */ export class Composer extends Record { - static id = OR("threadLocalId", "messageLocalId"); + static id = OR("thread", "message"); /** @returns {Composer} */ static new(data) { return super.new(data); @@ -57,8 +57,7 @@ export class Composer extends Record { /** @type {import("@mail/core/common/attachment_model").Attachment[]} */ attachments = []; - /** @type {import("@mail/core/common/message_model").Message.localId} */ - messageLocalId; + message = Record.one("Message"); /** @type {RawMentions} */ rawMentions = { partnerIds: new Set(), @@ -68,8 +67,7 @@ export class Composer extends Record { cannedResponseIds = new Set(); /** @type {string} */ textInputContent; - /** @type {import("@mail/core/common/thread_model").Thread.localId} */ - threadLocalId; + thread = Record.one("Thread"); /** @type {{ start: number, end: number, direction: "forward" | "backward" | "none"}}*/ selection = { start: 0, @@ -79,26 +77,6 @@ export class Composer extends Record { /** @type {boolean} */ forceCursorMove; isFocused = false; - - /** @type {import("@mail/core/common/message_model").Message} */ - get message() { - return this._store.Message.records[this.messageLocalId]; - } - - /** @param {import("@mail/core/common/message_model").Message} */ - set message(newMessage) { - this.messageLocalId = newMessage?.localId; - } - - /** @type {import("@mail/core/common/thread_model").Thread} */ - get thread() { - return this._store.Thread.records[this.threadLocalId]; - } - - /** @param {import("@mail/core/common/thread_model").Thread} */ - set thread(newThread) { - this.threadLocalId = newThread?.localId; - } } Composer.register(); diff --git a/addons/mail/static/src/core/common/follower_model.js b/addons/mail/static/src/core/common/follower_model.js index 4f4a73fe77a..6e95d63e6a8 100644 --- a/addons/mail/static/src/core/common/follower_model.js +++ b/addons/mail/static/src/core/common/follower_model.js @@ -37,14 +37,12 @@ export class Follower extends Record { return follower; } - /** @type {import("@mail/core/common/thread_model").Thread} */ - followedThread; + followedThread = Record.one("Thread"); /** @type {number} */ id; /** @type {boolean} */ isActive; - /** @type {import("@mail/core/common/persona_model").Persona} */ - partner; + partner = Record.one("Persona"); /** * @returns {boolean} diff --git a/addons/mail/static/src/core/common/link_preview_confirm_delete.js b/addons/mail/static/src/core/common/link_preview_confirm_delete.js index 134347fd46a..1a23a1c6c9f 100644 --- a/addons/mail/static/src/core/common/link_preview_confirm_delete.js +++ b/addons/mail/static/src/core/common/link_preview_confirm_delete.js @@ -23,7 +23,7 @@ export class LinkPreviewConfirmDelete extends Component { } get message() { - return this.store.Message.get(this.props.linkPreview.message_id); + return this.props.linkPreview.message; } onClickOk() { 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 d37dc88d2fe..18a9696b35a 100644 --- a/addons/mail/static/src/core/common/link_preview_model.js +++ b/addons/mail/static/src/core/common/link_preview_model.js @@ -18,6 +18,8 @@ export class LinkPreview extends Record { */ static insert(data) { 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); @@ -30,8 +32,7 @@ export class LinkPreview extends Record { /** @type {number} */ id; - /** @type {number} */ - message_id; + message = Record.one("Message"); /** @type {string} */ image_mimetype; /** @type {string} */ diff --git a/addons/mail/static/src/core/common/message_model.js b/addons/mail/static/src/core/common/message_model.js index 4887bbb6dee..c802fb71ff7 100644 --- a/addons/mail/static/src/core/common/message_model.js +++ b/addons/mail/static/src/core/common/message_model.js @@ -42,8 +42,7 @@ export class Message extends Record { /** @type {Object[]} */ attachments = []; - /** @type {import("@mail/core/common/persona_model").Persona} */ - author; + author = Record.one("Persona"); /** @type {string} */ body; /** @type {string} */ @@ -64,8 +63,7 @@ export class Message extends Record { needaction_partner_ids = []; /** @type {number[]} */ history_partner_ids = []; - /** @type {Message|undefined} */ - parentMessage; + parentMessage = Record.one("Message"); /** @type {MessageReactions[]} */ reactions = []; /** @type {import("@mail/core/common/notification_model").Notification[]} */ 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 d46d3ec88f1..b568a42deeb 100644 --- a/addons/mail/static/src/core/common/message_reactions_model.js +++ b/addons/mail/static/src/core/common/message_reactions_model.js @@ -3,7 +3,7 @@ import { AND, Record } from "@mail/core/common/record"; export class MessageReactions extends Record { - static id = AND("messageId", "content"); + static id = AND("message", "content"); /** @returns {MessageReactions} */ static new(data) { return super.new(data); @@ -48,7 +48,7 @@ export class MessageReactions extends Record { Object.assign(reaction, { count: data.count, content: data.content, - messageId: data.message.id, + message: data.message, personaLocalIds: reaction.personaLocalIds.filter( (localId) => !personasToUnlink.has(localId) ), @@ -62,8 +62,7 @@ export class MessageReactions extends Record { count; /** @type {number[]} */ personaLocalIds = []; - /** @type {number} */ - messageId; + message = Record.one("Message"); /** @type {import("@mail/core/common/persona_model").Persona[]} */ get personas() { diff --git a/addons/mail/static/src/core/common/message_service.js b/addons/mail/static/src/core/common/message_service.js index 24d9b125c8a..0183085c111 100644 --- a/addons/mail/static/src/core/common/message_service.js +++ b/addons/mail/static/src/core/common/message_service.js @@ -182,7 +182,7 @@ export class MessageService { { action: "remove", content: reaction.content, - message_id: reaction.messageId, + message_id: reaction.message.id, }, { silent: true } ); @@ -284,7 +284,7 @@ export class MessageService { replaceArrayWithCompare( message.notifications, notifications.map((notification) => - this.store.Notification.insert({ ...notification, messageId: message.id }) + this.store.Notification.insert({ ...notification, message }) ) ); replaceArrayWithCompare( @@ -342,7 +342,7 @@ export class MessageService { updateNotification(notification, data) { Object.assign(notification, { - messageId: data.messageId, + message: data.message, notification_status: data.notification_status, notification_type: data.notification_type, failure_type: data.failure_type, @@ -390,10 +390,10 @@ export class MessageService { group.notifications = group.notifications.filter( ({ id }) => !notificationIdsToRemove.has(id) ); - group.lastMessageId = group.notifications[0]?.message.id; + group.lastMessage = group.notifications[0]?.message; for (const notification of group.notifications) { - if (group.lastMessageId < notification.message.id) { - group.lastMessageId = notification.message.id; + if (group.lastMessage?.id < notification.message?.id) { + group.lastMessage = notification.message; } } group.resIds.add(data.resId); 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 3211a805045..15f8830e86a 100644 --- a/addons/mail/static/src/core/common/notification_group_model.js +++ b/addons/mail/static/src/core/common/notification_group_model.js @@ -51,8 +51,7 @@ export class NotificationGroup extends Record { modelName; /** @type {string} */ resModel; - /** @type {number} */ - lastMessageId; + lastMessage = Record.one("Message"); /** @type {Set} */ resIds = new Set(); /** @type {'sms' | 'email'} */ @@ -66,10 +65,6 @@ export class NotificationGroup extends Record { return _t("An error occurred when sending an email"); } - get lastMessage() { - return this._store.Message.get(this.lastMessageId); - } - get datetime() { return this.lastMessage?.datetime; } diff --git a/addons/mail/static/src/core/common/notification_model.js b/addons/mail/static/src/core/common/notification_model.js index 28b46030e6f..42e3d820d87 100644 --- a/addons/mail/static/src/core/common/notification_model.js +++ b/addons/mail/static/src/core/common/notification_model.js @@ -29,20 +29,14 @@ export class Notification extends Record { /** @type {number} */ id; - /** @type {number} */ - messageId; + message = Record.one("Message"); /** @type {string} */ notification_status; /** @type {string} */ notification_type; /** @type {string} */ failure_type; - /** @type {import("@mail/core/common/persona_model").Persona} */ - persona; - - get message() { - return this._store.Message.get(this.messageId); - } + persona = Record.one("Persona"); get isFailure() { return ["exception", "bounce"].includes(this.notification_status); diff --git a/addons/mail/static/src/core/common/persona_model.js b/addons/mail/static/src/core/common/persona_model.js index 72e10517157..fc8574f7809 100644 --- a/addons/mail/static/src/core/common/persona_model.js +++ b/addons/mail/static/src/core/common/persona_model.js @@ -35,8 +35,6 @@ export class Persona extends Record { return persona; } - /** @type {string} */ - localId; /** @type {number} */ id; /** @type {boolean | undefined} */ diff --git a/addons/mail/static/src/core/common/persona_service.js b/addons/mail/static/src/core/common/persona_service.js index 8d3c04c8539..5d6cf610a3a 100644 --- a/addons/mail/static/src/core/common/persona_service.js +++ b/addons/mail/static/src/core/common/persona_service.js @@ -110,7 +110,7 @@ export class PersonaService { } return b.lastInterestDateTime.ts - a.lastInterestDateTime.ts; }) - .map((thread) => thread.chatPartnerId); + .map((thread) => thread.chatPartner?.id); } } diff --git a/addons/mail/static/src/core/common/record.js b/addons/mail/static/src/core/common/record.js index 2ab6de49bde..d0f984801fd 100644 --- a/addons/mail/static/src/core/common/record.js +++ b/addons/mail/static/src/core/common/record.js @@ -5,6 +5,7 @@ import { registry } from "@web/core/registry"; export const modelRegistry = registry.category("discuss.model"); +const ONE_SYM = Symbol("one"); const OR_SYM = Symbol("or"); const AND_SYM = Symbol("and"); @@ -31,7 +32,7 @@ export class Record { } static localId(data) { let idStr; - if (typeof data === "object") { + if (typeof data === "object" && data !== null) { idStr = this._localId(this.id, data); } else { idStr = data; // non-object data => single id @@ -40,6 +41,10 @@ export class Record { } static _localId(expr, data, { brackets = false } = {}) { if (!Array.isArray(expr)) { + if (this.Class.__rels__.has(expr)) { + // relational field (note: optional when OR) + return `(${data[expr]?.localId})`; + } return data[expr]; } const vals = []; @@ -79,13 +84,28 @@ export class Record { } return record; } - + /** + * @template {keyof import("model ").Models} M + * @param {M} modelName + * @returns {import("models").Models[M]} + */ + static one(modelName) { + return ONE_SYM; + } /** * @param {Object} data * @returns {Record} */ static insert(data) {} + /** + * Raw relational values of the record, each of which contains object id(s) + * rather than the record(s). This allows data in store and models being normalized, + * which eases handling relations notably in when a record gets deleted. + * + * @type {Map} + */ + __rels__ = new Map(); /** @type {import("@mail/core/common/store_service").Store} */ _store; /** diff --git a/addons/mail/static/src/core/common/store_service.js b/addons/mail/static/src/core/common/store_service.js index c6ab8ef2156..5a0b36f580c 100644 --- a/addons/mail/static/src/core/common/store_service.js +++ b/addons/mail/static/src/core/common/store_service.js @@ -7,7 +7,7 @@ import { reactive } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; -import { modelRegistry } from "./record"; +import { modelRegistry, Record } from "./record"; export class Store { /** @type {typeof import("@mail/core/web/activity_model").Activity} */ @@ -187,9 +187,43 @@ export const storeService = { // 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 }); - entry.Class = Model; + // Produce another class with changed prototype, so that there are automatic get/set on relational fields + const Class = { + [Model.name]: class extends Model { + static __rels__ = new Set(); + constructor() { + super(); + for (const name of this.constructor.__rels__) { + // Relational fields contain symbols for detection in original class. + // This constructor is called on genuine records: + // - 'one' fields => undefined + const newVal = undefined; + this[name] = newVal; + this.__rels__.set(name, newVal); + } + } + }, + }[Model.name]; + entry.Class = Class; entry.records = JSON.parse(JSON.stringify(Model.records)); res[name] = entry; + // Detect relational fields with a dummy record and setup getter/setters on them + const obj = new Model(); + for (const [name, val] of Object.entries(obj)) { + if (val !== Record.one()) { + continue; + } + Class.__rels__.add(name); + Object.defineProperty(Class.prototype, name, { + get() { + const localId = this.__rels__.get(name); + return res.get(localId); + }, + set(record) { + this.__rels__.set(name, record?.localId); + }, + }); + } } onChange(res.Thread, "records", () => res.updateBusSubscription()); services.ui.bus.addEventListener("resize", () => { diff --git a/addons/mail/static/src/core/common/thread_icon.js b/addons/mail/static/src/core/common/thread_icon.js index a7267c85ba0..abd326af2de 100644 --- a/addons/mail/static/src/core/common/thread_icon.js +++ b/addons/mail/static/src/core/common/thread_icon.js @@ -24,6 +24,6 @@ export class ThreadIcon extends Component { } get chatPartner() { - return this.store.Persona.get({ type: "partner", id: this.props.thread.chatPartnerId }); + return this.props.thread.chatPartner; } } diff --git a/addons/mail/static/src/core/common/thread_model.js b/addons/mail/static/src/core/common/thread_model.js index 57f400fa007..bf535570faf 100644 --- a/addons/mail/static/src/core/common/thread_model.js +++ b/addons/mail/static/src/core/common/thread_model.js @@ -101,21 +101,18 @@ export class Thread extends Record { areAttachmentsLoaded = false; /** @type {import("@mail/core/common/attachment_model").Attachment[]} */ attachments = []; - /** @type {integer} */ - activeRtcSessionId; + activeRtcSession = Record.one("RtcSession"); /** @type {object|undefined} */ channel; /** @type {import("@mail/core/common/channel_member_model").ChannelMember[]} */ channelMembers = []; /** @type {Object} */ rtcSessions = {}; - invitingRtcSessionId; + rtcInvitingSession = Record.one("RtcSession"); /** @type {Set} */ invitedMemberIds = new Set(); - /** @type {integer} */ - chatPartnerId; - /** @type {import("@mail/core/common/composer_model").Composer} */ - composer; + chatPartner = Record.one("Persona"); + composer = Record.one("Composer"); counter = 0; /** @type {string} */ customName; @@ -123,8 +120,7 @@ export class Thread extends Record { description; /** @type {Set} */ followers = new Set(); - /** @type {import("@mail/core/common/follower_model").Follower} */ - selfFollower; + selfFollower = Record.one("Follower"); /** @type {integer|undefined} */ followersCount; isAdmin = false; @@ -133,8 +129,7 @@ export class Thread extends Record { isLoadingAttachments = false; isLoadedDeferred = new Deferred(); isLoaded = false; - /** @type {import("@mail/core/common/attachment_model").Attachment} */ - mainAttachment; + mainAttachment = Record.one("Attachment"); memberCount = 0; message_needaction_counter = 0; message_unread_counter = 0; @@ -197,8 +192,7 @@ export class Thread extends Record { canPostOnReadonly; /** @type {String} */ last_interest_dt; - /** @type {number} */ - lastServerMessageId; + lastServerMessage = Record.one("Message"); /** @type {Boolean} */ is_editable; @@ -211,14 +205,6 @@ export class Thread extends Record { }); } - get activeRtcSession() { - return this._store.RtcSession.get(this.activeRtcSessionId); - } - - set activeRtcSession(session) { - this.activeRtcSessionId = session?.id; - } - get areAllMembersLoaded() { return this.memberCount === this.channelMembers.length; } @@ -276,12 +262,8 @@ export class Thread extends Record { } get displayName() { - if (this.type === "chat" && this.chatPartnerId) { - return ( - this.customName || - this._store.Persona.get({ type: "partner", id: this.chatPartnerId }) - .nameOrDisplayName - ); + if (this.type === "chat" && this.chatPartner) { + return this.customName || this.chatPartner.nameOrDisplayName; } if (this.type === "group" && !this.name) { const listFormatter = new Intl.ListFormat( @@ -471,10 +453,6 @@ export class Thread extends Record { return this.memberCount - this.channelMembers.length; } - get rtcInvitingSession() { - return this._store.RtcSession.get(this.invitingRtcSessionId); - } - get hasNeedactionMessages() { return this.needactionMessages.length > 0; } diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js index 5be8b53d8c5..0b7d2f8bc10 100644 --- a/addons/mail/static/src/core/common/thread_service.js +++ b/addons/mail/static/src/core/common/thread_service.js @@ -78,7 +78,7 @@ export class ThreadService { thread.memberCount = results["memberCount"]; for (const channelMember of channelMembers) { if (channelMember.persona || channelMember.partner) { - this.store.ChannelMember.insert({ ...channelMember, threadId: thread.id }); + this.store.ChannelMember.insert({ ...channelMember, thread }); } } } @@ -533,7 +533,7 @@ export class ThreadService { return; } return Object.values(this.store.Thread.records).find( - (thread) => thread.type === "chat" && thread.chatPartnerId === partner.id + (thread) => thread.type === "chat" && thread.chatPartner?.eq(partner) ); } @@ -707,7 +707,12 @@ export class ThreadService { 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; + const lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessage?.id; + if (thread.lastServerMessage?.id !== lastServerMessageId) { + thread.lastServerMessage = this.store.Message.insert({ + id: lastServerMessageId, + }); + } if (thread.model === "discuss.channel" && serverData.channel) { nullifyClearCommands(serverData.channel); thread.channel = assignDefined(thread.channel ?? {}, serverData.channel); @@ -730,7 +735,7 @@ export class ThreadService { (serverData.channel.channelMembers[0][1].length === 1 && member.persona?.eq(thread._store.user)) ) { - thread.chatPartnerId = member.persona.id; + thread.chatPartner = member.persona; } } } diff --git a/addons/mail/static/src/core/web/thread_service_patch.js b/addons/mail/static/src/core/web/thread_service_patch.js index 09b9bb4e38e..2ff8805239f 100644 --- a/addons/mail/static/src/core/web/thread_service_patch.js +++ b/addons/mail/static/src/core/web/thread_service_patch.js @@ -166,9 +166,7 @@ patch(ThreadService.prototype, { thread.suggestedRecipients = recipients; }, async leaveChannel(channel) { - const chatWindow = this.store.ChatWindow.records.find( - (c) => c.threadLocalId === channel.localId - ); + const chatWindow = this.store.ChatWindow.records.find((c) => c.thread?.eq(channel)); if (chatWindow) { this.chatWindowService.close(chatWindow); } @@ -249,9 +247,7 @@ patch(ThreadService.prototype, { follower.delete(); }, unpin(thread) { - const chatWindow = this.store.ChatWindow.records.find( - (c) => c.threadLocalId === thread.localId - ); + const chatWindow = this.store.ChatWindow.records.find((c) => c.thread?.eq(thread)); if (chatWindow) { this.chatWindowService.close(chatWindow); } diff --git a/addons/mail/static/src/discuss/call/common/rtc_service.js b/addons/mail/static/src/discuss/call/common/rtc_service.js index ddae9b97f23..5238e01e484 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_service.js +++ b/addons/mail/static/src/discuss/call/common/rtc_service.js @@ -290,7 +290,9 @@ export class Rtc { thread, record: data.rtc_inviting_session, }); - thread.invitingRtcSessionId = data.rtc_inviting_session.id; + thread.rtcInvitingSession = this.store.RtcSession.insert({ + id: data.rtc_inviting_session.id, + }); if (!this.store.ringingThreads.includes(thread.localId)) { this.store.ringingThreads.push(thread.localId); } @@ -298,7 +300,7 @@ export class Rtc { if ("rtcInvitingSession" in data) { if (Array.isArray(data.rtcInvitingSession)) { if (data.rtcInvitingSession[0][0] === "unlink") { - thread.invitingRtcSessionId = undefined; + thread.rtcInvitingSession = undefined; removeFromArray(this.store.ringingThreads, thread.localId); } return; @@ -307,7 +309,9 @@ export class Rtc { thread, record: data.rtcInvitingSession, }); - thread.invitingRtcSessionId = data.rtcInvitingSession.id; + thread.rtcInvitingSession = this.store.RtcSession.insert({ + id: data.rtcInvitingSession.id, + }); this.store.ringingThreads.push(thread.localId); } if ("rtcSessions" in data) { diff --git a/addons/mail/static/src/discuss/call/common/rtc_session_model.js b/addons/mail/static/src/discuss/call/common/rtc_session_model.js index 9fff093ad73..59fc621ba91 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_session_model.js +++ b/addons/mail/static/src/discuss/call/common/rtc_session_model.js @@ -29,7 +29,7 @@ export class RtcSession extends Record { } if (channelMember) { const channelMemberRecord = this.store.ChannelMember.insert(channelMember); - channelMemberRecord.rtcSessionId = session.id; + channelMemberRecord.rtcSession = session; session.channelMemberId = channelMemberRecord.id; if (channelMemberRecord.thread) { channelMemberRecord.thread.rtcSessions[session.id] = session; diff --git a/addons/mail/static/src/discuss/core/common/channel_invitation.js b/addons/mail/static/src/discuss/core/common/channel_invitation.js index 135338e1182..c0046d68309 100644 --- a/addons/mail/static/src/discuss/core/common/channel_invitation.js +++ b/addons/mail/static/src/discuss/core/common/channel_invitation.js @@ -103,7 +103,7 @@ export class ChannelInvitation extends Component { async onClickInvite() { if (this.props.thread.type === "chat") { await this.discussCoreCommonService.startChat([ - this.props.thread.chatPartnerId, + this.props.thread.chatPartner?.id, ...this.state.selectedPartners.map((partner) => partner.id), ]); } else { @@ -120,13 +120,13 @@ export class ChannelInvitation extends Component { } else if (this.props.thread.type === "group") { return _t("Invite to Group Chat"); } else if (this.props.thread.type === "chat") { - if (this.props.thread.chatPartnerId === this.store.self.id) { + if (this.props.thread.chatPartner?.eq(this.store.self)) { if (this.state.selectedPartners.length === 0) { return _t("Invite"); } if (this.state.selectedPartners.length === 1) { - const alreadyChat = Object.values(this.store.Thread.records).some( - (thread) => thread.chatPartnerId === this.state.selectedPartners[0].id + const alreadyChat = Object.values(this.store.Thread.records).some((thread) => + thread.chatPartner?.eq(this.state.selectedPartners[0]) ); if (alreadyChat) { return _t("Go to conversation"); diff --git a/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js b/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js index 37c3a707257..3bab9b66963 100644 --- a/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js +++ b/addons/mail/static/src/discuss/core/common/discuss_core_common_service.js @@ -269,7 +269,7 @@ export class DiscussCoreCommon { } } } - if (channel.chatPartnerId !== this.store.odoobot?.id) { + if (!channel.chatPartner?.eq(this.store.odoobot)) { if ( !this.presence.isOdooFocused() && channel.isChatChannel && diff --git a/addons/mail/static/src/discuss/core/common/thread_model_patch.js b/addons/mail/static/src/discuss/core/common/thread_model_patch.js index 8b12c3f76d9..787642e52d4 100644 --- a/addons/mail/static/src/discuss/core/common/thread_model_patch.js +++ b/addons/mail/static/src/discuss/core/common/thread_model_patch.js @@ -15,7 +15,7 @@ patch(Thread.prototype, { ); } if (this.type === "chat") { - return `/web/image/res.partner/${this.chatPartnerId}/avatar_128`; + return `/web/image/res.partner/${this.chatPartner.id}/avatar_128`; } return super.imgUrl; }, diff --git a/addons/mail/static/src/discuss/core/web/command_palette.js b/addons/mail/static/src/discuss/core/web/command_palette.js index 3015f2a90d1..e3170bfdb97 100644 --- a/addons/mail/static/src/discuss/core/web/command_palette.js +++ b/addons/mail/static/src/discuss/core/web/command_palette.js @@ -44,14 +44,14 @@ commandProviderRegistry.add("mail.partner", { if (!options.searchValue) { mentionedChannels.slice(0, 3).map((channel) => { if (channel.type === "chat") { - displayedPartnerIds.add(channel.chatPartnerId); + displayedPartnerIds.add(channel.chatPartner.id); } commands.push({ Component: DiscussCommand, async action() { switch (channel.type) { case "chat": - threadService.openChat({ partnerId: channel.chatPartnerId }); + threadService.openChat({ partnerId: channel.chatPartner.id }); break; case "group": threadService.open(channel); diff --git a/addons/mail/static/src/discuss/core/web/discuss_core_web_service.js b/addons/mail/static/src/discuss/core/web/discuss_core_web_service.js index af9a855930c..53d09bc885b 100644 --- a/addons/mail/static/src/discuss/core/web/discuss_core_web_service.js +++ b/addons/mail/static/src/discuss/core/web/discuss_core_web_service.js @@ -90,8 +90,8 @@ export class DiscussCoreWeb { if (data.serverFoldState && thread && data.serverFoldState !== thread.state) { thread.state = data.serverFoldState; if (thread.state === "closed") { - const chatWindow = this.store.ChatWindow.records.find( - (chatWindow) => chatWindow.threadLocalId === thread.localId + const chatWindow = this.store.ChatWindow.records.find((chatWindow) => + chatWindow.thread?.eq(thread) ); if (chatWindow) { this.chatWindowService.close(chatWindow); diff --git a/addons/mail/tooling/types/models.d.ts b/addons/mail/tooling/types/models.d.ts new file mode 100644 index 00000000000..f71b13b9af9 --- /dev/null +++ b/addons/mail/tooling/types/models.d.ts @@ -0,0 +1,20 @@ +/* add this file in jsconfig.json, in typeRoots array */ +declare module "models" { + import { Attachment } from "@mail/core/common/attachment_model"; + import { Composer } from "@mail/core/common/composer_model"; + import { Follower } from "@mail/core/common/follower_model"; + import { Message } from "@mail/core/common/message_model"; + import { Persona } from "@mail/core/common/persona_model"; + import { RtcSession } from "@mail/discuss/call/common/rtc_session_model"; + import { Thread } from "@mail/core/common/thread_model"; + + export interface Models { + "Attachment": Attachment, + "Composer": Composer, + "Follower": Follower, + "Message": Message, + "Persona": Persona, + "RtcSession": RtcSession, + "Thread": Thread, + } +}