From 631ae1c4d865b912a59ddc8a068bdbf2dedd6a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 5 Oct 2023 17:21:49 +0200 Subject: [PATCH] [REF] mail: simplify JS discuss models further - Remove "channel" field in thread formatter. - Pass `type` for all persona formatters. - Remove some snake_case to camelCase conversions. - Add support for inverse fields in discuss JS models. Details: 1. [REF] mail: remove field 'channel' in thread formatter This field was used for channel-specific fields, which made sense when there was a dedicated Channel model that was modeled with composition with Thread. To simplify formatter of threads, it's best to flatten props so that channel-specific fields are immediately available on thread model. This will improve insertion of data with Threads. This commit also makes the following other changes: - remove `discuss.channel/legacy_insert` to use `mail.record/insert` instead. - Introduce `toData()` on record, which is helpful to have record in data format e.g. to pass as a JSON.stringifiable object. 2. [REF] mail: slightly simplify Message.insert from notif The handling of `mail.record/insert` for Message was handling transition from starred non-empty message to starred empty message. To simplify all record insert from server formatted data, the notif data is just inserted in Message. The adjustment of starred counter is managed at model level. This is a prerequisite to significantly simplify all `mail.record/insert` handling. 3. [REF] mail: rename 'res.users.settings' notifications Before this commit, notifications related to changes of user settings were using named notification `mail.record/insert`. This named notification should be only used for Discuss data that should be inserted in models. `res.users.settings` is not integrated in Discuss model, thus it has no reason to use this named notification. This commit rename the notification name to `res.users.settings` for these specific notifications. This prepares simplification on handling any `mail.record/insert` notifications that should simply call `Record.insert()` 4. [REF] mail: make dedicate notif for Thread/fold_state This was using named notif "mail.record/insert", which should be used to immediately insert data in models. This is however a dedicated notification to imperatively manager chat window state based on timing of receiving thread data. This may eventually become a `mail.record/insert` in the future, but right now it's much simpler to define it as its own named notification, in preparation to simplify `mail.record/insert` notifications handling. 5. [REF] mail: remove Channel in mail.record/insert This is replaced by `Thread`, so that these data can be immediately inserted in Thread model. 6. [REF] mail: simplify slightly Attachment.update() Now that data containing commands is supported, we could just assign with the command rather than destructure and pick the dict data part. 7. [REF] mail: introduce assignIn() utils This function helps reduce LOCs from using the "in" conditional in sequence: ```js if (a in data) { this[a] = data[a]; } if (b in data) { this[b] = data[b]; } if (c in data) { this[c] = data[c]; } ``` To simply: ```js assignIn(this, data, [a, b, c]); ``` 8. [REF] mail: remove snake_case to camelCase conversion in models They exist for the sake of keeping Python code snake_case and JS camelCase. While it's good that each language have a community that prefer syntax convention, when a codebase uses both languages and they should work with the same data, it's not great to convert snake_case to camelCase and vice-versa all the time. Since server has authority over the data, the server chooses the format for the keys. Most of them are snake_cased, therefore this is usually the one we pick. 9. [REF] mail: rename Message.messageReactionGroups to Message.reactions Easier to read, and matches relation name in JS model 10. [REF] mail: remove explicit assign of some many relations in Message This reduce amount of custom code in insert(), in preparation to make all models behave the same in response to inserting data. 11. [REF] mail: rename Thread.customName to Thread.channel_custom_name To match server data field name, and avoid useless conversion in JS. 12. [REF] mail: simplify Message.insert for recipients Have formatted data contain `type: "partner"` so it can be assigned in relational field without adding `type: "partner"` manually in JS. 13. [REF] mail: introduce inverse field in discuss models With this commit, fields in different models can be linked together, so that one is mirror of the other field. This simplifies some `onAdd`/`onDelete` that were added to sync such fields, and this also simplifies insertion in relational fields for discuss models that are identified by records, such as the `MessageReactions` that is identified by the message and the emoji. 14. [REF] mail: rename CannedResponse.name to 'source' To make JS model and server data more alike. 15. [REF] mail: remove assignDefined in Persona model So that eventually all model inserts use `Object.assign()`. 16. [REF] mail: remove 'last_message_id' from channel_info At some point it was used to display last message in messaging menu. This is already covered by `channel_fetch_preview` when opening the messaging menu for the 1st time, so passing `last_message_id` in channel_info is obsolete. closes odoo/odoo#137750 Related: odoo/enterprise#48484 Signed-off-by: Matthieu Stockbauer (tsm) --- .../hr_holidays/tests/test_out_of_office.py | 2 +- addons/im_livechat/models/discuss_channel.py | 4 +- .../src/composer/suggestion_service_patch.js | 21 +- .../static/src/core/@types/models.d.ts | 2 + .../src/core/channel_member_list_patch.xml | 4 +- .../static/src/core/thread_model_patch.js | 12 +- .../src/core/web/livechat_core_web_service.js | 7 +- .../static/src/embed/core/livechat_service.js | 4 + .../src/embed/core/thread_model_patch.js | 2 +- .../mock_server/models/discuss_channel.js | 2 +- .../static/tests/sidebar_patch_tests.js | 16 +- .../tests/test_get_discuss_channel.py | 24 +- addons/im_livechat/tests/test_message.py | 4 +- addons/mail/models/discuss/discuss_channel.py | 60 +- .../models/discuss/discuss_channel_member.py | 2 +- addons/mail/models/discuss/res_partner.py | 6 +- addons/mail/models/mail_link_preview.py | 2 +- addons/mail/models/mail_message.py | 10 +- addons/mail/models/mail_thread.py | 4 +- addons/mail/models/res_users_settings.py | 4 +- .../src/core/common/attachment_model.js | 52 +- .../src/core/common/canned_response_model.js | 15 +- .../src/core/common/channel_member_model.js | 27 +- .../mail/static/src/core/common/composer.js | 2 +- .../mail/static/src/core/common/composer.xml | 2 +- .../static/src/core/common/composer_model.js | 18 +- .../mail/static/src/core/common/discuss.xml | 2 +- .../core/common/discuss_app_category_model.js | 6 +- .../static/src/core/common/follower_model.js | 5 +- .../src/core/common/link_preview_model.js | 17 +- .../core/common/mail_core_common_service.js | 27 +- addons/mail/static/src/core/common/message.js | 11 +- .../mail/static/src/core/common/message.xml | 26 +- .../static/src/core/common/message_model.js | 93 +- .../src/core/common/message_reactions.xml | 4 +- .../core/common/message_reactions_model.js | 16 +- .../static/src/core/common/message_service.js | 12 - .../core/common/notification_group_model.js | 5 +- .../src/core/common/notification_model.js | 12 +- .../src/core/common/out_of_focus_service.js | 2 +- .../static/src/core/common/persona_model.js | 9 +- addons/mail/static/src/core/common/record.js | 223 +++- .../static/src/core/common/store_service.js | 132 ++- .../src/core/common/suggestion_service.js | 14 +- addons/mail/static/src/core/common/thread.js | 8 +- .../static/src/core/common/thread_model.js | 83 +- .../static/src/core/common/thread_service.js | 8 +- .../core/web/activity_markasdone_popover.js | 8 +- .../static/src/core/web/activity_service.js | 16 +- .../static/src/core/web/message_patch.xml | 6 +- .../src/core/web/thread_service_patch.js | 16 +- .../call/common/call_participant_card.js | 2 +- .../src/discuss/call/common/rtc_service.js | 11 +- .../discuss/call/common/rtc_session_model.js | 30 +- .../common/discuss_core_common_service.js | 28 +- .../discuss/core/common/thread_model_patch.js | 2 +- .../src/discuss/core/public/discuss_public.js | 2 +- .../core/web/discuss_core_web_service.js | 46 +- addons/mail/static/src/utils/common/misc.js | 9 + .../src/views/web/form/form_controller.js | 2 +- .../static/tests/core/message_model_tests.js | 6 +- .../static/tests/discuss_app/sidebar_tests.js | 32 +- .../mock_server/controllers/discuss.js | 4 +- .../mock_server/models/discuss_channel.js | 68 +- .../models/discuss_channel_member.js | 2 +- .../mock_server/models/mail_link_preview.js | 2 +- .../mock_server/models/mail_message.js | 8 +- .../helpers/mock_server/models/res_partner.js | 14 +- .../mock_server/models/res_users_settings.js | 5 +- .../tests/discuss/test_discuss_channel.py | 7 +- .../tests/discuss/test_message_controller.py | 6 +- addons/mail/tests/discuss/test_rtc.py | 46 +- addons/mail/tests/test_link_preview.py | 2 +- addons/mail/tests/test_res_users_settings.py | 8 +- .../tests/test_performance.py | 1003 ++++++++--------- addons/test_mail/tests/test_performance.py | 4 +- .../website_livechat_notification_handler.js | 2 +- 77 files changed, 1159 insertions(+), 1261 deletions(-) diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py index a4c89cb1920..8850e2c765e 100644 --- a/addons/hr_holidays/tests/test_out_of_office.py +++ b/addons/hr_holidays/tests/test_out_of_office.py @@ -50,7 +50,7 @@ class TestOutOfOffice(TestHrHolidaysCommon): }) channel_info = channel._channel_info()[0] # shape of channelMembers is [('ADD', data...)], [0][1] accesses the data - members_data = channel_info['channel']['channelMembers'][0][1] + members_data = channel_info['channelMembers'][0][1] self.assertEqual(len(members_data), 2, "Channel info should get info for the 2 members") partner_info = next(member for member in members_data if member['persona']['email'] == partner.email) partner2_info = next(member for member in members_data if member['persona']['email'] == partner2.email) diff --git a/addons/im_livechat/models/discuss_channel.py b/addons/im_livechat/models/discuss_channel.py index fb5424d061a..bab7313d9ae 100644 --- a/addons/im_livechat/models/discuss_channel.py +++ b/addons/im_livechat/models/discuss_channel.py @@ -51,8 +51,8 @@ class DiscussChannel(models.Model): for channel in self: if channel.chatbot_current_step_id: channel_infos_dict[channel.id]["chatbot_script_id"] = channel.chatbot_current_step_id.chatbot_script_id.id - channel_infos_dict[channel.id]['channel']['anonymous_name'] = channel.anonymous_name - channel_infos_dict[channel.id]['channel']['anonymous_country'] = { + channel_infos_dict[channel.id]['anonymous_name'] = channel.anonymous_name + channel_infos_dict[channel.id]['anonymous_country'] = { 'code': channel.country_id.code, 'id': channel.country_id.id, 'name': channel.country_id.name, diff --git a/addons/im_livechat/static/src/composer/suggestion_service_patch.js b/addons/im_livechat/static/src/composer/suggestion_service_patch.js index 9fa578b849f..f312cf2bbd7 100644 --- a/addons/im_livechat/static/src/composer/suggestion_service_patch.js +++ b/addons/im_livechat/static/src/composer/suggestion_service_patch.js @@ -3,7 +3,6 @@ import { SuggestionService } from "@mail/core/common/suggestion_service"; import { cleanTerm } from "@mail/utils/common/format"; -import { _t } from "@web/core/l10n/translation"; import { patch } from "@web/core/utils/patch"; patch(SuggestionService.prototype, { @@ -31,20 +30,14 @@ patch(SuggestionService.prototype, { }, searchCannedResponseSuggestions(cleanedSearchTerm, sort) { - const cannedResponses = Object.values(this.store.CannedResponse.records) - .filter((cannedResponse) => { - return cleanTerm(cannedResponse.name).includes(cleanedSearchTerm); - }) - .map(({ id, name, substitution }) => { - return { - id, - name, - substitution: _t(substitution), - }; - }); + const cannedResponses = Object.values(this.store.CannedResponse.records).filter( + (cannedResponse) => { + return cleanTerm(cannedResponse.source).includes(cleanedSearchTerm); + } + ); const sortFunc = (c1, c2) => { - const cleanedName1 = cleanTerm(c1.name); - const cleanedName2 = cleanTerm(c2.name); + const cleanedName1 = cleanTerm(c1.source); + const cleanedName2 = cleanTerm(c2.source); if ( cleanedName1.startsWith(cleanedSearchTerm) && !cleanedName2.startsWith(cleanedSearchTerm) 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 f7bed8977b6..c53e11abc92 100644 --- a/addons/im_livechat/static/src/core/@types/models.d.ts +++ b/addons/im_livechat/static/src/core/@types/models.d.ts @@ -3,6 +3,8 @@ declare module "models" { livechat: DiscussAppCategory, } export interface Thread { + anonymous_country: Object, + anonymous_name: String, operator: Persona, } } diff --git a/addons/im_livechat/static/src/core/channel_member_list_patch.xml b/addons/im_livechat/static/src/core/channel_member_list_patch.xml index b905d651d61..a0fc7fe4cd3 100644 --- a/addons/im_livechat/static/src/core/channel_member_list_patch.xml +++ b/addons/im_livechat/static/src/core/channel_member_list_patch.xml @@ -9,9 +9,9 @@ - + - + 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 fbea6d3c9ed..d183a4a1c97 100644 --- a/addons/im_livechat/static/src/core/thread_model_patch.js +++ b/addons/im_livechat/static/src/core/thread_model_patch.js @@ -2,7 +2,7 @@ import { Record } from "@mail/core/common/record"; import { Thread } from "@mail/core/common/thread_model"; -import { assignDefined } from "@mail/utils/common/misc"; +import { assignIn } from "@mail/utils/common/misc"; import { patch } from "@web/core/utils/patch"; @@ -10,9 +10,7 @@ patch(Thread, { insert(data) { const thread = super.insert(data); if (thread.type === "livechat") { - if (data?.channel) { - assignDefined(thread, data.channel, ["anonymous_name"]); - } + assignIn(thread, data, ["anonymous_name", "anonymous_country"]); if (data?.operator_pid) { thread.operator = { type: "partner", @@ -69,10 +67,8 @@ patch(Thread.prototype, { if (!this.correspondent.is_public && this.correspondent.country) { return `${this.getMemberName(this.correspondent)} (${this.correspondent.country.name})`; } - if (this.channel?.anonymous_country) { - return `${this.getMemberName(this.correspondent)} (${ - this.channel.anonymous_country.name - })`; + if (this.anonymous_country) { + return `${this.getMemberName(this.correspondent)} (${this.anonymous_country.name})`; } return this.getMemberName(this.correspondent); }, diff --git a/addons/im_livechat/static/src/core/web/livechat_core_web_service.js b/addons/im_livechat/static/src/core/web/livechat_core_web_service.js index 6ded0c0c2eb..981cccef403 100644 --- a/addons/im_livechat/static/src/core/web/livechat_core_web_service.js +++ b/addons/im_livechat/static/src/core/web/livechat_core_web_service.js @@ -20,11 +20,10 @@ export class LivechatCoreWeb { if (data.current_user_settings?.is_discuss_sidebar_category_livechat_open) { this.store.discuss.livechat.isOpen = true; } - this.busService.subscribe("mail.record/insert", (payload) => { - const { "res.users.settings": settings } = payload; - if (settings) { + this.busService.subscribe("res.users.settings", (payload) => { + if (payload) { this.store.discuss.livechat.isOpen = - settings.is_discuss_sidebar_category_livechat_open ?? + payload.is_discuss_sidebar_category_livechat_open ?? this.store.discuss.livechat.isOpen; } }); diff --git a/addons/im_livechat/static/src/embed/core/livechat_service.js b/addons/im_livechat/static/src/embed/core/livechat_service.js index 853e0aa08c2..ed3092ce524 100644 --- a/addons/im_livechat/static/src/embed/core/livechat_service.js +++ b/addons/im_livechat/static/src/embed/core/livechat_service.js @@ -1,5 +1,6 @@ /* @odoo-module */ +import { Record } from "@mail/core/common/record"; import { reactive } from "@odoo/owl"; import { browser } from "@web/core/browser/browser"; @@ -106,6 +107,9 @@ export class LivechatService { * @param {Object} values */ updateSession(values) { + if (Record.isRecord(values?.channel)) { + values.channel = values.channel.toData(); + } const session = JSON.parse(cookie.get(this.SESSION_COOKIE) ?? "{}"); Object.assign(session, { visitor_uid: this.visitorUid, 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 25d618ee9e7..93e8efb8286 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 @@ -26,7 +26,7 @@ patch(Thread, { livechatService.updateSession({ state: thread.state, seen_message_id: thread.seen_message_id, - channel: thread.channel, + channel: thread, allow_public_upload: thread.allow_public_upload, }); } diff --git a/addons/im_livechat/static/tests/helpers/mock_server/models/discuss_channel.js b/addons/im_livechat/static/tests/helpers/mock_server/models/discuss_channel.js index 4f6dc0d7792..36c1c4f08d4 100644 --- a/addons/im_livechat/static/tests/helpers/mock_server/models/discuss_channel.js +++ b/addons/im_livechat/static/tests/helpers/mock_server/models/discuss_channel.js @@ -13,7 +13,7 @@ patch(MockServer.prototype, { const channelInfos = super._mockDiscussChannelChannelInfo(...arguments); for (const channelInfo of channelInfos) { const channel = this.getRecords("discuss.channel", [["id", "=", channelInfo.id]])[0]; - channelInfo["channel"]["anonymous_name"] = channel.anonymous_name; + channelInfo["anonymous_name"] = channel.anonymous_name; // add the last message date if (channel.channel_type === "livechat") { // add the operator id diff --git a/addons/im_livechat/static/tests/sidebar_patch_tests.js b/addons/im_livechat/static/tests/sidebar_patch_tests.js index 1f963bcc4cd..7b546c7cebd 100644 --- a/addons/im_livechat/static/tests/sidebar_patch_tests.js +++ b/addons/im_livechat/static/tests/sidebar_patch_tests.js @@ -182,11 +182,9 @@ QUnit.test("Open from the bus", async () => { await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel", { count: 0, }); - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { - "res.users.settings": { - id: settingsId, - is_discuss_sidebar_category_livechat_open: true, - }, + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", { + id: settingsId, + is_discuss_sidebar_category_livechat_open: true, }); await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel"); }); @@ -210,11 +208,9 @@ QUnit.test("Close from the bus", async () => { const { openDiscuss } = await start(); openDiscuss(); await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel"); - pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { - "res.users.settings": { - id: settingsId, - is_discuss_sidebar_category_livechat_open: false, - }, + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", { + id: settingsId, + is_discuss_sidebar_category_livechat_open: false, }); await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel", { count: 0, diff --git a/addons/im_livechat/tests/test_get_discuss_channel.py b/addons/im_livechat/tests/test_get_discuss_channel.py index 49a3bac3f59..ba8d3f36c13 100644 --- a/addons/im_livechat/tests/test_get_discuss_channel.py +++ b/addons/im_livechat/tests/test_get_discuss_channel.py @@ -36,12 +36,12 @@ class TestGetDiscussChannel(TestImLivechatCommon): 'country_id': belgium.id, }, ) - self.assertEqual(channel_info['channel']['anonymous_name'], "Visitor 22") - self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) + self.assertEqual(channel_info['anonymous_name'], "Visitor 22") + self.assertEqual(channel_info['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) # ensure member info are hidden (in particular email and real name when livechat username is present) # shape of channelMembers is [('ADD', data...)], [0][1] accesses the data - self.assertEqual(sorted((m['persona'] for m in channel_info['channel']['channelMembers'][0][1]), key=lambda m: m['id']), sorted([{ + self.assertEqual(sorted((m['persona'] for m in channel_info['channelMembers'][0][1]), key=lambda m: m['id']), sorted([{ 'id': self.env['discuss.channel'].browse(channel_info['id']).channel_member_ids.filtered(lambda m: m.guest_id)[0].guest_id.id, 'name': 'Visitor', 'im_status': 'offline', @@ -64,11 +64,11 @@ class TestGetDiscussChannel(TestImLivechatCommon): 'user_id': test_user.id, 'channel_id': self.livechat_channel.id, }) - self.assertFalse(channel_info['channel']['anonymous_name']) - self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) - self.assertEqual(channel_info['channel']['channelMembers'], [['ADD', [ + self.assertFalse(channel_info['anonymous_name']) + self.assertEqual(channel_info['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'}) + self.assertEqual(channel_info['channelMembers'], [['ADD', [ { - 'channel': {'id': channel_info['id']}, + 'thread': {'id': channel_info['id'], 'model': "discuss.channel"}, 'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id, 'persona': { 'active': True, @@ -81,7 +81,7 @@ class TestGetDiscussChannel(TestImLivechatCommon): }, }, { - 'channel': {'id': channel_info['id']}, + 'thread': {'id': channel_info['id'], 'model': "discuss.channel"}, 'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', test_user.partner_id.id)]).id, 'persona': { 'active': True, @@ -109,11 +109,11 @@ class TestGetDiscussChannel(TestImLivechatCommon): 'channel_id': self.livechat_channel.id, }) self.assertEqual(channel_info['operator_pid'], [operator.partner_id.id, "Michel Operator"]) - self.assertFalse(channel_info['channel']['anonymous_name']) - self.assertEqual(channel_info['channel']['anonymous_country'], False) - self.assertEqual(channel_info['channel']['channelMembers'], [['ADD', [ + self.assertFalse(channel_info['anonymous_name']) + self.assertEqual(channel_info['anonymous_country'], False) + self.assertEqual(channel_info['channelMembers'], [['ADD', [ { - 'channel': {'id': channel_info['id']}, + 'thread': {'id': channel_info['id'], 'model': "discuss.channel"}, 'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id, 'persona': { 'active': True, diff --git a/addons/im_livechat/tests/test_message.py b/addons/im_livechat/tests/test_message.py index 57386ef54fa..95a12d3533d 100644 --- a/addons/im_livechat/tests/test_message.py +++ b/addons/im_livechat/tests/test_message.py @@ -53,7 +53,7 @@ class TestImLivechatMessage(HttpCase): rating_id=record_rating.id, ) self.assertEqual(message.message_format(), [{ - 'attachment_ids': [], + 'attachments': [], 'author': { 'id': self.users[1].partner_id.id, 'is_company': self.users[1].partner_id.is_company, @@ -75,7 +75,7 @@ class TestImLivechatMessage(HttpCase): 'is_note': True, 'linkPreviews': [], 'message_type': 'notification', - 'messageReactionGroups': [], + 'reactions': [], 'model': 'discuss.channel', 'module_icon': '/mail/static/description/icon.png', 'needaction_partner_ids': [], diff --git a/addons/mail/models/discuss/discuss_channel.py b/addons/mail/models/discuss/discuss_channel.py index cdcfbedf96c..d5c9e2e8219 100644 --- a/addons/mail/models/discuss/discuss_channel.py +++ b/addons/mail/models/discuss/discuss_channel.py @@ -295,9 +295,10 @@ class Channel(models.Model): if 'image_128' in vals: for channel in self: notifications.append([channel, 'mail.record/insert', { - 'Channel': { + 'Thread': { 'avatarCacheKey': channel._get_avatar_cache_key(), 'id': channel.id, + 'model': "discuss.channel", } }]) self.env['bus.bus']._sendmany(notifications) @@ -355,10 +356,11 @@ class Channel(models.Model): # post 'channel left' message as root since the partner just unsubscribed from the channel self.sudo().message_post(body=notification, subtype_xmlid="mail.mt_comment", author_id=partner.id) self.env['bus.bus']._sendone(self, 'mail.record/insert', { - 'Channel': { + 'Thread': { 'channelMembers': [('DELETE', {'id': member_id})], 'id': self.id, 'memberCount': self.member_count, + 'model': "discuss.channel", } }) @@ -431,10 +433,11 @@ class Channel(models.Model): 'channel': member.channel_id.sudo()._channel_info()[0], })) notifications.append((channel, 'mail.record/insert', { - 'Channel': { + 'Thread': { 'channelMembers': [('ADD', list(new_members._discuss_channel_member_format().values()))], 'id': channel.id, 'memberCount': channel.member_count, + 'model': "discuss.channel", } })) if existing_members: @@ -442,10 +445,11 @@ class Channel(models.Model): # In particular this fixes issues where the current user is not aware of its own member in the following case: # create channel from form view, and then join from discuss without refreshing the page. notifications.append((current_partner or current_guest, 'mail.record/insert', { - 'Channel': { + 'Thread': { 'channelMembers': [('ADD', list(existing_members._discuss_channel_member_format().values()))], 'id': channel.id, 'memberCount': channel.member_count, + 'model': "discuss.channel", } })) if invite_to_rtc_call: @@ -690,7 +694,7 @@ class Channel(models.Model): allowed_company_ids=user_id.company_ids.ids ) for channel_info in user_channels._channel_info(): - notifications.append((partner, 'discuss.channel/legacy_insert', channel_info)) + notifications.append((partner, 'mail.record/insert', {"Thread": channel_info})) return notifications def _channel_message_notifications(self, message, message_format=False): @@ -809,7 +813,6 @@ class Channel(models.Model): return [] channel_infos = [] rtc_sessions_by_channel = self.sudo().rtc_session_ids._mail_rtc_session_format_by_channel() - channel_last_message_ids = dict((r['id'], r['message_id']) for r in self._channel_last_message_ids()) current_partner, current_guest = self.env["res.partner"]._get_current_persona() self.env['discuss.channel'].flush_model() self.env['discuss.channel.member'].flush_model() @@ -843,13 +846,10 @@ class Channel(models.Model): if (current_partner and member.partner_id == current_partner) or (current_guest and member.guest_id == current_guest): member_of_current_user_by_channel[member.channel_id] = member for channel in self: - channel_data = { + info = { 'avatarCacheKey': channel._get_avatar_cache_key(), 'channel_type': channel.channel_type, - 'id': channel.id, 'memberCount': channel.member_count, - } - info = { 'id': channel.id, 'name': channel.name, 'defaultDisplayMode': channel.default_display_mode, @@ -862,20 +862,19 @@ class Channel(models.Model): 'create_uid': channel.create_uid.id, 'authorizedGroupFullName': channel.group_public_id.full_name, 'allow_public_upload': channel.allow_public_upload, + 'model': "discuss.channel", } - # add last message preview (only used in mobile) - info['last_message_id'] = channel_last_message_ids.get(channel.id, False) # find the channel member state if current_partner or current_guest: info['message_needaction_counter'] = channel.message_needaction_counter member = member_of_current_user_by_channel.get(channel, self.env['discuss.channel.member']).with_prefetch([m.id for m in member_of_current_user_by_channel.values()]) if member: - channel_data['channelMembers'] = [('ADD', list(member._discuss_channel_member_format().values()))] + info['channelMembers'] = [('ADD', list(member._discuss_channel_member_format().values()))] info['state'] = member.fold_state or 'open' - channel_data['message_unread_counter'] = member.message_unread_counter + info['message_unread_counter'] = member.message_unread_counter info['is_minimized'] = member.is_minimized info['seen_message_id'] = member.seen_message_id.id - channel_data['custom_channel_name'] = member.custom_channel_name + info['custom_channel_name'] = member.custom_channel_name info['is_pinned'] = member.is_pinned info['last_interest_dt'] = member.last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if member.rtc_inviting_session_id: @@ -885,7 +884,7 @@ class Channel(models.Model): # avoid sending potentially a lot of members for big channels # exclude chat and other small channels from this optimization because they are # assumed to be smaller and it's important to know the member list for them - channel_data['channelMembers'] = [('ADD', list(members_by_channel[channel]._discuss_channel_member_format().values()))] + info['channelMembers'] = [('ADD', list(members_by_channel[channel]._discuss_channel_member_format().values()))] info['seen_partners_info'] = sorted([{ 'id': cp.id, 'partner_id': cp.partner_id.id, @@ -897,9 +896,6 @@ class Channel(models.Model): 'invitedMembers': [('ADD', list(invited_members_by_channel[channel]._discuss_channel_member_format(fields={'id': True, 'channel': {}, 'persona': {'partner': {'id', 'name', 'im_status'}, 'guest': {'id', 'name', 'im_status'}}}).values()))], 'rtcSessions': [('ADD', rtc_sessions_by_channel.get(channel, []))], }) - - info['channel'] = channel_data - channel_infos.append(info) return channel_infos @@ -925,6 +921,7 @@ class Channel(models.Model): data = {} if 'id' in fields: data['id'] = channel.id + data['model'] = "discuss.channel" channels_formatted_data[channel] = data return channels_formatted_data @@ -1012,13 +1009,11 @@ class Channel(models.Model): vals['is_minimized'] = is_minimized if vals: session_state.write(vals) - self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', { - 'Thread': { - 'foldStateCount': state_count, - 'id': session_state.channel_id.id, - 'model': 'discuss.channel', - 'fold_state': state, - } + self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.Thread/fold_state', { + 'foldStateCount': state_count, + 'id': session_state.channel_id.id, + 'model': 'discuss.channel', + 'fold_state': state, }) def channel_pin(self, pinned=False): @@ -1030,7 +1025,7 @@ class Channel(models.Model): if not pinned: self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/unpin', {'id': self.id}) else: - self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/legacy_insert', self._channel_info()[0]) + self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', {"Thread": self._channel_info()[0]}) def _channel_seen(self, last_message_id=None, allow_older=False): """ @@ -1119,9 +1114,10 @@ class Channel(models.Model): member = self.env['discuss.channel.member'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', self.id)]) member.write({'custom_channel_name': name}) self.env['bus.bus']._sendone(member.partner_id, 'mail.record/insert', { - 'Channel': { + 'Thread': { 'custom_channel_name': name, 'id': self.id, + 'model': "discuss.channel", } }) @@ -1160,7 +1156,7 @@ class Channel(models.Model): } new_channel.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment") channel_info = new_channel._channel_info()[0] - self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/legacy_insert', channel_info) + self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', {"Thread": channel_info}) return channel_info @api.model @@ -1196,10 +1192,8 @@ class Channel(models.Model): channels = self.search(domain, limit=limit) return [{ 'authorizedGroupFullName': channel.group_public_id.full_name, - 'channel': { - 'channel_type': channel.channel_type, - 'id': channel.id, - }, + 'channel_type': channel.channel_type, + 'model': "discuss.channel", 'id': channel.id, 'name': channel.name, } for channel in channels] diff --git a/addons/mail/models/discuss/discuss_channel_member.py b/addons/mail/models/discuss/discuss_channel_member.py index 78551757553..397499c2de0 100644 --- a/addons/mail/models/discuss/discuss_channel_member.py +++ b/addons/mail/models/discuss/discuss_channel_member.py @@ -152,7 +152,7 @@ class ChannelMember(models.Model): if 'id' in fields: data['id'] = member.id if 'channel' in fields: - data['channel'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id) + data['thread'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id) if 'persona' in fields: if member.partner_id: persona = member._get_partner_data(fields=fields.get('persona', {}).get('partner')) diff --git a/addons/mail/models/discuss/res_partner.py b/addons/mail/models/discuss/res_partner.py index 7d7aa529ce3..1bd21e10ed0 100644 --- a/addons/mail/models/discuss/res_partner.py +++ b/addons/mail/models/discuss/res_partner.py @@ -108,8 +108,7 @@ class ResPartner(models.Model): } partners_format = partners.mail_partner_format() for partner in partners: - partners_format.get(partner)["persona"] = { - "channelMembers": [ + partners_format.get(partner)["channelMembers"] = [ ( "ADD", member_by_partner.get(partner) @@ -122,6 +121,5 @@ class ResPartner(models.Model): ) .get(member_by_partner.get(partner)), ) - ], - } + ] return list(partners_format.values()) diff --git a/addons/mail/models/mail_link_preview.py b/addons/mail/models/mail_link_preview.py index 77deb7fa933..5aff9473f63 100644 --- a/addons/mail/models/mail_link_preview.py +++ b/addons/mail/models/mail_link_preview.py @@ -91,7 +91,7 @@ class LinkPreview(models.Model): def _link_preview_format(self): return [{ 'id': preview.id, - 'message_id': preview.message_id.id, + 'message': {'id': preview.message_id.id}, 'image_mimetype': preview.image_mimetype, 'og_description': preview.og_description, 'og_image': preview.og_image, diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 15af41e6d95..186b3fbca0e 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -870,7 +870,7 @@ class Message(models.Model): "personas": personas, "message": {"id": self.id}, } - payload = {"Message": {"id": self.id, "messageReactionGroups": [(group_command, group_values)]}} + payload = {"Message": {"id": self.id, "reactions": [(group_command, group_values)]}} self.env["bus.bus"]._sendone(self._bus_notification_target(), "mail.record/insert", payload) # ------------------------------------------------------ @@ -918,10 +918,10 @@ class Message(models.Model): 'author': author, 'default_subject': default_subject, 'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(), - 'attachment_ids': sorted(message_sudo.attachment_ids._attachment_format(), key=lambda a: a["id"]), + 'attachments': sorted(message_sudo.attachment_ids._attachment_format(), key=lambda a: a["id"]), 'trackingValues': allowed_tracking_ids._tracking_value_format(), 'linkPreviews': message_sudo.link_preview_ids._link_preview_format(), - 'messageReactionGroups': reaction_groups, + 'reactions': reaction_groups, 'pinned_at': message_sudo.pinned_at, 'record_name': record_name, 'create_date': message_sudo.create_date, @@ -976,7 +976,7 @@ class Message(models.Model): 'body': HTML content of the message 'model': u'res.partner', 'record_name': u'Agrolait', - 'attachment_ids': [ + 'attachments': [ { 'file_type_icon': u'webimage', 'id': 45, @@ -1042,7 +1042,7 @@ class Message(models.Model): 'is_note': message_sudo.subtype_id.id == note_id, 'is_discussion': message_sudo.subtype_id.id == com_id, 'subtype_description': message_sudo.subtype_id.description, - 'recipients': [{'id': p.id, 'name': p.name} for p in message_sudo.partner_ids], + 'recipients': [{'id': p.id, 'name': p.name, 'type': "partner"} for p in message_sudo.partner_ids], 'scheduledDatetime': scheduled_dt_by_msg_id.get(vals['id'], False), }) if vals['model'] and self.env[vals['model']]._original_module: diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index c0a3bc396df..48a23bbd4dc 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -4139,9 +4139,9 @@ class MailThread(models.AbstractModel): 'Message': { 'id': message.id, 'body': message.body, - 'attachment_ids': message.attachment_ids.sorted("id")._attachment_format(), + 'attachments': message.attachment_ids.sorted("id")._attachment_format(), 'pinned_at': message.pinned_at, - 'recipients': [{'id': p.id, 'name': p.name} for p in message.partner_ids], + 'recipients': [{'id': p.id, 'name': p.name, 'type': "partner"} for p in message.partner_ids], 'write_date': message.write_date, } }) diff --git a/addons/mail/models/res_users_settings.py b/addons/mail/models/res_users_settings.py index af22b408c8d..7501e992795 100644 --- a/addons/mail/models/res_users_settings.py +++ b/addons/mail/models/res_users_settings.py @@ -26,7 +26,7 @@ class ResUsersSettings(models.Model): def set_res_users_settings(self, new_settings): formated = super().set_res_users_settings(new_settings) - self.env['bus.bus']._sendone(self.user_id.partner_id, 'mail.record/insert', {'res.users.settings': formated}) + self.env['bus.bus']._sendone(self.user_id.partner_id, 'res.users.settings', formated) return formated def set_volume_setting(self, partner_id, volume, guest_id=None): @@ -50,4 +50,4 @@ class ResUsersSettings(models.Model): 'partner_id': partner_id, 'guest_id': guest_id, }) - self.env['bus.bus']._sendone(self.user_id.partner_id, 'mail.record/insert', {'res.users.settings.volumes': volume_setting._discuss_users_settings_volume_format()}) + self.env['bus.bus']._sendone(self.user_id.partner_id, 'res.users.settings.volumes', volume_setting._discuss_users_settings_volume_format()) diff --git a/addons/mail/static/src/core/common/attachment_model.js b/addons/mail/static/src/core/common/attachment_model.js index fbff4c4df68..6869a2fa821 100644 --- a/addons/mail/static/src/core/common/attachment_model.js +++ b/addons/mail/static/src/core/common/attachment_model.js @@ -1,7 +1,7 @@ /* @odoo-module */ import { Record } from "@mail/core/common/record"; -import { assignDefined } from "@mail/utils/common/misc"; +import { onChange } from "@mail/utils/common/misc"; import { deserializeDateTime } from "@web/core/l10n/dates"; import { FileModelMixin } from "@web/core/file_viewer/file_model"; @@ -14,54 +14,30 @@ export class Attachment extends FileModelMixin(Record) { static get(data) { return super.get(data); } - /** - * @param {Object} data - * @returns {import("models").Attachment} - */ + /** @returns {import("models").Attachment} */ static insert(data) { if (!("id" in data)) { throw new Error("Cannot insert attachment: id is missing in data"); } + return super.insert(data); + } + static new(data) { /** @type {import("models").Attachment} */ - const attachment = this.preinsert(data); - attachment.update(data); + const attachment = super.new(data); + onChange(attachment, ["extension", "name"], () => { + if (!attachment.extension && attachment.name) { + attachment.extension = attachment.name.split(".").pop(); + } + }); return attachment; } update(data) { - assignDefined(this, data, [ - "checksum", - "create_date", - "filename", - "mimetype", - "name", - "type", - "url", - "uploading", - "extension", - "accessToken", - "tmpUrl", - "message", - "res_name", - ]); - if (!("extension" in data) && data["name"]) { - this.extension = this.name.split(".").pop(); - } - if (data.originThread !== undefined) { - const threadData = Array.isArray(data.originThread) - ? data.originThread[0][1] - : data.originThread; - 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)); - } + super.update(data); + this.originThread?.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1)); } - originThread = Record.one("Thread"); + originThread = Record.one("Thread", { inverse: "attachments" }); res_name; message = Record.one("Message"); /** @type {string} */ 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 43848aa5abf..d95f333d153 100644 --- a/addons/mail/static/src/core/common/canned_response_model.js +++ b/addons/mail/static/src/core/common/canned_response_model.js @@ -10,24 +10,15 @@ export class CannedResponse extends Record { static get(data) { return super.get(data); } - /** - * @param {Object} data - * @returns {import("models").CannedResponse} - */ + /** @returns {import("models").CannedResponse} */ static insert(data) { - /** @type {import("models").CannedResponse} */ - const cannedResponse = this.preinsert(data); - Object.assign(cannedResponse, { - name: data.source, - substitution: data.substitution, - }); - return cannedResponse; + return super.insert(data); } /** @type {number} */ id; /** @type {string} */ - name; + source; /** @type {string} */ substitution; } 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 b6838960516..3266988be77 100644 --- a/addons/mail/static/src/core/common/channel_member_model.js +++ b/addons/mail/static/src/core/common/channel_member_model.js @@ -17,35 +17,14 @@ export class ChannelMember extends Record { static get(data) { return super.get(data); } - /** - * @param {Object|Array} data - * @returns {import("models").ChannelMember} - */ + /** @returns {import("models").ChannelMember} */ static insert(data) { - /** @type {import("models").ChannelMember} */ - const member = this.preinsert(data); - member.update(data); - return member; - } - - update(data) { - this.id = data.id; - if ("persona" in data) { - this.persona = data.persona; - } - let thread = data.thread ?? this.thread; - if (!thread && data.channel?.id) { - thread = { - id: data.channel.id, - model: "discuss.channel", - }; - } - this.thread ??= thread; + return super.insert(data); } /** @type {number} */ id; - persona = Record.one("Persona"); + persona = Record.one("Persona", { inverse: "channelMembers" }); rtcSession = Record.one("RtcSession"); thread = Record.one("Thread"); diff --git a/addons/mail/static/src/core/common/composer.js b/addons/mail/static/src/core/common/composer.js index 2344c752bd4..91d68d0e075 100644 --- a/addons/mail/static/src/core/common/composer.js +++ b/addons/mail/static/src/core/common/composer.js @@ -315,7 +315,7 @@ export class Composer extends Component { options: suggestions.map((suggestion) => { return { cannedResponse: suggestion, - name: suggestion.name, + source: suggestion.source, label: suggestion.substitution, classList: "o-mail-Composer-suggestion", }; diff --git a/addons/mail/static/src/core/common/composer.xml b/addons/mail/static/src/core/common/composer.xml index 4c79bf2a0dc..d1560c8050e 100644 --- a/addons/mail/static/src/core/common/composer.xml +++ b/addons/mail/static/src/core/common/composer.xml @@ -140,7 +140,7 @@ - + diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index a18661b9e98..5ce1d07524f 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -8,27 +8,13 @@ export class Composer extends Record { static get(data) { return super.get(data); } - /** - * @param {Object} data - * @returns {import("models").Composer} - */ + /** @returns {import("models").Composer} */ static insert(data) { const { message, thread } = data; if (Boolean(message) === Boolean(thread)) { throw new Error("Composer shall have a thread xor a message."); } - /** @type {import("models").Composer} */ - const composer = this.preinsert(data); - if ("textInputContent" in data) { - composer.textInputContent = data.textInputContent; - } - if ("selection" in data) { - composer.selection = data.selection; - } - if ("mentionedPartners" in data) { - composer.mentionedPartners = data.mentionedPartners; - } - return composer; + return super.insert(data); } attachments = Record.many("Attachment"); diff --git a/addons/mail/static/src/core/common/discuss.xml b/addons/mail/static/src/core/common/discuss.xml index 16ad3ecd1a6..51c7720c670 100644 --- a/addons/mail/static/src/core/common/discuss.xml +++ b/addons/mail/static/src/core/common/discuss.xml @@ -63,7 +63,7 @@
- +
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 0ec6ca23532..8bc371d38c4 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 @@ -1,6 +1,5 @@ /* @odoo-module */ -import { assignDefined } from "@mail/utils/common/misc"; import { Record } from "./record"; export class DiscussAppCategory extends Record { @@ -11,10 +10,7 @@ export class DiscussAppCategory extends Record { } /** @returns {import("models").DiscussAppCategory} */ static insert(data) { - /** @type {import("models").DiscussAppCategory} */ - const category = this.preinsert(data); - assignDefined(category, data); - return category; + return super.insert(data); } /** @type {string} */ diff --git a/addons/mail/static/src/core/common/follower_model.js b/addons/mail/static/src/core/common/follower_model.js index 9c4aad8831c..284f64cdea0 100644 --- a/addons/mail/static/src/core/common/follower_model.js +++ b/addons/mail/static/src/core/common/follower_model.js @@ -12,10 +12,7 @@ export class Follower extends Record { } /** @returns {import("models").Follower} */ static insert(data) { - /** @type {import("models").Follower} */ - const follower = this.preinsert(data); - Object.assign(follower, data); - return follower; + return super.insert(data); } followedThread = Record.one("Thread"); 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 801e7e95c3a..f8576e8b905 100644 --- a/addons/mail/static/src/core/common/link_preview_model.js +++ b/addons/mail/static/src/core/common/link_preview_model.js @@ -8,24 +8,14 @@ export class LinkPreview extends Record { static get(data) { return super.get(data); } - /** - * @param {Object} data - * @returns {import("models").LinkPreview} - */ + /** @returns {import("models").LinkPreview} */ static insert(data) { - const message = this.store.Message.get(data.message_id); - data.message = message; - delete data.message_id; - /** @type {import("models").LinkPreview} */ - const linkPreview = this.preinsert(data); - Object.assign(linkPreview, data); - message?.linkPreviews.add(linkPreview); - return linkPreview; + return super.insert(data); } /** @type {number} */ id; - message = Record.one("Message"); + message = Record.one("Message", { inverse: "linkPreviews" }); /** @type {string} */ image_mimetype; /** @type {string} */ @@ -58,7 +48,6 @@ export class LinkPreview extends Record { get isCard() { return !this.isImage && !this.isVideo; } - } LinkPreview.register(); 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 d77488dadfe..d0afd78c5cf 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 @@ -49,8 +49,8 @@ export class MailCoreCommon { if (!message) { continue; } - message.delete(); this.env.bus.trigger("mail.message/delete", { message }); + message.delete(); } }); this.busService.subscribe("mail.message/notification_update", (payload) => { @@ -69,7 +69,20 @@ export class MailCoreCommon { const { message_ids: messageIds, starred } = payload; for (const messageId of messageIds) { const message = this.store.Message.insert({ id: messageId }); - this.messageService.updateStarred(message, starred); + message.isStarred = starred; + const starredBox = this.store.discuss.starred; + if (starred) { + starredBox.counter++; + starredBox.messages.add(message); + } else { + starredBox.counter--; + starredBox.messages.delete(message); + } + } + }); + this.busService.subscribe("res.users.settings", (payload) => { + if (payload) { + this.userSettingsService.updateFromCommands(payload); } }); this.busService.subscribe("mail.record/insert", (payload) => { @@ -100,18 +113,10 @@ export class MailCoreCommon { } const { Message: messageData } = payload; if (messageData) { - const isStarred = this.store.Message.get(messageData.id)?.isStarred; - const message = this.store.Message.insert({ + this.store.Message.insert({ ...messageData, body: messageData.body ? markup(messageData.body) : messageData.body, }); - if (isStarred && message.isEmpty) { - this.messageService.updateStarred(message, false); - } - } - const { "res.users.settings": settings } = payload; - if (settings) { - this.userSettingsService.updateFromCommands(settings); } }); }); diff --git a/addons/mail/static/src/core/common/message.js b/addons/mail/static/src/core/common/message.js index d16b1302178..efa8b278451 100644 --- a/addons/mail/static/src/core/common/message.js +++ b/addons/mail/static/src/core/common/message.js @@ -239,8 +239,8 @@ export class Message extends Component { get showSubtypeDescription() { return ( - this.message.subtypeDescription && - this.message.subtypeDescription.toLowerCase() !== + this.message.subtype_description && + this.message.subtype_description.toLowerCase() !== htmlToTextContentInline(this.message.body || "").toLowerCase() ); } @@ -252,7 +252,7 @@ export class Message extends Component { if (this.props.message.type === "auto_comment") { return _t("Automated message"); } - if (!this.props.message.isDiscussion && this.props.message.type !== "user_notification") { + if (!this.props.message.is_discussion && this.props.message.type !== "user_notification") { return _t("Note"); } return _t("Message"); @@ -262,7 +262,7 @@ export class Message extends Component { * @returns {boolean} */ get canAddReaction() { - return Boolean(!this.message.isTransient && this.message.resId); + return Boolean(!this.message.is_transient && this.message.res_id); } get deletable() { @@ -284,7 +284,7 @@ export class Message extends Component { * @returns {boolean} */ get canToggleStar() { - return Boolean(!this.message.isTransient && this.message.resId && this.store.user); + return Boolean(!this.message.is_transient && this.message.res_id && this.store.user); } get showUnfollow() { @@ -440,7 +440,6 @@ export class Message extends Component { const messageContent = convertBrToLineBreak(this.props.message.body); this.props.message.composer = { mentionedPartners: this.props.message.recipients, - message: this.props.message, textInputContent: messageContent, selection: { start: messageContent.length, diff --git a/addons/mail/static/src/core/common/message.xml b/addons/mail/static/src/core/common/message.xml index 0b78e640906..666aa820395 100644 --- a/addons/mail/static/src/core/common/message.xml +++ b/addons/mail/static/src/core/common/message.xml @@ -18,7 +18,7 @@
- + @@ -36,7 +36,7 @@ - + - @@ -45,11 +45,11 @@ className="'ms-1'" message="props.message" thread="props.thread"/> - - + + on - + (from ) @@ -72,20 +72,20 @@ >
- +
@@ -95,7 +95,7 @@
- +
diff --git a/addons/mail/static/src/core/common/message_model.js b/addons/mail/static/src/core/common/message_model.js index 89bef68e4d1..cf87ad9dea1 100644 --- a/addons/mail/static/src/core/common/message_model.js +++ b/addons/mail/static/src/core/common/message_model.js @@ -2,7 +2,7 @@ import { Record } from "@mail/core/common/record"; import { htmlToTextContentInline } from "@mail/utils/common/format"; -import { assignDefined } from "@mail/utils/common/misc"; +import { assignDefined, assignIn, onChange } from "@mail/utils/common/misc"; import { toRaw } from "@odoo/owl"; @@ -17,14 +17,23 @@ export class Message extends Record { static id = "id"; /** @type {Object.} */ static records = {}; + static new(data) { + const message = super.new(data); + onChange(message, "isEmpty", () => { + if (message.isEmpty && message.isStarred) { + message.isStarred = false; + const starred = this.store.discuss.starred; + starred.counter--; + starred.messages.delete(message); + } + }); + return message; + } /** @returns {import("models").Message} */ static get(data) { return super.get(data); } - /** - * @param {Object} data - * @returns {import("models").Message} - */ + /** @returns {import("models").Message} */ static insert(data) { if (data.res_id) { this.store.Thread.insert({ @@ -32,46 +41,23 @@ export class Message extends Record { id: data.res_id, }); } - /** @type {import("models").Message} */ - const message = this.preinsert(data); - message.update(data); - return message; + return super.insert(data); } /** @param {Object} data */ update(data) { const { - attachment_ids: attachments = this.attachments, - default_subject: defaultSubject = this.defaultSubject, - is_discussion: isDiscussion = this.isDiscussion, - is_note: isNote = this.isNote, - is_transient: isTransient = this.isTransient, - linkPreviews = this.linkPreviews, message_type: type = this.type, - model: resModel = this.resModel, module_icon, - notifications = this.notifications, - parentMessage, - recipients = this.recipients, record_name, - res_id: resId = this.resId, res_model_name, - subtype_description: subtypeDescription = this.subtypeDescription, ...remainingData } = data; assignDefined(this, remainingData); assignDefined(this, { - defaultSubject, - isDiscussion, - isNote, isStarred: this._store.user ? this.starred_partner_ids.includes(this._store.user.id) : false, - isTransient, - parentMessage: parentMessage || undefined, - resId, - resModel, - subtypeDescription, type, }); // origin thread before other information (in particular notification insert uses it) @@ -85,13 +71,7 @@ export class Message extends Record { : record_name || undefined, }); } - this.attachments = attachments.map((attachment) => ({ message: this, ...attachment })); - if ("author" in data) { - this.author = data.author; - } - this.linkPreviews = linkPreviews.map((data) => ({ ...data, message: this })); - this.notifications = notifications.map((notif) => ({ ...notif, message: this })); - this.recipients = recipients.map((recipient) => ({ ...recipient, type: "partner" })); + assignIn(this, data, ["author", "notifications", "reactions", "recipients"]); if ("user_follower_id" in data && data.user_follower_id && this._store.self) { this.originThread.selfFollower = { followedThread: this.originThread, @@ -100,9 +80,6 @@ export class Message extends Record { partner: this._store.self, }; } - if ("messageReactionGroups" in data) { - this.reactions = data.messageReactionGroups; - } if (this.isNotification && !this.notificationType) { const parser = new DOMParser(); const htmlBody = parser.parseFromString(this.body, "text/html"); @@ -110,36 +87,36 @@ export class Message extends Record { } } - attachments = Record.many("Attachment"); + attachments = Record.many("Attachment", { inverse: "message" }); author = Record.one("Persona"); /** @type {string} */ body; - composer = Record.one("Composer", { onDelete: (r) => r.delete() }); + composer = Record.one("Composer", { inverse: "message", onDelete: (r) => r.delete() }); /** @type {string} */ - defaultSubject; + default_subject; /** @type {number|string} */ id; /** @type {boolean} */ - isDiscussion; + is_discussion; /** @type {boolean} */ - isNote; + is_note; /** @type {boolean} */ isStarred; /** @type {boolean} */ - isTransient; - linkPreviews = Record.many("LinkPreview"); + is_transient; + linkPreviews = Record.many("LinkPreview", { inverse: "message" }); /** @type {number[]} */ needaction_partner_ids = []; /** @type {number[]} */ history_partner_ids = []; parentMessage = Record.one("Message"); - reactions = Record.many("MessageReactions"); - notifications = Record.many("Notification"); + reactions = Record.many("MessageReactions", { inverse: "message" }); + notifications = Record.many("Notification", { inverse: "message" }); recipients = Record.many("Persona"); /** @type {number|string} */ - resId; + res_id; /** @type {string|undefined} */ - resModel; + model; /** @type {string} */ scheduledDatetime; /** @type {Number[]} */ @@ -147,7 +124,7 @@ export class Message extends Record { /** @type {string} */ subject; /** @type {string} */ - subtypeDescription; + subtype_description; /** @type {Object[]} */ trackingValues = []; /** @type {string} */ @@ -207,7 +184,7 @@ export class Message extends Record { } get isHighlightedFromMention() { - return this.isSelfMentioned && this.resModel === "discuss.channel"; + return this.isSelfMentioned && this.model === "discuss.channel"; } get isSelfAuthored() { @@ -222,7 +199,7 @@ export class Message extends Record { } get hasActions() { - return !this.isTransient; + return !this.is_transient; } /** @@ -236,7 +213,7 @@ export class Message extends Record { * @returns {boolean} */ get isNotification() { - return this.type === "notification" && this.resModel === "discuss.channel"; + return this.type === "notification" && this.model === "discuss.channel"; } get isSubjectSimilarToOriginThreadName() { @@ -251,17 +228,17 @@ export class Message extends Record { get isSubjectDefault() { const threadName = this.originThread?.name?.trim().toLowerCase(); - const defaultSubject = this.defaultSubject ? this.defaultSubject.toLowerCase() : ""; + const defaultSubject = this.default_subject ? this.default_subject.toLowerCase() : ""; const candidates = new Set([defaultSubject, threadName]); return candidates.has(this.subject?.toLowerCase()); } get originThread() { - return this._store.Thread.get({ model: this.resModel, id: this.resId }); + return this._store.Thread.get({ model: this.model, id: this.res_id }); } get resUrl() { - return `${url("/web")}#model=${this.resModel}&id=${this.resId}`; + return `${url("/web")}#model=${this.model}&id=${this.res_id}`; } get editDate() { @@ -277,7 +254,7 @@ export class Message extends Record { this.isBodyEmpty && this.attachments.length === 0 && this.trackingValues.length === 0 && - !this.subtypeDescription + !this.subtype_description ); } get isBodyEmpty() { diff --git a/addons/mail/static/src/core/common/message_reactions.xml b/addons/mail/static/src/core/common/message_reactions.xml index 786c1930fe6..9d671fc684a 100644 --- a/addons/mail/static/src/core/common/message_reactions.xml +++ b/addons/mail/static/src/core/common/message_reactions.xml @@ -4,9 +4,9 @@
+ t-attf-class="{{ props.message.is_discussion ? 'mt-n2' : 'mt-1' }}">