diff --git a/addons/mail/static/src/core/common/attachment_model.js b/addons/mail/static/src/core/common/attachment_model.js index a78c0c45400..17d7bbb30c9 100644 --- a/addons/mail/static/src/core/common/attachment_model.js +++ b/addons/mail/static/src/core/common/attachment_model.js @@ -8,19 +8,19 @@ import { url } from "@web/core/utils/urls"; export class Attachment extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Attachment} */ + /** @returns {import("models").Models["Attachment"]} */ static new(data) { return super.new(data); } - /** @returns {Attachment} */ + /** @returns {import("models").Models["Attachment"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {Attachment} + * @returns {import("models").Models["Attachment"]} */ static insert(data) { if (!("id" in data)) { diff --git a/addons/mail/static/src/core/common/attachment_service.js b/addons/mail/static/src/core/common/attachment_service.js index 6add0e631d2..6374e8cdbcc 100644 --- a/addons/mail/static/src/core/common/attachment_service.js +++ b/addons/mail/static/src/core/common/attachment_service.js @@ -1,6 +1,5 @@ /* @odoo-module */ -import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays"; import { assignDefined } from "@mail/utils/common/misc"; import { registry } from "@web/core/registry"; @@ -65,22 +64,6 @@ export class AttachmentService { URL.revokeObjectURL(attachment.tmpUrl); } attachment.delete(); - if (attachment.originThread) { - removeFromArrayWithPredicate(attachment.originThread.attachments, (att) => - att.eq(attachment) - ); - } - for (const message of Object.values(this.store.Message.records)) { - removeFromArrayWithPredicate(message.attachments, (att) => att.eq(attachment)); - if (message.composer) { - removeFromArrayWithPredicate(message.composer.attachments, (att) => - att.eq(attachment) - ); - } - } - for (const thread of Object.values(this.store.Thread.records)) { - removeFromArrayWithPredicate(thread.composer.attachments, (att) => att.eq(attachment)); - } } /** 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 c4bc0ec7d52..c10cac9172c 100644 --- a/addons/mail/static/src/core/common/canned_response_model.js +++ b/addons/mail/static/src/core/common/canned_response_model.js @@ -4,19 +4,19 @@ import { Record } from "@mail/core/common/record"; export class CannedResponse extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {CannedResponse} */ + /** @returns {import("models").Models["CannedResponse"]} */ static new(data) { return super.new(data); } - /** @returns {CannedResponse} */ + /** @returns {import("models").Models["CannedResponse"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {CannedResponse} + * @returns {import("models").Models["CannedResponse"]} */ static insert(data) { const cannedResponse = this.get(data) ?? this.new(data); 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 1ec3dc78cf5..73b65fcefdd 100644 --- a/addons/mail/static/src/core/common/channel_member_model.js +++ b/addons/mail/static/src/core/common/channel_member_model.js @@ -11,19 +11,19 @@ import { Record } from "@mail/core/common/record"; */ export class ChannelMember extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {ChannelMember} */ + /** @returns {import("models").Models["ChannelMember"]} */ static new(data) { return super.new(data); } - /** @returns {ChannelMember} */ + /** @returns {import("models").Models["ChannelMember"]} */ static get(data) { return super.get(data); } /** * @param {Object|Array} data - * @returns {ChannelMember} + * @returns {import("models").Models["ChannelMember"]} */ static insert(data) { const memberData = Array.isArray(data) ? data[1] : data; 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 a62df699c90..66ab0c0af5a 100644 --- a/addons/mail/static/src/core/common/chat_window_model.js +++ b/addons/mail/static/src/core/common/chat_window_model.js @@ -9,19 +9,19 @@ import { _t } from "@web/core/l10n/translation"; export class ChatWindow extends Record { static id = "thread"; - /** @type {ChatWindow[]} */ + /** @type {import("models").Models["ChatWindow"][]} */ static records = []; - /** @returns {ChatWindow} */ + /** @returns {import("models").Models["ChatWindow"]} */ static new(data) { return super.new(data); } - /** @returns {ChatWindow} */ + /** @returns {import("models").Models["ChatWindow"]} */ static get(data) { return super.get(data); } /** * @param {ChatWindowData} [data] - * @returns {ChatWindow} + * @returns {import("models").Models["ChatWindow"]} */ static insert(data = {}) { const chatWindow = this.records.find((c) => c.thread?.eq(data.thread)); diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index e576fd290da..1df4f160f00 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -8,17 +8,17 @@ import { OR, Record } from "@mail/core/common/record"; export class Composer extends Record { static id = OR("thread", "message"); - /** @returns {Composer} */ + /** @returns {import("models").Models["Composer"]} */ static new(data) { return super.new(data); } - /** @returns {Composer} */ + /** @returns {import("models").Models["Composer"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {Composer} + * @returns {import("models").Models["Composer"]} */ static insert(data) { const { message, thread } = data; diff --git a/addons/mail/static/src/core/common/follower_model.js b/addons/mail/static/src/core/common/follower_model.js index 6e95d63e6a8..321daaa481e 100644 --- a/addons/mail/static/src/core/common/follower_model.js +++ b/addons/mail/static/src/core/common/follower_model.js @@ -12,19 +12,19 @@ import { Record } from "@mail/core/common/record"; export class Follower extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Follower} */ + /** @returns {import("models").Models["Follower"]} */ static new(data) { return super.new(data); } - /** @returns {Follower} */ + /** @returns {import("models").Models["Follower"]} */ static get(data) { return super.get(data); } /** * @param {Data} data - * @returns {Follower} + * @returns {import("models").Models["Follower"]} */ static insert(data) { const follower = this.get(data) ?? this.new(data); 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 18a9696b35a..c8fb7957e61 100644 --- a/addons/mail/static/src/core/common/link_preview_model.js +++ b/addons/mail/static/src/core/common/link_preview_model.js @@ -4,17 +4,17 @@ import { Record } from "@mail/core/common/record"; export class LinkPreview extends Record { static id = "id"; - /** @returns {LinkPreview} */ + /** @returns {import("models").Models["LinkPreview"]} */ static new(data) { return super.new(data); } - /** @returns {LinkPreview} */ + /** @returns {import("models").Models["LinkPreview"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {LinkPreview} + * @returns {import("models").Models["LinkPreview"]} */ static insert(data) { const message = this.store.Message.get(data.message_id); diff --git a/addons/mail/static/src/core/common/mail_core_common_service.js b/addons/mail/static/src/core/common/mail_core_common_service.js index 1acfe607acd..d77488dadfe 100644 --- a/addons/mail/static/src/core/common/mail_core_common_service.js +++ b/addons/mail/static/src/core/common/mail_core_common_service.js @@ -50,11 +50,6 @@ export class MailCoreCommon { continue; } message.delete(); - if (message.originThread) { - removeFromArrayWithPredicate(message.originThread.messages, (msg) => - msg.eq(message) - ); - } this.env.bus.trigger("mail.message/delete", { message }); } }); diff --git a/addons/mail/static/src/core/common/message_model.js b/addons/mail/static/src/core/common/message_model.js index 1b40a2b9cc4..92568181ab1 100644 --- a/addons/mail/static/src/core/common/message_model.js +++ b/addons/mail/static/src/core/common/message_model.js @@ -14,19 +14,19 @@ const { DateTime } = luxon; export class Message extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Message} */ + /** @returns {import("models").Models["Message"]} */ static new(data) { return super.new(data); } - /** @returns {Message} */ + /** @returns {import("models").Models["Message"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {Message} + * @returns {import("models").Models["Message"]} */ static insert(data) { if (data.res_id) { 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 104d21b3da9..1c40f30b7a0 100644 --- a/addons/mail/static/src/core/common/message_reactions_model.js +++ b/addons/mail/static/src/core/common/message_reactions_model.js @@ -4,17 +4,17 @@ import { AND, Record } from "@mail/core/common/record"; export class MessageReactions extends Record { static id = AND("message", "content"); - /** @returns {MessageReactions} */ + /** @returns {import("models").Models["MessageReactions"]} */ static new(data) { return super.new(data); } - /** @returns {MessageReactions} */ + /** @returns {import("models").Models["MessageReactions"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {MessageReactions} + * @returns {import("models").Models["MessageReactions"]} */ static insert(data) { let reaction = this.store.Message.get(data.message.id)?.reactions.find( @@ -48,7 +48,7 @@ export class MessageReactions extends Record { Object.assign(reaction, { count: data.count, content: data.content, - message: data.message, + message: this.store.Message.insert(data.message), personas: reaction.personas.filter((p) => !personasToUnlink.has(p)), }); return reaction; 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 dccc4661825..10834557c9d 100644 --- a/addons/mail/static/src/core/common/notification_group_model.js +++ b/addons/mail/static/src/core/common/notification_group_model.js @@ -8,19 +8,19 @@ import { _t } from "@web/core/l10n/translation"; let nextId = 1; export class NotificationGroup extends Record { static id = "id"; - /** @type {NotificationGroup[]} */ + /** @type {import("models").Models["NotificationGroup"][]} */ static records = []; - /** @returns {NotificationGroup} */ + /** @returns {import("models").Models["NotificationGroup"]} */ static new(data) { return super.new(data); } - /** @returns {NotificationGroup} */ + /** @returns {import("models").Models["NotificationGroup"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {NotificationGroup} + * @returns {import("models").Models["NotificationGroup"]} */ static insert(data) { let group = this.records.find((group) => { diff --git a/addons/mail/static/src/core/common/notification_model.js b/addons/mail/static/src/core/common/notification_model.js index 42e3d820d87..d3c14edf779 100644 --- a/addons/mail/static/src/core/common/notification_model.js +++ b/addons/mail/static/src/core/common/notification_model.js @@ -6,19 +6,19 @@ import { _t } from "@web/core/l10n/translation"; export class Notification extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Notification} */ + /** @returns {import("models").Models["Notification"]} */ static new(data) { return super.new(data); } - /** @returns {Notification} */ + /** @returns {import("models").Models["Notification"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {Notification} + * @returns {import("models").Models["Notification"]} */ static insert(data) { const notification = this.get(data) ?? this.new(data); diff --git a/addons/mail/static/src/core/common/persona_model.js b/addons/mail/static/src/core/common/persona_model.js index fc8574f7809..627c4e58fd5 100644 --- a/addons/mail/static/src/core/common/persona_model.js +++ b/addons/mail/static/src/core/common/persona_model.js @@ -14,19 +14,19 @@ import { AND, Record } from "@mail/core/common/record"; export class Persona extends Record { static id = AND("type", "id"); - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Persona} */ + /** @returns {import("models").Models["Persona"]} */ static new(data) { return super.new(data); } - /** @returns {Persona} */ + /** @returns {import("models").Models["Persona"]} */ static get(data) { return super.get(data); } /** * @param {Data} data - * @returns {Persona} + * @returns {import("models").Models["Persona"]} */ static insert(data) { const persona = this.get(data) ?? this.new(data); diff --git a/addons/mail/static/src/core/common/record.js b/addons/mail/static/src/core/common/record.js index d6d49543c0d..ba2fbd29327 100644 --- a/addons/mail/static/src/core/common/record.js +++ b/addons/mail/static/src/core/common/record.js @@ -148,14 +148,14 @@ export class RecordList extends Array { return this.__list__.length; } /** - * @param {Function} func + * @param {(a: R, b: R) => boolean} func * @returns {R[]} */ map(func) { return this.__list__.map((localId) => func(this.__store__.get(localId))); } /** - * @param {Function} predicate + * @param {(a: R, b: R) => boolean} predicate * @returns {R[]} */ filter(predicate) { @@ -163,16 +163,16 @@ export class RecordList extends Array { .filter((localId) => predicate(this.__store__.get(localId))) .map((localId) => this.__store__.get(localId)); } - /** @param {Function} predicate */ + /** @param {(a: R, b: R) => boolean} predicate */ some(predicate) { return this.__list__.some((localId) => predicate(this.__store__.get(localId))); } - /** @param {Function} predicate */ + /** @param {(a: R, b: R) => boolean} predicate */ every(predicate) { return this.__list__.every((localId) => predicate(this.__store__.get(localId))); } /** - * @param {Function} predicate + * @param {(a: R, b: R) => boolean} predicate * @returns {R} */ find(predicate) { @@ -180,7 +180,7 @@ export class RecordList extends Array { this.__list__.find((localId) => predicate(this.__store__.get(localId))) ); } - /** @param {Function} predicate */ + /** @param {(a: R, b: R) => boolean} predicate */ findIndex(predicate) { return this.__list__.findIndex((localId) => predicate(this.__store__.get(localId))); } @@ -215,7 +215,7 @@ export class RecordList extends Array { this.__addInverse__(r); } } - /** @param {Function} func */ + /** @param {(a: R, b: R) => boolean} func */ sort(func) { this.__list__.sort((a, b) => func(this.__store__.get(a), this.__store__.get(b))); } @@ -225,6 +225,7 @@ export class RecordList extends Array { .map((localId) => this.__store__.get(localId)) .concat(...collections.map((c) => [...c])); } + /** @yields {R} */ *[Symbol.iterator]() { for (const localId of this.__list__) { yield this.__store__.get(localId); @@ -275,16 +276,19 @@ export class RecordSet extends Set { has(record) { return this.__set__.has(record?.localId); } + /** @yields {R} */ *values() { for (const localId of this.__set__) { yield this.__store__.get(localId); } } + /** @yields {R} */ *keys() { for (const localId of this.__set__) { yield this.__store__.get(localId); } } + /** @yields {R} */ *[Symbol.iterator]() { for (const localId of this.__set__) { yield this.__store__.get(localId); @@ -454,6 +458,12 @@ export class Record { /** @type {string} */ localId; + constructor() { + this.setup(); + } + + setup() {} + delete() { const r1 = this; for (const [name, l1] of r1.__rels__.entries()) { @@ -468,6 +478,11 @@ export class Record { for (const [localId, names] of r1.__invs__.__map__.entries()) { for (const [name2, count] of names.entries()) { const r2 = this._store.get(localId); + if (!r2) { + // record already deleted, clean inverses + r1.__invs__.__map__.delete(localId); + continue; + } const l2 = r2.__rels__.get(name2); if (l2 instanceof RecordList) { for (let c = 0; c < count; c++) { diff --git a/addons/mail/static/src/core/common/store_service.js b/addons/mail/static/src/core/common/store_service.js index c0d51ac5d94..2b2e1aab355 100644 --- a/addons/mail/static/src/core/common/store_service.js +++ b/addons/mail/static/src/core/common/store_service.js @@ -205,11 +205,15 @@ export const storeService = { // instance props. const entry = Object.assign(Object.create(Model), { env, store: res }); // 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(); constructor() { super(); + if (detecting) { + return; + } for (const name of this.constructor.__rels__) { // Relational fields contain symbols for detection in original class. // This constructor is called on genuine records: @@ -230,10 +234,100 @@ export const storeService = { newVal.name = name; newVal.owner = this; } - this[name] = newVal; this.__rels__.set(name, newVal); this.__invs__ = new RecordInverses(); + this[name] = newVal; } + return new Proxy(this, { + /** @param {Record} receiver */ + get(target, name, receiver) { + if (name !== "__rels__" && receiver.__rels__.has(name)) { + const l1 = receiver.__rels__.get(name); + if (l1 instanceof RecordList || l1 instanceof RecordSet) { + return l1; + } + return res.get(l1); + } + return Reflect.get(target, name, receiver); + }, + deleteProperty(target, key) { + if (name !== "__rels__" && target.__rels__.has(name)) { + const r1 = target; + const l1 = r1.__rels__.get(name); + const r2 = res.get(l1); + if (r2) { + r2.__invs__.delete(r1.localId, name); + } + } + const ret = Reflect.deleteProperty(target, key); + return ret; + }, + /** @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|RecordSet} */ + 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) { + l1.__list__.push(r3.localId); + r3.__invs__.add(r1.localId, name); + } + } else if (oldVal instanceof RecordSet) { + const r1 = receiver; + /** @type {RecordSet} */ + const l1 = r1.__rels__.get(name); + /** @type {Record[]|Set|RecordList|RecordSet} */ + const collection = val; + const oldRecords = new Set(); + const newRecords = new Set(); + for (const r of collection) { + if (!l1.__set__.has(r?.localId)) { + newRecords.add(r); + } + } + for (const r of r1) { + if (r.notIn(oldRecords)) { + oldRecords.add(r); + } + } + for (const r of oldRecords) { + r1.delete(r); + } + for (const r of newRecords) { + r1.add(r); + } + } else { + const r1 = receiver; + const l1 = r1.__rels__.get(name); + const r2 = res.get(l1); + /** @type {Record} */ + const r3 = val; + if (r2 && r2.notEq(r3)) { + r2.__invs__.delete(r1.localId, name); + } + r1.__rels__.set(name, r3?.localId); + if (r3) { + if (!(r3 instanceof Record)) { + return true; // not a record, ignored + } + r3.__invs__.add(r1.localId, name); + } + } + } else { + Reflect.set(target, name, val, receiver); + } + return true; + }, + }); } }, }[Model.name]; @@ -242,87 +336,12 @@ export const storeService = { res[name] = entry; // Detect relational fields with a dummy record and setup getter/setters on them const obj = new Model(); + detecting = false; for (const [name, val] of Object.entries(obj)) { if (![Record.one(), Record.List(), Record.Set()].includes(val)) { continue; } Class.__rels__.add(name); - if (val === Record.one()) { - Object.defineProperty(Class.prototype, name, { - /** @this {Record} */ - get() { - return res.get(this.__rels__.get(name).value); - }, - /** - * @this {Record} - * @param {Record} r3 - */ - set(r3) { - const r1 = this; - const l1 = r1.__rels__.get(name); - const r2 = res.get(l1.value); - if (r2 && r2.notEq(r3)) { - r2.__invs__.delete(r1.localId, name); - } - l1.value = r3?.localId; - if (r3) { - r3.__invs__.add(r1.localId, name); - } - }, - }); - } - if (val === Record.List()) { - Object.defineProperty(Class.prototype, name, { - /** - * @this {Record} - * @param {Record[]|Set|RecordList|RecordSet} collection - */ - set(collection) { - const r1 = this; - /** @type {RecordList} */ - const l1 = r1.__rels__.get(name).value; - const oldRecords = l1.slice(); - l1.__list__ = []; - for (const r2 of oldRecords) { - r2.__invs__.delete(r1.localId, name); - } - for (const r3 of collection) { - l1.__list__.push(r3.localId); - r3.__invs__.add(r1.localId, name); - } - }, - }); - } - if (val === Record.Set()) { - Object.defineProperty(Class.prototype, name, { - /** - * @this {Record} - * @param {Record[]|Set|RecordList|RecordSet} collection - */ - set(collection) { - /** @type {RecordSet} */ - const l1 = this.__rels__.get(name).value; - const oldRecords = new Set(); - const newRecords = new Set(); - for (const r of collection) { - if (!l1.__set__.has(r?.localId)) { - newRecords.add(r); - } - } - for (const r of this) { - if (r.notIn(oldRecords)) { - oldRecords.add(r); - } - } - for (const r of oldRecords) { - this.delete(r); - } - for (const r of newRecords) { - this.add(r); - } - }, - }); - } } } onChange(res.Thread, "records", () => res.updateBusSubscription()); diff --git a/addons/mail/static/src/core/common/thread_model.js b/addons/mail/static/src/core/common/thread_model.js index 81a340e48fa..f50cefbc77d 100644 --- a/addons/mail/static/src/core/common/thread_model.js +++ b/addons/mail/static/src/core/common/thread_model.js @@ -23,18 +23,18 @@ import { Deferred } from "@web/core/utils/concurrency"; export class Thread extends Record { static id = AND("model", "id"); - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Thread} */ + /** @returns {import("models").Models["Thread"]} */ static new(data) { return super.new(data); } - /** @returns {Thread} */ + /** @returns {import("models").Models["Thread"]} */ static get(data) { return super.get(data); } /** - * @param {Thread.localId} localId + * @param {string} localId * @returns {string} */ static localIdToActiveId(localId) { @@ -46,7 +46,7 @@ export class Thread extends Record { } /** * @param {Object} data - * @returns {Thread} + * @returns {import("models").Models["Thread"]} */ static insert(data) { if (!("id" in data)) { diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js index 0b7d2f8bc10..6fead89aa62 100644 --- a/addons/mail/static/src/core/common/thread_service.js +++ b/addons/mail/static/src/core/common/thread_service.js @@ -2,11 +2,7 @@ import { loadEmoji } from "@web/core/emoji_picker/emoji_picker"; import { DEFAULT_AVATAR } from "@mail/core/common/persona_service"; -import { - removeFromArray, - removeFromArrayWithPredicate, - replaceArrayWithCompare, -} from "@mail/utils/common/arrays"; +import { removeFromArray, replaceArrayWithCompare } from "@mail/utils/common/arrays"; import { prettifyMessageContent } from "@mail/utils/common/format"; import { assignDefined, nullifyClearCommands } from "@mail/utils/common/misc"; @@ -868,10 +864,7 @@ export class ThreadService { thread.seen_message_id = tmpMsg.id; } const data = await this.rpc(this.getMessagePostRoute(thread), params); - if (thread.type !== "chatter") { - removeFromArrayWithPredicate(thread.messages, (msg) => msg.eq(tmpMsg)); - tmpMsg.delete(); - } + tmpMsg?.delete(); if (!data) { return; } diff --git a/addons/mail/static/src/core/web/activity_model.js b/addons/mail/static/src/core/web/activity_model.js index 3870ef76cc9..5cfbff660ff 100644 --- a/addons/mail/static/src/core/web/activity_model.js +++ b/addons/mail/static/src/core/web/activity_model.js @@ -35,13 +35,13 @@ import { assignDefined } from "@mail/utils/common/misc"; export class Activity extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {Activity} */ + /** @returns {import("models").Models["Activity"]} */ static new(data) { return super.new(data); } - /** @returns {Activity} */ + /** @returns {import("models").Models["Activity"]} */ static get(data) { return super.get(data); } @@ -49,7 +49,7 @@ export class Activity extends Record { * @param {Data} data * @param {Object} [param1] * @param {boolean} param1.broadcast - * @returns {Activity} + * @returns {import("models").Models["Activity"]} */ static insert(data, { broadcast = true } = {}) { const activity = this.get(data) ?? this.new(data); diff --git a/addons/mail/static/src/core/web/mail_core_web_service.js b/addons/mail/static/src/core/web/mail_core_web_service.js index 176ba71394a..5c3ffd11345 100644 --- a/addons/mail/static/src/core/web/mail_core_web_service.js +++ b/addons/mail/static/src/core/web/mail_core_web_service.js @@ -47,27 +47,11 @@ export class MailCoreWeb { }); this.env.bus.addEventListener("mail.message/delete", ({ detail: { message } }) => { if (message.isNeedaction) { - removeFromArrayWithPredicate( - this.store.discuss.inbox.messages, - ({ id }) => id === message.id - ); this.store.discuss.inbox.counter--; } if (message.isStarred) { - removeFromArrayWithPredicate( - this.store.discuss.starred.messages, - ({ id }) => id === message.id - ); this.store.discuss.starred.counter--; } - if (message.originThread) { - if (message.isNeedaction) { - removeFromArrayWithPredicate( - message.originThread.needactionMessages, - ({ id }) => id === message.id - ); - } - } }); this.busService.subscribe("mail.message/inbox", (payload) => { const data = Object.assign(payload, { body: markup(payload.body) }); diff --git a/addons/mail/static/src/core/web/messaging_menu.js b/addons/mail/static/src/core/web/messaging_menu.js index ee61ce18190..91929ac312b 100644 --- a/addons/mail/static/src/core/web/messaging_menu.js +++ b/addons/mail/static/src/core/web/messaging_menu.js @@ -298,7 +298,7 @@ export class MessagingMenu extends Component { (thread) => thread.is_pinned && thread.message_unread_counter > 0 ).length + Object.values(this.store.NotificationGroup.records).reduce( - (acc, ng) => acc + parseInt(Object.values(ng.notifications).length), + (acc, ng) => acc + parseInt(ng.notifications.length), 0 ); if (this.notification.permission === "prompt") { 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 2ff8805239f..4e6aa718fa2 100644 --- a/addons/mail/static/src/core/web/thread_service_patch.js +++ b/addons/mail/static/src/core/web/thread_service_patch.js @@ -237,13 +237,6 @@ patch(ThreadService.prototype, { [follower.followedThread.id], [follower.partner.id], ]); - const thread = follower.followedThread; - if (follower.eq(thread.selfFollower)) { - thread.selfFollower = undefined; - } else { - thread.followers.delete(follower); - } - this.removeRecipient(follower); follower.delete(); }, unpin(thread) { 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 59fc621ba91..a6493fb14d4 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 @@ -4,19 +4,19 @@ import { Record } from "@mail/core/common/record"; export class RtcSession extends Record { static id = "id"; - /** @type {Object.} */ + /** @type {Object.} */ static records = {}; - /** @returns {RtcSession} */ + /** @returns {import("models").Models["RtcSession"]} */ static new(data) { return super.new(data); } - /** @returns {RtcSession} */ + /** @returns {import("models").Models["RtcSession"]} */ static get(data) { return super.get(data); } /** * @param {Object} data - * @returns {number, RtcSession} + * @returns {number, import("models").Models["RtcSession"]} */ static insert(data) { const session = this.get(data) ?? this.new(data); 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 3bab9b66963..6d6095fb06e 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 @@ -1,7 +1,5 @@ /* @odoo-module */ -import { removeFromArrayWithPredicate } from "@mail/utils/common/arrays"; - import { markup, reactive } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; @@ -226,7 +224,6 @@ export class DiscussCoreCommon { if (!channel.is_pinned) { this.threadService.pin(channel); } - removeFromArrayWithPredicate(channel.messages, ({ id }) => id === messageData.temporary_id); this.store.Message.get(messageData.temporary_id)?.delete(); messageData.temporary_id = null; if ("parentMessage" in messageData && messageData.parentMessage.body) { diff --git a/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js b/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js index 66a45cca1ff..da21f5825ee 100644 --- a/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js +++ b/addons/mail/static/src/discuss/message_pin/common/message_pin_service.js @@ -2,26 +2,38 @@ import { Message } from "@mail/core/common/message"; import { MessageConfirmDialog } from "@mail/core/common/message_confirm_dialog"; +import { Message as MessageModel } from "@mail/core/common/message_model"; +import { Thread } from "@mail/core/common/thread_model"; import { markup, reactive } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; +import { patch } from "@web/core/utils/patch"; export const OTHER_LONG_TYPING = 60000; +patch(Thread.prototype, { + setup() { + super.setup(); + /** @type {'loaded'|'loading'|'error'|undefined} */ + this.pinnedMessagesState = undefined; + this.pinnedMessages = Thread.Set("Message"); + }, +}); + +patch(MessageModel.prototype, { + setup() { + super.setup(); + /** @type {string} */ + this.pinnedAt = undefined; + }, +}); + export class MessagePin { busService; - /** @type {Map} */ - loadStateByChannelId = new Map(); - /** @type {Map>} */ - messageIdsByChannelId = new Map(); - /** @type {Map} */ - channelIdByMessageId = new Map(); - /** @type {Map} */ - pinnedAtByMessageId = new Map(); /** @type {import("@mail/core/common/store_service").Store} */ - storeService; + store; /** * @param {import("@web/env").OdooEnv} env @@ -33,47 +45,27 @@ export class MessagePin { this.dialogService = services.dialog; this.ormService = services.orm; this.rpcService = services.rpc; - this.storeService = services["mail.store"]; + this.store = services["mail.store"]; } setup() { this.env.bus.addEventListener("mail.message/onUpdate", ({ detail: { message, data } }) => { this.onMessageUpdate(message, data); }); - this.busService.subscribe("mail.message/delete", ({ message_ids }) => { - for (const messageId of message_ids) { - this.removePinnedMessage(messageId); - } - }); this.busService.start(); } /** - * @param {number} channelId - * @param {number} messageId - * @param {string} pinnedAt - */ - addPinnedMessage(channelId, messageId, pinnedAt) { - if (!this.messageIdsByChannelId.has(channelId)) { - this.messageIdsByChannelId.set(channelId, new Set()); - } - const messageIds = this.messageIdsByChannelId.get(channelId); - messageIds.add(messageId); - this.channelIdByMessageId.set(messageId, channelId); - this.pinnedAtByMessageId.set(messageId, pinnedAt); - } - - /** - * @param {import("@mail/core/common/thread_model").Thread} channel + * @param {import("models").Models["Thread"]} channel */ async fetchPinnedMessages(channel) { if ( channel.model !== "discuss.channel" || - ["loaded", "loading"].includes(this.loadStateByChannelId.get(channel.id)) + ["loaded", "loading"].includes(channel.pinnedMessagesState) ) { return; } - this.loadStateByChannelId.set(channel.id, "loading"); + channel.pinnedMessagesState = "loading"; try { const messagesData = await this.rpcService("/discuss/channel/pinned_messages", { channel_id: channel.id, @@ -83,11 +75,11 @@ export class MessagePin { messageData.parentMessage.body = markup(messageData.parentMessage.body); } messageData.body = markup(messageData.body); - this.storeService.Message.insert(messageData); + this.store.Message.insert(messageData); }); - this.loadStateByChannelId.set(channel.id, "loaded"); + channel.pinnedMessagesState = "loaded"; } catch (e) { - this.loadStateByChannelId.set(channel.id, "error"); + channel.pinnedMessagesState = "error"; throw e; } } @@ -97,25 +89,21 @@ export class MessagePin { * @returns {string|null} */ getPinnedAt(messageId) { - const pinnedAt = this.pinnedAtByMessageId.get(messageId); + const pinnedAt = this.store.Message.get(messageId)?.pinnedAt; return pinnedAt ? luxon.DateTime.fromISO(new Date(pinnedAt).toISOString()) : null; } /** - * @param {import("@mail/core/common/thread_model").Thread} channel - * @returns {import("@mail/core/common/message_model").Message[]} + * @param {import("models").Models["Thread"]} channel + * @returns {import("models").Models["Message"][]} */ getPinnedMessages(channel) { - return [...(this.messageIdsByChannelId.get(channel.id) ?? new Set())] - .map((id) => this.storeService.Message.insert({ id })) - .sort((a, b) => { - const aPinnedAt = this.pinnedAtByMessageId.get(a.id); - const bPinnedAt = this.pinnedAtByMessageId.get(b.id); - if (aPinnedAt === bPinnedAt) { - return b.id - a.id; - } - return aPinnedAt < bPinnedAt ? 1 : -1; - }); + return [...channel.pinnedMessages].sort((a, b) => { + if (a.pinnedAt === b.pinnedAt) { + return b.id - a.id; + } + return a.pinnedAt < b.pinnedAt ? 1 : -1; + }); } /** @@ -136,9 +124,11 @@ export class MessagePin { (pinnedAt !== undefined || message.isEmpty) ) { if (pinnedAt && !message.isEmpty) { - this.addPinnedMessage(message.originThread.id, message.id, pinnedAt); + message.originThread.pinnedMessages.add(message); + message.pinnedAt = pinnedAt; } else { - this.removePinnedMessage(message.id); + delete message.pinnedAt; + message.originThread.pinnedMessages.delete(message); } } } @@ -163,25 +153,6 @@ export class MessagePin { }); } - /** - * @param {number} messageId - */ - removePinnedMessage(messageId) { - const channelId = this.channelIdByMessageId.get(messageId); - if (!channelId) { - return; - } - const messageIds = this.messageIdsByChannelId.get(channelId); - if (messageIds) { - messageIds.delete(messageId); - if (messageIds.size === 0) { - this.messageIdsByChannelId.delete(channelId); - } - } - this.channelIdByMessageId.delete(messageId); - this.pinnedAtByMessageId.delete(messageId); - } - /** * @param {import("@mail/core/common/message_model").Message} * @param {boolean} pinned diff --git a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js index 9522999d110..b3918381edd 100644 --- a/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js +++ b/addons/mail/static/tests/messaging_menu/messaging_menu_tests.js @@ -37,10 +37,12 @@ QUnit.test("messaging menu should have topbar buttons", async () => { QUnit.test("counter is taking into account failure notification", async () => { patchBrowserNotification("denied"); const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({}); + const channelId = pyEnv["discuss.channel"].create({ display_name: "general" }); const messageId = pyEnv["mail.message"].create({ model: "discuss.channel", res_id: channelId, + record_name: "general", + res_model_name: "Channel", }); const [memberId] = pyEnv["discuss.channel.member"].search([ ["channel_id", "=", channelId], diff --git a/addons/mail/tooling/types/models.d.ts b/addons/mail/tooling/types/models.d.ts index 17cff8a25c8..066ffe5ab41 100644 --- a/addons/mail/tooling/types/models.d.ts +++ b/addons/mail/tooling/types/models.d.ts @@ -1,5 +1,6 @@ /* add this file in jsconfig.json, in typeRoots array */ declare module "models" { + import { Activity } from "@mail/core/web/activity_model"; import { Attachment } from "@mail/core/common/attachment_model"; import { CannedResponse } from "@mail/core/common/canned_response_model"; import { ChannelMember } from "@mail/core/common/channel_member_model"; @@ -14,18 +15,28 @@ declare module "models" { import { Thread } from "@mail/core/common/thread_model"; export interface Models { + "Activity": Activity, "Attachment": Attachment, "CannedResponse": CannedResponse, "ChannelMember": ChannelMember, "Composer": Composer, "Follower": Follower, "LinkPreview": LinkPreview, - "Message": Message, + "Message": Message & Partial, "MessageReactions": MessageReactions, "Notification": Notification, "Persona": Persona, "RtcSession": RtcSession, - "Thread": Thread, + "Thread": Thread & Partial, + } + + export interface Message_Patch { + pinnedAt: string, + } + + export interface Thread_Patch { + pinnedMessages: RecordSet, + pinnedMessagesState: "loaded"|"loading"|"error"|undefined, } import { RecordList, RecordSet } from "@mail/core/common/record";