[IMP] mail, *: introduce public livechat bundle

*: 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) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-09-22 15:50:12 +02:00
parent 47541460f5
commit 5b59a07729
29 changed files with 554 additions and 529 deletions
+1 -1
View File
@@ -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', {
/**
+11 -3
View File
@@ -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',
@@ -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();
},
},
});
@@ -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=[]]
@@ -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,
}),
});
+5 -9
View File
@@ -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',
],
@@ -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,
}),
},
});
+398 -482
View File
@@ -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',
},
]);
@@ -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() {
+3
View File
@@ -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/*/*',
],
@@ -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({
@@ -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';
@@ -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';
@@ -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', {
/**
@@ -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');