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');