From 5b59a07729a6552dd7e010601835c79b3191a671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Tue, 20 Sep 2022 12:33:03 +0000 Subject: [PATCH] [IMP] mail, *: introduce public livechat bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *: hr, im_livechat, snailmail, website_livechat This commit introduces the public livechat bundle, which contains the models that are specifically used in public livechat. Doing so reduces the amount of model and data in frontend and external lib, which makes the page load faster. Task-2990182 Task-2990191 closes odoo/odoo#100768 Related: odoo/enterprise#31608 Signed-off-by: Alexandre Kühn (aku) --- addons/hr/static/src/models/messaging.js | 2 +- addons/im_livechat/__manifest__.py | 14 +- .../static/src/models/messaging.js | 21 +- .../src/models/messaging_initializer.js | 10 - .../src/{models => public_models}/chatbot.js | 0 .../{models => public_models}/chatbot_step.js | 0 .../livechat_button_view.js | 0 .../livechat_operator.js | 0 .../static/src/public_models/messaging.js | 12 + .../public_livechat.js | 0 .../public_livechat_feedback_view.js | 0 .../public_livechat_global.js | 0 ...ic_livechat_global_notification_handler.js | 0 .../public_livechat_message.js | 0 .../public_livechat_view.js | 0 .../public_livechat_window.js | 0 addons/mail/__manifest__.py | 14 +- .../src/{models => core_models}/device.js | 0 .../src/{models => core_models}/locale.js | 0 .../mail/static/src/core_models/messaging.js | 115 +++ .../src/{models => core_models}/record.js | 0 addons/mail/static/src/models/messaging.js | 880 ++++++++---------- .../snailmail/static/src/models/messaging.js | 2 +- addons/website_livechat/__manifest__.py | 3 + .../src/{models => public_models}/chatbot.js | 2 +- .../livechat_button_view.js | 2 +- .../public_livechat_global.js | 2 +- .../tours/website_livechat_chatbot_flow.js | 2 +- .../tests/tours/website_livechat_common.js | 2 +- 29 files changed, 554 insertions(+), 529 deletions(-) rename addons/im_livechat/static/src/{models => public_models}/chatbot.js (100%) rename addons/im_livechat/static/src/{models => public_models}/chatbot_step.js (100%) rename addons/im_livechat/static/src/{models => public_models}/livechat_button_view.js (100%) rename addons/im_livechat/static/src/{models => public_models}/livechat_operator.js (100%) create mode 100644 addons/im_livechat/static/src/public_models/messaging.js rename addons/im_livechat/static/src/{models => public_models}/public_livechat.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_feedback_view.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_global.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_global_notification_handler.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_message.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_view.js (100%) rename addons/im_livechat/static/src/{models => public_models}/public_livechat_window.js (100%) rename addons/mail/static/src/{models => core_models}/device.js (100%) rename addons/mail/static/src/{models => core_models}/locale.js (100%) create mode 100644 addons/mail/static/src/core_models/messaging.js rename addons/mail/static/src/{models => core_models}/record.js (100%) rename addons/website_livechat/static/src/{models => public_models}/chatbot.js (95%) rename addons/website_livechat/static/src/{models => public_models}/livechat_button_view.js (97%) rename addons/website_livechat/static/src/{models => public_models}/public_livechat_global.js (94%) diff --git a/addons/hr/static/src/models/messaging.js b/addons/hr/static/src/models/messaging.js index 0ac64a70dc3..c99042ff579 100644 --- a/addons/hr/static/src/models/messaging.js +++ b/addons/hr/static/src/models/messaging.js @@ -2,7 +2,7 @@ import { patchRecordMethods } from '@mail/model/model_core'; // ensure that the model definition is loaded before the patch -import '@mail/models/messaging'; +import '@mail/core_models/messaging'; patchRecordMethods('Messaging', { /** diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index f5c85a73204..5703bf19b67 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -50,11 +50,16 @@ Help your customers with this chat, and analyse their feedback. 'im_livechat/static/src/components/*/*', ], 'web.assets_frontend': [ + ('include', 'mail.assets_public_livechat'), 'im_livechat/static/src/public/main.js', 'im_livechat/static/src/services/*.js', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', 'im_livechat/static/src/legacy/public_livechat.scss', 'im_livechat/static/src/legacy/public_livechat_chatbot.scss', + 'mail/static/src/utils/*.js', + 'mail/static/src/js/utils.js', + 'mail/static/src/component_hooks/*.js', + 'mail/static/src/services/messaging_service.js', ], 'web.assets_backend': [ 'im_livechat/static/src/js/im_livechat_channel_form_view.js', @@ -74,16 +79,19 @@ Help your customers with this chat, and analyse their feedback. ], 'web.qunit_suite_tests': [ 'im_livechat/static/tests/qunit_suite_tests/components/**/*.js', - 'im_livechat/static/src/legacy/public_livechat_chatbot.js', ], 'web.assets_tests': [ 'im_livechat/static/tests/tours/**/*', ], 'mail.assets_messaging': [ + 'im_livechat/static/src/models/*.js', + ], + 'mail.assets_public_livechat': [ + ('include', 'mail.assets_core_messaging'), 'im_livechat/static/src/legacy/models/*', 'im_livechat/static/src/legacy/widgets/*', 'im_livechat/static/src/legacy/widgets/*/*', - 'im_livechat/static/src/models/*.js', + 'im_livechat/static/src/public_models/*.js', ], # Bundle of External Librairies of the Livechat (Odoo + required modules) 'im_livechat.external_lib': [ @@ -198,7 +206,7 @@ Help your customers with this chat, and analyse their feedback. 'mail/static/src/utils/*.js', 'mail/static/src/js/emojis.js', 'mail/static/src/component_hooks/*.js', - ('include', 'mail.assets_messaging'), + ('include', 'mail.assets_public_livechat'), 'mail/static/src/services/messaging_service.js', # Framework JS 'bus/static/src/*.js', diff --git a/addons/im_livechat/static/src/models/messaging.js b/addons/im_livechat/static/src/models/messaging.js index 2dfbfa56d3e..de9e75765fb 100644 --- a/addons/im_livechat/static/src/models/messaging.js +++ b/addons/im_livechat/static/src/models/messaging.js @@ -1,10 +1,9 @@ /** @odoo-module **/ -import { addFields, patchFields } from '@mail/model/model_core'; -import { many, one } from '@mail/model/model_field'; -import { clear } from '@mail/model/model_field_command'; +import { addFields } from '@mail/model/model_core'; +import { many } from '@mail/model/model_field'; // ensure that the model definition is loaded before the patch -import '@mail/models/messaging'; +import '@mail/core_models/messaging'; addFields('Messaging', { /** @@ -14,18 +13,4 @@ addFields('Messaging', { inverse: 'messagingAsPinnedLivechat', readonly: true, }), - publicLivechatGlobal: one('PublicLivechatGlobal', { - isCausal: true, - }), -}); - -patchFields('Messaging', { - notificationHandler: { - compute() { - if (this.publicLivechatGlobal) { - return clear(); - } - return this._super(); - }, - }, }); diff --git a/addons/im_livechat/static/src/models/messaging_initializer.js b/addons/im_livechat/static/src/models/messaging_initializer.js index 19a19ccd503..ba166e2531e 100644 --- a/addons/im_livechat/static/src/models/messaging_initializer.js +++ b/addons/im_livechat/static/src/models/messaging_initializer.js @@ -6,16 +6,6 @@ import { insert } from '@mail/model/model_field_command'; import '@mail/models/messaging_initializer'; patchRecordMethods('MessagingInitializer', { - /** - * @override - */ - async performInitRpc() { - if (this.messaging.publicLivechatGlobal) { - return {}; - } else { - return this._super(); - } - }, /** * @override * @param {Object[]} [param0.channel_livechat=[]] diff --git a/addons/im_livechat/static/src/models/chatbot.js b/addons/im_livechat/static/src/public_models/chatbot.js similarity index 100% rename from addons/im_livechat/static/src/models/chatbot.js rename to addons/im_livechat/static/src/public_models/chatbot.js diff --git a/addons/im_livechat/static/src/models/chatbot_step.js b/addons/im_livechat/static/src/public_models/chatbot_step.js similarity index 100% rename from addons/im_livechat/static/src/models/chatbot_step.js rename to addons/im_livechat/static/src/public_models/chatbot_step.js diff --git a/addons/im_livechat/static/src/models/livechat_button_view.js b/addons/im_livechat/static/src/public_models/livechat_button_view.js similarity index 100% rename from addons/im_livechat/static/src/models/livechat_button_view.js rename to addons/im_livechat/static/src/public_models/livechat_button_view.js diff --git a/addons/im_livechat/static/src/models/livechat_operator.js b/addons/im_livechat/static/src/public_models/livechat_operator.js similarity index 100% rename from addons/im_livechat/static/src/models/livechat_operator.js rename to addons/im_livechat/static/src/public_models/livechat_operator.js diff --git a/addons/im_livechat/static/src/public_models/messaging.js b/addons/im_livechat/static/src/public_models/messaging.js new file mode 100644 index 00000000000..87432dc9aa8 --- /dev/null +++ b/addons/im_livechat/static/src/public_models/messaging.js @@ -0,0 +1,12 @@ +/** @odoo-module **/ + +import { addFields } from '@mail/model/model_core'; +import { one } from '@mail/model/model_field'; +// ensure that the model definition is loaded before the patch +import '@mail/core_models/messaging'; + +addFields('Messaging', { + publicLivechatGlobal: one('PublicLivechatGlobal', { + isCausal: true, + }), +}); diff --git a/addons/im_livechat/static/src/models/public_livechat.js b/addons/im_livechat/static/src/public_models/public_livechat.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat.js rename to addons/im_livechat/static/src/public_models/public_livechat.js diff --git a/addons/im_livechat/static/src/models/public_livechat_feedback_view.js b/addons/im_livechat/static/src/public_models/public_livechat_feedback_view.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_feedback_view.js rename to addons/im_livechat/static/src/public_models/public_livechat_feedback_view.js diff --git a/addons/im_livechat/static/src/models/public_livechat_global.js b/addons/im_livechat/static/src/public_models/public_livechat_global.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_global.js rename to addons/im_livechat/static/src/public_models/public_livechat_global.js diff --git a/addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js b/addons/im_livechat/static/src/public_models/public_livechat_global_notification_handler.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js rename to addons/im_livechat/static/src/public_models/public_livechat_global_notification_handler.js diff --git a/addons/im_livechat/static/src/models/public_livechat_message.js b/addons/im_livechat/static/src/public_models/public_livechat_message.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_message.js rename to addons/im_livechat/static/src/public_models/public_livechat_message.js diff --git a/addons/im_livechat/static/src/models/public_livechat_view.js b/addons/im_livechat/static/src/public_models/public_livechat_view.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_view.js rename to addons/im_livechat/static/src/public_models/public_livechat_view.js diff --git a/addons/im_livechat/static/src/models/public_livechat_window.js b/addons/im_livechat/static/src/public_models/public_livechat_window.js similarity index 100% rename from addons/im_livechat/static/src/models/public_livechat_window.js rename to addons/im_livechat/static/src/public_models/public_livechat_window.js diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 029f472964d..9b056fff56f 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -114,8 +114,12 @@ For more specific needs, you may also assign custom-defined actions 'installable': True, 'application': True, 'assets': { - 'mail.assets_messaging': [ + 'mail.assets_core_messaging': [ 'mail/static/src/model/*.js', + 'mail/static/src/core_models/*.js', + ], + 'mail.assets_messaging': [ + ('include', 'mail.assets_core_messaging'), 'mail/static/src/models/*.js', 'mail/static/src/models_data/*.js', 'mail/static/lib/selfie_segmentation/selfie_segmentation.js', @@ -173,14 +177,6 @@ For more specific needs, you may also assign custom-defined actions 'web/static/src/legacy/utils.js', 'web/static/src/legacy/xml/base.xml', ], - 'web.assets_frontend': [ - 'mail/static/src/utils/*.js', - 'mail/static/src/js/emojis.js', - 'mail/static/src/js/utils.js', - 'mail/static/src/component_hooks/*.js', - ('include', 'mail.assets_messaging'), - 'mail/static/src/services/messaging_service.js', - ], 'web._assets_primary_variables': [ 'mail/static/src/scss/variables/primary_variables.scss', ], diff --git a/addons/mail/static/src/models/device.js b/addons/mail/static/src/core_models/device.js similarity index 100% rename from addons/mail/static/src/models/device.js rename to addons/mail/static/src/core_models/device.js diff --git a/addons/mail/static/src/models/locale.js b/addons/mail/static/src/core_models/locale.js similarity index 100% rename from addons/mail/static/src/models/locale.js rename to addons/mail/static/src/core_models/locale.js diff --git a/addons/mail/static/src/core_models/messaging.js b/addons/mail/static/src/core_models/messaging.js new file mode 100644 index 00000000000..942ab43b020 --- /dev/null +++ b/addons/mail/static/src/core_models/messaging.js @@ -0,0 +1,115 @@ +/** @odoo-module **/ + +import { registerModel } from '@mail/model/model_core'; +import { attr, many, one } from '@mail/model/model_field'; +import { makeDeferred } from '@mail/utils/deferred'; + +import { browser } from '@web/core/browser/browser'; + +const { EventBus } = owl; + +registerModel({ + name: 'Messaging', + lifecycleHooks: { + _created() { + odoo.__DEBUG__.messaging = this; + }, + _willDelete() { + delete odoo.__DEBUG__.messaging; + }, + }, + recordMethods: { + /** + * Perform a rpc call and return a promise resolving to the result. + * + * @param {Object} params + * @return {any} + */ + async rpc(params, options = {}) { + if (params.route) { + const { route, params: rpcParameters } = params; + const { shadow: silent, ...rpcSettings } = options; + return this.env.services.rpc(route, rpcParameters, { silent, ...rpcSettings }); + } else { + const { args, method, model, kwargs = {} } = params; + const { domain, fields, groupBy } = kwargs; + + const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm; + switch (method) { + case 'create': + return ormService.create(model, args[0], kwargs); + case 'read': + return ormService.read(model, args[0], args.length > 1 ? args[1] : undefined, kwargs); + case 'read_group': + return ormService.readGroup(model, domain, fields, groupBy, kwargs); + case 'search': + return ormService.search(model, args[0], kwargs); + case 'search_read': + return ormService.searchRead(model, domain, fields, kwargs); + case 'unlink': + return ormService.unlink(model, args[0], kwargs); + case 'write': + return ormService.write(model, args[0], args[1], kwargs); + default: + return ormService.call(model, method, args, kwargs); + } + } + }, + /** + * Starts messaging and related records. + */ + async start() { + this.update({ isInitialized: true }); + this.initializedPromise.resolve(); + }, + }, + fields: { + /** + * Inverse of the messaging field present on all models. This field + * therefore contains all existing records. + */ + allRecords: many('Record', { + inverse: 'messaging', + isCausal: true, + }), + browser: attr({ + compute() { + return browser; + }, + }), + device: one('Device', { + default: {}, + isCausal: true, + readonly: true, + }), + /** + * Promise that will be resolved when messaging is initialized. + */ + initializedPromise: attr({ + compute() { + return makeDeferred(); + }, + required: true, + }), + isInitialized: attr({ + default: false, + }), + locale: one('Locale', { + default: {}, + isCausal: true, + readonly: true, + }), + /** + * Determines the bus that is used to communicate messaging events. + */ + messagingBus: attr({ + compute() { + if (this.messagingBus) { + return; // avoid overwrite if already provided (example in tests) + } + return new EventBus(); + }, + required: true, + }), + }, +}); diff --git a/addons/mail/static/src/models/record.js b/addons/mail/static/src/core_models/record.js similarity index 100% rename from addons/mail/static/src/models/record.js rename to addons/mail/static/src/core_models/record.js diff --git a/addons/mail/static/src/models/messaging.js b/addons/mail/static/src/models/messaging.js index d4fb532525e..be742a404ed 100644 --- a/addons/mail/static/src/models/messaging.js +++ b/addons/mail/static/src/models/messaging.js @@ -1,496 +1,412 @@ /** @odoo-module **/ -import { registerModel } from '@mail/model/model_core'; +import { addFields, addOnChanges, addRecordMethods, patchLifecycleHooks, patchRecordMethods } from '@mail/model/model_core'; import { attr, many, one } from '@mail/model/model_field'; import { clear } from '@mail/model/model_field_command'; -import { makeDeferred } from '@mail/utils/deferred'; +// ensure that the model definition is loaded before the patch +import '@mail/core_models/messaging'; -import { browser } from '@web/core/browser/browser'; - -const { EventBus } = owl; - -registerModel({ - name: 'Messaging', - lifecycleHooks: { - _created() { - odoo.__DEBUG__.messaging = this; - this.refreshIsNotificationPermissionDefault(); - }, - _willDelete() { - this.env.services['im_status'].unregisterFromImStatus('res.partner'); - this.env.services['im_status'].unregisterFromImStatus('mail.guest'); - this.env.bus.removeEventListener('window_focus', this._handleGlobalWindowFocus); - delete odoo.__DEBUG__.messaging; - }, +patchLifecycleHooks('Messaging', { + _created() { + this._super(); + this.refreshIsNotificationPermissionDefault(); }, - recordMethods: { - /** - * Starts messaging and related records. - */ - async start() { - this.env.bus.addEventListener('window_focus', this._handleGlobalWindowFocus); - await this.initializer.start(); - if (!this.exists()) { - return; + _willDelete() { + this.env.services['im_status'].unregisterFromImStatus('res.partner'); + this.env.services['im_status'].unregisterFromImStatus('mail.guest'); + this.env.bus.removeEventListener('window_focus', this._handleGlobalWindowFocus); + this._super(); + }, +}); + +patchRecordMethods('Messaging', { + /** + * Starts messaging and related records. + */ + async start() { + this.env.bus.addEventListener('window_focus', this._handleGlobalWindowFocus); + await this.initializer.start(); + if (!this.exists()) { + return; + } + if (this.notificationHandler) { + this.notificationHandler.start(); + } + await this._super(); + }, +}); + +addRecordMethods('Messaging', { + /** + * Executes the provided functions in order, but with a potential delay between + * them if they take too much time. This is done in order to avoid blocking the + * main thread for too long. + * + * @param {function[]} functions + */ + async executeGracefully(functions) { + let date = new Date(); + for (const func of functions) { + if (new Date() - date > 100) { + await new Promise(resolve => setTimeout(resolve, this.isInQUnitTest ? 0 : 50)); + date = new Date(); } - if (this.notificationHandler) { - this.notificationHandler.start(); - } - this.update({ isInitialized: true }); - this.initializedPromise.resolve(); - }, - /** - * Executes the provided functions in order, but with a potential delay between - * them if they take too much time. This is done in order to avoid blocking the - * main thread for too long. - * - * @param {function[]} functions - */ - async executeGracefully(functions) { - let date = new Date(); - for (const func of functions) { - if (new Date() - date > 100) { - await new Promise(resolve => setTimeout(resolve, this.isInQUnitTest ? 0 : 50)); - date = new Date(); - } - await func(); - } - }, - /** - * Open the form view of the record with provided id and model. - * Gets the chat with the provided person and returns it. - * - * If a chat is not appropriate, a notification is displayed instead. - * - * @param {Object} param0 - * @param {integer} [param0.partnerId] - * @param {integer} [param0.userId] - * @param {Object} [options] - * @returns {Channel|undefined} - */ - async getChat({ partnerId, userId }) { - if (userId) { - const user = this.messaging.models['User'].insert({ id: userId }); - return user.getChat(); - } - if (partnerId) { - const partner = this.messaging.models['Partner'].insert({ id: partnerId }); - return partner.getChat(); - } - }, - /** - * Display a notification to the user. - * - * @param {Object} params - * @param {string} [params.message] - * @param {string} [params.subtitle] - * @param {Object[]} [params.buttons] - * @param {boolean} [params.sticky] - * @param {string} [params.type] - * @param {string} [params.className] - * @param {function} [params.onClose] - * @return {number} the id of the notification. - */ - notify(params) { - const { message, ...options } = params; - return this.env.services.notification.add(message, options); - }, - /** - * Opens a chat with the provided person and returns it. - * - * If a chat is not appropriate, a notification is displayed instead. - * - * @param {Object} person forwarded to @see `getChat()` - * @param {Object} [options] forwarded to @see `Thread:open()` - */ - async openChat(person, options) { - const chat = await this.getChat(person); - if (!this.exists() || !chat) { - return; - } - await chat.thread.open(options); - if (!this.exists()) { - return; - } - }, - /** - * Opens the form view of the record with provided id and model. - * - * @param {Object} param0 - * @param {integer} param0.id - * @param {string} param0.model - */ - async openDocument({ id, model }) { - this.env.services.action.doAction({ - type: 'ir.actions.act_window', - res_model: model, - views: [[false, 'form']], - res_id: id, - }); - if (this.messaging.device.isSmall) { - // When opening documents chat windows need to be closed - this.messaging.chatWindowManager.closeAll(); - // messaging menu has a higher z-index than views so it must - // be closed to ensure the visibility of the view - this.messaging.messagingMenu.close(); - } - }, - /** - * Opens the most appropriate view that is a profile for provided id and - * model. - * - * @param {Object} param0 - * @param {integer} param0.id - * @param {string} param0.model - */ - async openProfile({ id, model }) { - if (model === 'res.partner') { - const partner = this.messaging.models['Partner'].insert({ id }); - return partner.openProfile(); - } - if (model === 'res.users') { - const user = this.messaging.models['User'].insert({ id }); - return user.openProfile(); - } - if (model === 'mail.channel') { - let channel = this.messaging.models['Thread'].findFromIdentifyingData({ id, model: 'mail.channel' }); - if (!channel) { - const res = await this.messaging.models['Thread'].performRpcChannelInfo({ ids: [id] }); - if (!this.exists()) { - return; - } - channel = res[0]; - } - if (!channel) { - this.messaging.notify({ - message: this.env._t("You can only open the profile of existing channels."), - type: 'warning', - }); + await func(); + } + }, + /** + * Open the form view of the record with provided id and model. + * Gets the chat with the provided person and returns it. + * + * If a chat is not appropriate, a notification is displayed instead. + * + * @param {Object} param0 + * @param {integer} [param0.partnerId] + * @param {integer} [param0.userId] + * @param {Object} [options] + * @returns {Channel|undefined} + */ + async getChat({ partnerId, userId }) { + if (userId) { + const user = this.messaging.models['User'].insert({ id: userId }); + return user.getChat(); + } + if (partnerId) { + const partner = this.messaging.models['Partner'].insert({ id: partnerId }); + return partner.getChat(); + } + }, + /** + * Display a notification to the user. + * + * @param {Object} params + * @param {string} [params.message] + * @param {string} [params.subtitle] + * @param {Object[]} [params.buttons] + * @param {boolean} [params.sticky] + * @param {string} [params.type] + * @param {string} [params.className] + * @param {function} [params.onClose] + * @return {number} the id of the notification. + */ + notify(params) { + const { message, ...options } = params; + return this.env.services.notification.add(message, options); + }, + /** + * Opens a chat with the provided person and returns it. + * + * If a chat is not appropriate, a notification is displayed instead. + * + * @param {Object} person forwarded to @see `getChat()` + * @param {Object} [options] forwarded to @see `Thread:open()` + */ + async openChat(person, options) { + const chat = await this.getChat(person); + if (!this.exists() || !chat) { + return; + } + await chat.thread.open(options); + if (!this.exists()) { + return; + } + }, + /** + * Opens the form view of the record with provided id and model. + * + * @param {Object} param0 + * @param {integer} param0.id + * @param {string} param0.model + */ + async openDocument({ id, model }) { + this.env.services.action.doAction({ + type: 'ir.actions.act_window', + res_model: model, + views: [[false, 'form']], + res_id: id, + }); + if (this.messaging.device.isSmall) { + // When opening documents chat windows need to be closed + this.messaging.chatWindowManager.closeAll(); + // messaging menu has a higher z-index than views so it must + // be closed to ensure the visibility of the view + this.messaging.messagingMenu.close(); + } + }, + /** + * Opens the most appropriate view that is a profile for provided id and + * model. + * + * @param {Object} param0 + * @param {integer} param0.id + * @param {string} param0.model + */ + async openProfile({ id, model }) { + if (model === 'res.partner') { + const partner = this.messaging.models['Partner'].insert({ id }); + return partner.openProfile(); + } + if (model === 'res.users') { + const user = this.messaging.models['User'].insert({ id }); + return user.openProfile(); + } + if (model === 'mail.channel') { + let channel = this.messaging.models['Thread'].findFromIdentifyingData({ id, model: 'mail.channel' }); + if (!channel) { + const res = await this.messaging.models['Thread'].performRpcChannelInfo({ ids: [id] }); + if (!this.exists()) { return; } - return channel.openProfile(); + channel = res[0]; } - return this.messaging.openDocument({ id, model }); - }, - /** - * Perform a rpc call and return a promise resolving to the result. - * - * @param {Object} params - * @return {any} - */ - async rpc(params, options = {}) { - if (params.route) { - const { route, params: rpcParameters } = params; - const { shadow: silent, ...rpcSettings } = options; - return this.env.services.rpc(route, rpcParameters, { silent, ...rpcSettings }); - } else { - const { args, method, model, kwargs = {} } = params; - const { domain, fields, groupBy } = kwargs; - - const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm; - switch (method) { - case 'create': - return ormService.create(model, args[0], kwargs); - case 'read': - return ormService.read(model, args[0], args.length > 1 ? args[1] : undefined, kwargs); - case 'read_group': - return ormService.readGroup(model, domain, fields, groupBy, kwargs); - case 'search': - return ormService.search(model, args[0], kwargs); - case 'search_read': - return ormService.searchRead(model, domain, fields, kwargs); - case 'unlink': - return ormService.unlink(model, args[0], kwargs); - case 'write': - return ormService.write(model, args[0], args[1], kwargs); - default: - return ormService.call(model, method, args, kwargs); - } + if (!channel) { + this.messaging.notify({ + message: this.env._t("You can only open the profile of existing channels."), + type: 'warning', + }); + return; } - }, - /** - * Refreshes the value of `isNotificationPermissionDefault`. - * - * Must be called in flux-specific way because the browser does not - * provide an API to detect when this value changes. - */ - refreshIsNotificationPermissionDefault() { - const browserNotification = this.messaging.browser.Notification; - this.update({ - isNotificationPermissionDefault: Boolean(browserNotification) && browserNotification.permission === 'default', - }); - }, - updateImStatusRegistration() { - const partnerIds = []; - for (const partner of this.models['Partner'].all()) { - if (partner.im_status !== 'im_partner' && !partner.is_public) { - partnerIds.push(partner.id); - } - } - const guestIds = []; - for (const guest of this.models['Guest'].all()) { - guestIds.push(guest.id); - } - this.env.services['im_status'].registerToImStatus('res.partner', partnerIds); - this.env.services['im_status'].registerToImStatus('mail.guest', guestIds); - }, - /** - * @private - */ - _handleGlobalWindowFocus() { - this.update({ outOfFocusUnreadMessageCounter: 0 }); - this.env.bus.trigger('set_title_part', { - part: '_chat', - }); - }, - /** - * @private - */ - _onChangeAllCurrentClientThreads() { - if (this.isInitialized) { - this.env.services.bus_service.forceUpdateChannels(); - } - }, - /** - * @private - */ - _onChangeAllPersonas() { - if (this.isInitialized) { - this.updateImStatusRegisterThrottle.do(); - } - }, - /** - * @private - */ - _onChangeRingingThreads() { - if (this.ringingThreads && this.ringingThreads.length > 0) { - this.soundEffects.incomingCall.play({ loop: true }); - } else { - this.soundEffects.incomingCall.stop(); - } - }, + return channel.openProfile(); + } + return this.messaging.openDocument({ id, model }); }, - fields: { - allMailboxes: many('Mailbox', { - inverse: 'messagingAsAnyMailbox', - }), - allPersonas: many('Persona', { - inverse: 'messagingAsAnyPersona', - }), - /** - * Inverse of the messaging field present on all models. This field - * therefore contains all existing records. - */ - allRecords: many('Record', { - inverse: 'messaging', - isCausal: true, - }), - /** - * This field contains all current client channels. - */ - allCurrentClientThreads: many('Thread', { - inverse: 'messagingAsAllCurrentClientThreads', - }), - browser: attr({ - compute() { - return browser; - }, - }), - cannedResponses: many('CannedResponse'), - chatWindowManager: one('ChatWindowManager', { - default: {}, - isCausal: true, - readonly: true, - }), - /** - * Determines which message view is currently clicked, if any. - */ - clickedMessageView: one('MessageView', { - inverse: 'messagingAsClickedMessageView', - }), - commands: many('ChannelCommand'), - companyName: attr(), - currentGuest: one('Guest'), - currentPartner: one('Partner'), - currentUser: one('User'), - device: one('Device', { - default: {}, - isCausal: true, - readonly: true, - }), - dialogManager: one('DialogManager', { - default: {}, - isCausal: true, - readonly: true, - }), - /** - * Determines whether animations should be disabled. - */ - disableAnimation: attr({ - default: false, - }), - discuss: one('Discuss', { - default: {}, - isCausal: true, - readonly: true, - }), - emojiRegistry: one('EmojiRegistry', { - default: {}, - isCausal: true, - readonly: true, - }), - hasLinkPreviewFeature: attr(), - history: one('Mailbox', { - default: {}, - inverse: 'messagingAsHistory', - }), - inbox: one('Mailbox', { - default: {}, - inverse: 'messagingAsInbox', - }), - /** - * Promise that will be resolved when messaging is initialized. - */ - initializedPromise: attr({ - compute() { - return makeDeferred(); - }, - required: true, - }), - initializer: one('MessagingInitializer', { - default: {}, - isCausal: true, - readonly: true, - }), - internalUserGroupId: attr(), - isCurrentUserGuest: attr({ - compute() { - return Boolean(!this.currentPartner && this.currentGuest); - }, - }), - isInitialized: attr({ - default: false, - }), - isInQUnitTest: attr({ - default: false, - }), - isNotificationBlocked: attr({ - compute() { - const windowNotification = this.browser.Notification; - return ( - windowNotification && - windowNotification.permission !== 'granted' && - !this.isNotificationPermissionDefault - ); - }, - }), - /** - * States whether browser Notification Permission is currently in its - * 'default' state. This means it is allowed to make a request to the - * user to enable notifications. - */ - isNotificationPermissionDefault: attr(), - locale: one('Locale', { - default: {}, - isCausal: true, - readonly: true, - }), - /** - * Determines after how much time in ms a "loading" indicator should be - * shown. Useful to avoid flicker for almost instant loading. - */ - loadingBaseDelayDuration: attr({ - default: 400, - }), - /** - * Determines the bus that is used to communicate messaging events. - */ - messagingBus: attr({ - compute() { - if (this.messagingBus) { - return; // avoid overwrite if already provided (example in tests) - } - return new EventBus(); - }, - required: true, - }), - messagingMenu: one('MessagingMenu', { - default: {}, - isCausal: true, - }), - notificationHandler: one('MessagingNotificationHandler', { - compute() { - return {}; - }, - isCausal: true, - }), - outOfFocusUnreadMessageCounter: attr({ - default: 0, - }), - partnerRoot: one('Partner'), - popoverManager: one('PopoverManager', { - default: {}, - isCausal: true, - readonly: true, - }), - /** - * Threads for which the current partner has a pending invitation. - * It is computed from the inverse relation for performance reasons. - */ - ringingThreads: many('Thread', { - inverse: 'messagingAsRingingThread', - }), - rtc: one('Rtc', { - default: {}, - isCausal: true, - readonly: true, - }), - callInviteRequestPopups: many('CallInviteRequestPopup', { - compute() { - if (this.ringingThreads.length === 0) { - return clear(); - } - return this.ringingThreads.map(thread => thread.callInviteRequestPopup); - }, - isCausal: true, - }), - soundEffects: one('SoundEffects', { - default: {}, - isCausal: true, - readonly: true, - }), - starred: one('Mailbox', { - default: {}, - inverse: 'messagingAsStarred', - }), - userNotificationManager: one('UserNotificationManager', { - default: {}, - isCausal: true, - readonly: true, - }), - updateImStatusRegisterThrottle: one('Throttle', { - compute() { - return { func: this.updateImStatusRegistration }; - }, - inverse: 'messagingAsUpdateImStatusRegister', - }), - userSetting: one('UserSetting', { - default: {}, - isCausal: true, - }), + /** + * Refreshes the value of `isNotificationPermissionDefault`. + * + * Must be called in flux-specific way because the browser does not + * provide an API to detect when this value changes. + */ + refreshIsNotificationPermissionDefault() { + const browserNotification = this.messaging.browser.Notification; + this.update({ + isNotificationPermissionDefault: Boolean(browserNotification) && browserNotification.permission === 'default', + }); + }, + updateImStatusRegistration() { + const partnerIds = []; + for (const partner of this.models['Partner'].all()) { + if (partner.im_status !== 'im_partner' && !partner.is_public) { + partnerIds.push(partner.id); + } + } + const guestIds = []; + for (const guest of this.models['Guest'].all()) { + guestIds.push(guest.id); + } + this.env.services['im_status'].registerToImStatus('res.partner', partnerIds); + this.env.services['im_status'].registerToImStatus('mail.guest', guestIds); + }, + /** + * @private + */ + _handleGlobalWindowFocus() { + this.update({ outOfFocusUnreadMessageCounter: 0 }); + this.env.bus.trigger('set_title_part', { + part: '_chat', + }); + }, + /** + * @private + */ + _onChangeAllCurrentClientThreads() { + if (this.isInitialized) { + this.env.services.bus_service.forceUpdateChannels(); + } + }, + /** + * @private + */ + _onChangeAllPersonas() { + if (this.isInitialized) { + this.updateImStatusRegisterThrottle.do(); + } + }, + /** + * @private + */ + _onChangeRingingThreads() { + if (this.ringingThreads && this.ringingThreads.length > 0) { + this.soundEffects.incomingCall.play({ loop: true }); + } else { + this.soundEffects.incomingCall.stop(); + } }, - onChanges: [ - { - dependencies: ['ringingThreads'], - methodName: '_onChangeRingingThreads', - }, - { - dependencies: ['allCurrentClientThreads'], - methodName: '_onChangeAllCurrentClientThreads', - }, - { - dependencies: ['allPersonas'], - methodName: '_onChangeAllPersonas', - }, - ], }); + +addFields('Messaging', { + allMailboxes: many('Mailbox', { + inverse: 'messagingAsAnyMailbox', + }), + allPersonas: many('Persona', { + inverse: 'messagingAsAnyPersona', + }), + /** + * This field contains all current client channels. + */ + allCurrentClientThreads: many('Thread', { + inverse: 'messagingAsAllCurrentClientThreads', + }), + cannedResponses: many('CannedResponse'), + chatWindowManager: one('ChatWindowManager', { + default: {}, + isCausal: true, + readonly: true, + }), + /** + * Determines which message view is currently clicked, if any. + */ + clickedMessageView: one('MessageView', { + inverse: 'messagingAsClickedMessageView', + }), + commands: many('ChannelCommand'), + companyName: attr(), + currentGuest: one('Guest'), + currentPartner: one('Partner'), + currentUser: one('User'), + dialogManager: one('DialogManager', { + default: {}, + isCausal: true, + readonly: true, + }), + /** + * Determines whether animations should be disabled. + */ + disableAnimation: attr({ + default: false, + }), + discuss: one('Discuss', { + default: {}, + isCausal: true, + readonly: true, + }), + emojiRegistry: one('EmojiRegistry', { + default: {}, + isCausal: true, + readonly: true, + }), + hasLinkPreviewFeature: attr(), + history: one('Mailbox', { + default: {}, + inverse: 'messagingAsHistory', + }), + inbox: one('Mailbox', { + default: {}, + inverse: 'messagingAsInbox', + }), + initializer: one('MessagingInitializer', { + default: {}, + isCausal: true, + readonly: true, + }), + internalUserGroupId: attr(), + isCurrentUserGuest: attr({ + compute() { + return Boolean(!this.currentPartner && this.currentGuest); + }, + }), + isInQUnitTest: attr({ + default: false, + }), + isNotificationBlocked: attr({ + compute() { + const windowNotification = this.browser.Notification; + return ( + windowNotification && + windowNotification.permission !== 'granted' && + !this.isNotificationPermissionDefault + ); + }, + }), + /** + * States whether browser Notification Permission is currently in its + * 'default' state. This means it is allowed to make a request to the + * user to enable notifications. + */ + isNotificationPermissionDefault: attr(), + /** + * Determines after how much time in ms a "loading" indicator should be + * shown. Useful to avoid flicker for almost instant loading. + */ + loadingBaseDelayDuration: attr({ + default: 400, + }), + messagingMenu: one('MessagingMenu', { + default: {}, + isCausal: true, + }), + notificationHandler: one('MessagingNotificationHandler', { + compute() { + return {}; + }, + isCausal: true, + }), + outOfFocusUnreadMessageCounter: attr({ + default: 0, + }), + partnerRoot: one('Partner'), + popoverManager: one('PopoverManager', { + default: {}, + isCausal: true, + readonly: true, + }), + /** + * Threads for which the current partner has a pending invitation. + * It is computed from the inverse relation for performance reasons. + */ + ringingThreads: many('Thread', { + inverse: 'messagingAsRingingThread', + }), + rtc: one('Rtc', { + default: {}, + isCausal: true, + readonly: true, + }), + callInviteRequestPopups: many('CallInviteRequestPopup', { + compute() { + if (this.ringingThreads.length === 0) { + return clear(); + } + return this.ringingThreads.map(thread => thread.callInviteRequestPopup); + }, + isCausal: true, + }), + soundEffects: one('SoundEffects', { + default: {}, + isCausal: true, + readonly: true, + }), + starred: one('Mailbox', { + default: {}, + inverse: 'messagingAsStarred', + }), + userNotificationManager: one('UserNotificationManager', { + default: {}, + isCausal: true, + readonly: true, + }), + updateImStatusRegisterThrottle: one('Throttle', { + compute() { + return { func: this.updateImStatusRegistration }; + }, + inverse: 'messagingAsUpdateImStatusRegister', + }), + userSetting: one('UserSetting', { + default: {}, + isCausal: true, + }), +}); + +addOnChanges('Messaging', [ + { + dependencies: ['ringingThreads'], + methodName: '_onChangeRingingThreads', + }, + { + dependencies: ['allCurrentClientThreads'], + methodName: '_onChangeAllCurrentClientThreads', + }, + { + dependencies: ['allPersonas'], + methodName: '_onChangeAllPersonas', + }, +]); diff --git a/addons/snailmail/static/src/models/messaging.js b/addons/snailmail/static/src/models/messaging.js index c28dc9c4e99..8dc66ce491b 100644 --- a/addons/snailmail/static/src/models/messaging.js +++ b/addons/snailmail/static/src/models/messaging.js @@ -3,7 +3,7 @@ import { addFields, addRecordMethods } from '@mail/model/model_core'; import { attr } from '@mail/model/model_field'; // ensure that the model definition is loaded before the patch -import '@mail/models/messaging'; +import '@mail/core_models/messaging'; addRecordMethods('Messaging', { async fetchSnailmailCreditsUrl() { diff --git a/addons/website_livechat/__manifest__.py b/addons/website_livechat/__manifest__.py index 2ddfc6d7ef3..129b09e8b81 100644 --- a/addons/website_livechat/__manifest__.py +++ b/addons/website_livechat/__manifest__.py @@ -28,6 +28,9 @@ Allow website visitors to chat with the collaborators. This module also brings a 'mail.assets_messaging': [ 'website_livechat/static/src/models/*.js', ], + 'mail.assets_public_livechat': [ + 'website_livechat/static/src/public_models/*.js', + ], 'mail.assets_discuss_public': [ 'website_livechat/static/src/components/*/*', ], diff --git a/addons/website_livechat/static/src/models/chatbot.js b/addons/website_livechat/static/src/public_models/chatbot.js similarity index 95% rename from addons/website_livechat/static/src/models/chatbot.js rename to addons/website_livechat/static/src/public_models/chatbot.js index 2d0f3101a0a..4a5d921b909 100644 --- a/addons/website_livechat/static/src/models/chatbot.js +++ b/addons/website_livechat/static/src/public_models/chatbot.js @@ -3,7 +3,7 @@ import { addFields, patchFields } from '@mail/model/model_core'; import { attr } from '@mail/model/model_field'; // ensure that the model definition is loaded before the patch -import '@im_livechat/models/chatbot'; +import '@im_livechat/public_models/chatbot'; addFields('Chatbot', { isWebsiteLivechatTourFlow: attr({ diff --git a/addons/website_livechat/static/src/models/livechat_button_view.js b/addons/website_livechat/static/src/public_models/livechat_button_view.js similarity index 97% rename from addons/website_livechat/static/src/models/livechat_button_view.js rename to addons/website_livechat/static/src/public_models/livechat_button_view.js index b3c42b38745..b8d89d34bb5 100644 --- a/addons/website_livechat/static/src/models/livechat_button_view.js +++ b/addons/website_livechat/static/src/public_models/livechat_button_view.js @@ -3,7 +3,7 @@ import { patchFields, patchRecordMethods } from '@mail/model/model_core'; import { clear } from '@mail/model/model_field_command'; // ensure that the model definition is loaded before the patch -import '@im_livechat/models/livechat_button_view'; +import '@im_livechat/public_models/livechat_button_view'; import { set_cookie, unaccent } from 'web.utils'; diff --git a/addons/website_livechat/static/src/models/public_livechat_global.js b/addons/website_livechat/static/src/public_models/public_livechat_global.js similarity index 94% rename from addons/website_livechat/static/src/models/public_livechat_global.js rename to addons/website_livechat/static/src/public_models/public_livechat_global.js index 7dfffe67f15..cd061c6514c 100644 --- a/addons/website_livechat/static/src/models/public_livechat_global.js +++ b/addons/website_livechat/static/src/public_models/public_livechat_global.js @@ -2,7 +2,7 @@ import { patchRecordMethods } from '@mail/model/model_core'; // ensure that the model definition is loaded before the patch -import '@im_livechat/models/public_livechat_global'; +import '@im_livechat/public_models/public_livechat_global'; import { set_cookie } from 'web.utils'; diff --git a/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js b/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js index a074b29584b..3615fa4d9f1 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js @@ -4,7 +4,7 @@ import tour from "web_tour.tour"; import { patchRecordMethods } from '@mail/model/model_core'; // ensure that the model definition is loaded before the patch -import '@im_livechat/models/livechat_button_view'; +import '@im_livechat/public_models/livechat_button_view'; patchRecordMethods('LivechatButtonView', { /** diff --git a/addons/website_livechat/static/tests/tours/website_livechat_common.js b/addons/website_livechat/static/tests/tours/website_livechat_common.js index 37b58f76e6c..0aa7f624606 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_common.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_common.js @@ -3,7 +3,7 @@ odoo.define('website_livechat.tour_common', function (require) { const { patchRecordMethods } = require('@mail/model/model_core'); // ensure that the model definition is loaded before the patch -require('@im_livechat/models/livechat_button_view'); +require('@im_livechat/public_models/livechat_button_view'); const { Markup } = require('web.utils');