summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-08-28 12:53:38 +0000
committerSébastien Theys <seb@odoo.com>2020-08-28 16:15:15 +0000
commit17611f4adc5e950b0a75a2a6615b1da3c956387f (patch)
treeede6cbc0bc67c1dceed3b5ca6c1bb3e08389675c /addons
parent5ba64925fd846e9a9a4884913e3d1004d05ecdf6 (diff)
[FIX] mail, *: fix opens and redirects, adapt messaging menu close
task-2244279 task-2262862 task-2274993 task-2277537 task-2282238 closes odoo/odoo#56745 X-original-commit: 109f202 Related: odoo/enterprise#12761 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com> Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/hr/__manifest__.py5
-rw-r--r--addons/hr/static/src/bugfix/bugfix.js10
-rw-r--r--addons/hr/static/src/bugfix/bugfix.scss6
-rw-r--r--addons/hr/static/src/bugfix/bugfix.xml11
-rw-r--r--addons/hr/static/src/bugfix/bugfix_tests.js18
-rw-r--r--addons/hr/static/src/js/chat.js43
-rw-r--r--addons/hr/static/src/js/many2one_avatar_employee.js36
-rw-r--r--addons/hr/static/src/models/employee/employee.js204
-rw-r--r--addons/hr/static/src/models/messaging/messaging.js36
-rw-r--r--addons/hr/static/src/models/partner/partner.js63
-rw-r--r--addons/hr/static/src/models/user/user.js18
-rw-r--r--addons/hr/static/tests/helpers/mock_models.js34
-rw-r--r--addons/hr/static/tests/many2one_avatar_employee_tests.js172
-rw-r--r--addons/hr/views/hr_templates.xml9
-rw-r--r--addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js9
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.js27
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.xml1
-rw-r--r--addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js4
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js124
-rw-r--r--addons/mail/static/src/components/chatter/chatter.js29
-rw-r--r--addons/mail/static/src/components/chatter/chatter.xml2
-rw-r--r--addons/mail/static/src/components/composer/composer.js6
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.js12
-rw-r--r--addons/mail/static/src/components/discuss/discuss.js41
-rw-r--r--addons/mail/static/src/components/discuss/discuss.xml4
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js28
-rw-r--r--addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js2
-rw-r--r--addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js2
-rw-r--r--addons/mail/static/src/components/follower/follower.js5
-rw-r--r--addons/mail/static/src/components/follower/follower_tests.js16
-rw-r--r--addons/mail/static/src/components/message/message.js66
-rw-r--r--addons/mail/static/src/components/message/message.xml4
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js5
-rw-r--r--addons/mail/static/src/components/message_list/message_list.xml1
-rw-r--r--addons/mail/static/src/components/messaging_menu/messaging_menu.js47
-rw-r--r--addons/mail/static/src/components/messaging_menu/messaging_menu.xml2
-rw-r--r--addons/mail/static/src/components/notification_group/notification_group.js6
-rw-r--r--addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js1
-rw-r--r--addons/mail/static/src/components/notification_request/notification_request.js4
-rw-r--r--addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js5
-rw-r--r--addons/mail/static/src/components/thread_preview/thread_preview.js5
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view.js2
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view.xml1
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js4
-rw-r--r--addons/mail/static/src/js/many2one_avatar_user.js93
-rw-r--r--addons/mail/static/src/models/chat_window/chat_window.js40
-rw-r--r--addons/mail/static/src/models/chat_window_manager/chat_window_manager.js41
-rw-r--r--addons/mail/static/src/models/chatter/chatter.js22
-rw-r--r--addons/mail/static/src/models/composer/composer.js7
-rw-r--r--addons/mail/static/src/models/discuss/discuss.js74
-rw-r--r--addons/mail/static/src/models/follower/follower.js10
-rw-r--r--addons/mail/static/src/models/messaging/messaging.js140
-rw-r--r--addons/mail/static/src/models/messaging/messaging_tests.js96
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js171
-rw-r--r--addons/mail/static/src/models/notification_group/notification_group.js96
-rw-r--r--addons/mail/static/src/models/partner/partner.js71
-rw-r--r--addons/mail/static/src/models/thread/thread.js316
-rw-r--r--addons/mail/static/src/models/user/user.js148
-rw-r--r--addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js2
-rw-r--r--addons/mail/static/tests/many2one_avatar_user_tests.js40
-rw-r--r--addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js3
-rw-r--r--addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer_tests.js14
-rw-r--r--addons/sms/static/src/models/notification_group/notification_group.js53
-rw-r--r--addons/snailmail/static/src/models/notification_group/notification_group.js53
64 files changed, 1697 insertions, 923 deletions
diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py
index ada77010824..cb2942140ed 100644
--- a/addons/hr/__manifest__.py
+++ b/addons/hr/__manifest__.py
@@ -46,5 +46,8 @@
'installable': True,
'application': True,
'auto_install': False,
- 'qweb': ['static/src/xml/hr_templates.xml'],
+ 'qweb': [
+ 'static/src/bugfix/bugfix.xml',
+ 'static/src/xml/hr_templates.xml',
+ ],
}
diff --git a/addons/hr/static/src/bugfix/bugfix.js b/addons/hr/static/src/bugfix/bugfix.js
new file mode 100644
index 00000000000..0351046fab3
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.js
@@ -0,0 +1,10 @@
+/**
+ * This file allows introducing new JS modules without contaminating other files.
+ * This is useful when bug fixing requires adding such JS modules in stable
+ * versions of Odoo. Any module that is defined in this file should be isolated
+ * in its own file in master.
+ */
+odoo.define('hr/static/src/bugfix/bugfix.js', function (require) {
+'use strict';
+
+});
diff --git a/addons/hr/static/src/bugfix/bugfix.scss b/addons/hr/static/src/bugfix/bugfix.scss
new file mode 100644
index 00000000000..c4272e527b4
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.scss
@@ -0,0 +1,6 @@
+/**
+* This file allows introducing new styles without contaminating other files.
+* This is useful when bug fixing requires adding new components for instance in
+* stable versions of Odoo. Any style that is defined in this file should be isolated
+* in its own file in master.
+*/
diff --git a/addons/hr/static/src/bugfix/bugfix.xml b/addons/hr/static/src/bugfix/bugfix.xml
new file mode 100644
index 00000000000..c17906f75bb
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+<!--
+ This file allows introducing new static templates without contaminating other files.
+ This is useful when bug fixing requires adding new components for instance in stable
+ versions of Odoo. Any template that is defined in this file should be isolated
+ in its own file in master.
+-->
+
+</templates>
diff --git a/addons/hr/static/src/bugfix/bugfix_tests.js b/addons/hr/static/src/bugfix/bugfix_tests.js
new file mode 100644
index 00000000000..8b23d37282d
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix_tests.js
@@ -0,0 +1,18 @@
+odoo.define('hr/static/src/bugfix/bugfix_tests.js', function (require) {
+'use strict';
+
+/**
+ * This file allows introducing new QUnit test modules without contaminating
+ * other test files. This is useful when bug fixing requires adding new
+ * components for instance in stable versions of Odoo. Any test that is defined
+ * in this file should be isolated in its own file in master.
+ */
+QUnit.module('hr', {}, function () {
+QUnit.module('bugfix', {}, function () {
+QUnit.module('bugfix_tests.js', {
+
+});
+});
+});
+
+});
diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js
index b9985e3929f..0489ccceac0 100644
--- a/addons/hr/static/src/js/chat.js
+++ b/addons/hr/static/src/js/chat.js
@@ -22,11 +22,7 @@ odoo.define('hr.employee_chat', function (require) {
var self = this;
return this._super.apply(this, arguments).then(function () {
var $chat_button = self.$el.find('.o_employee_chat_btn');
- if (self.state.context.uid === self.state.data.user_id.res_id) { // Hide the button for yourself
- $chat_button.hide();
- } else {
- $chat_button.off('click').on('click', self._onOpenChat.bind(self));
- }
+ $chat_button.off('click').on('click', self._onOpenChat.bind(self));
});
},
@@ -38,9 +34,8 @@ odoo.define('hr.employee_chat', function (require) {
_onOpenChat: function (ev) {
ev.preventDefault();
ev.stopImmediatePropagation();
- this.trigger_up('open_chat', {
- partner_id: this.state.data.user_partner_id.res_id
- });
+ const env = Component.env;
+ env.messaging.openChat({ employeeId: this.state.data.id });
return true;
},
};
@@ -48,23 +43,9 @@ odoo.define('hr.employee_chat', function (require) {
// USAGE OF CHAT MIXIN IN FORM VIEWS
var EmployeeFormRenderer = FormRenderer.extend(ChatMixin);
- var EmployeeFormController = FormController.extend({
- custom_events: _.extend({}, FormController.prototype.custom_events, {
- open_chat: '_onOpenChat'
- }),
-
- _onOpenChat: function (ev) {
- const env = Component.env;
- const partner = env.models['mail.partner'].insert({
- id: ev.data.partner_id,
- });
- partner.openChat();
- },
- });
-
var EmployeeFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
- Controller: EmployeeFormController,
+ Controller: FormController,
Renderer: EmployeeFormRenderer
}),
});
@@ -80,23 +61,9 @@ odoo.define('hr.employee_chat', function (require) {
}),
});
- var EmployeeKanbanController = KanbanController.extend({
- custom_events: _.extend({}, KanbanController.prototype.custom_events, {
- open_chat: '_onOpenChat'
- }),
-
- _onOpenChat: function (ev) {
- const env = Component.env;
- const partner = env.models['mail.partner'].insert({
- id: ev.data.partner_id,
- });
- partner.openChat();
- },
- });
-
var EmployeeKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
- Controller: EmployeeKanbanController,
+ Controller: KanbanController,
Renderer: EmployeeKanbanRenderer
}),
});
diff --git a/addons/hr/static/src/js/many2one_avatar_employee.js b/addons/hr/static/src/js/many2one_avatar_employee.js
index 5be29801ea9..0d5a245aaa2 100644
--- a/addons/hr/static/src/js/many2one_avatar_employee.js
+++ b/addons/hr/static/src/js/many2one_avatar_employee.js
@@ -8,48 +8,26 @@ odoo.define('hr.Many2OneAvatarEmployee', function (require) {
// Usage:
// <field name="employee_id" widget="many2one_avatar_employee"/>
- const { _t } = require('web.core');
const fieldRegistry = require('web.field_registry');
const { Many2OneAvatarUser, KanbanMany2OneAvatarUser } = require('mail.Many2OneAvatarUser');
- const session = require('web.session');
+ const { Component } = owl;
const Many2OneAvatarEmployeeMixin = {
- supportedModel: 'hr.employee',
-
- /**
- * Set the field to read on 'hr.employee' to get the partner id.
- *
- * @override
- */
- init() {
- this._super(...arguments);
- this.partnerField = 'user_partner_id';
- },
+ supportedModels: ['hr.employee', 'hr.employee.public'],
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
- * Display a warning if the user clicked on himself, or on an employee
- * not associated with any user.
- *
* @override
- * @param {number} [partnerId] the id of the clicked partner
*/
- _displayWarning(partnerId) {
- if (partnerId !== session.partner_id) {
- // this is not ourself, so if we get here it means that the
- // employee is not associated with any user
- this.displayNotification({
- message: _t('You can only chat with employees that have a dedicated user'),
- type: 'info',
- });
- } else {
- this._super(...arguments);
- }
- },
+ async _onAvatarClicked(ev) {
+ ev.stopPropagation(); // in list view, prevent from opening the record
+ const env = Component.env;
+ await env.messaging.openChat({ employeeId: this.value.res_id });
+ }
};
const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(Many2OneAvatarEmployeeMixin);
diff --git a/addons/hr/static/src/models/employee/employee.js b/addons/hr/static/src/models/employee/employee.js
new file mode 100644
index 00000000000..0cf877baab6
--- /dev/null
+++ b/addons/hr/static/src/models/employee/employee.js
@@ -0,0 +1,204 @@
+odoo.define('hr/static/src/models/employee/employee.js', function (require) {
+'use strict';
+
+const { registerNewModel } = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+function factory(dependencies) {
+
+ class Employee extends dependencies['mail.model'] {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @static
+ * @param {Object} data
+ * @returns {Object}
+ */
+ static convertData(data) {
+ const data2 = {};
+ if ('id' in data) {
+ data2.id = data.id;
+ }
+ if ('user_id' in data) {
+ data2.hasCheckedUser = true;
+ if (!data.user_id) {
+ data2.user = [['unlink']];
+ } else {
+ const partnerNameGet = data['user_partner_id'];
+ const partnerData = {
+ display_name: partnerNameGet[1],
+ id: partnerNameGet[0],
+ };
+ const userNameGet = data['user_id'];
+ const userData = {
+ id: userNameGet[0],
+ partner: [['insert', partnerData]],
+ partnerDisplayName: userNameGet[1],
+ };
+ data2.user = [['insert', userData]];
+ }
+ }
+ return data2;
+ }
+
+ /**
+ * Performs the `read` RPC on the `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {string[]} param0.fields
+ * @param {integer[]} param0.ids
+ */
+ static async performRpcRead({ context, fields, ids }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'read',
+ args: [ids],
+ kwargs: {
+ context,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Performs the `search_read` RPC on `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {Array[]} param0.domain
+ * @param {string[]} param0.fields
+ */
+ static async performRpcSearchRead({ context, domain, fields }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'search_read',
+ kwargs: {
+ context,
+ domain,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Checks whether this employee has a related user and partner and links
+ * them if applicable.
+ */
+ async checkIsUser() {
+ return this.env.models['hr.employee'].performRpcRead({
+ ids: [this.id],
+ fields: ['user_id', 'user_partner_id'],
+ context: { active_test: false },
+ });
+ }
+
+ /**
+ * Gets the chat between the user of this employee and the current user.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @returns {mail.thread|undefined}
+ */
+ async getChat() {
+ if (!this.user && !this.hasCheckedUser) {
+ await this.async(() => this.checkIsUser());
+ }
+ // prevent chatting with non-users
+ if (!this.user) {
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only chat with employees that have a dedicated user."),
+ type: 'info',
+ });
+ return;
+ }
+ return this.user.getChat();
+ }
+
+ /**
+ * Opens a chat between the user of this employee and the current user
+ * and returns it.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @param {Object} [options] forwarded to @see `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(options) {
+ const chat = await this.async(() => this.getChat());
+ if (!chat) {
+ return;
+ }
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the most appropriate view that is a profile for this employee.
+ */
+ async openProfile() {
+ return this.env.messaging.openDocument({
+ id: this.id,
+ model: 'hr.employee.public',
+ });
+ }
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static _createRecordLocalId(data) {
+ return `${this.modelName}_${data.id}`;
+ }
+
+ }
+
+ Employee.fields = {
+ /**
+ * Whether an attempt was already made to fetch the user corresponding
+ * to this employee. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedUser: attr({
+ default: false,
+ }),
+ /**
+ * Unique identifier for this employee.
+ */
+ id: attr(),
+ /**
+ * Partner related to this employee.
+ */
+ partner: one2one('mail.partner', {
+ inverse: 'employee',
+ related: 'user.partner',
+ }),
+ /**
+ * User related to this employee.
+ */
+ user: one2one('mail.user', {
+ inverse: 'employee',
+ }),
+ };
+
+ Employee.modelName = 'hr.employee';
+
+ return Employee;
+}
+
+registerNewModel('hr.employee', factory);
+
+});
diff --git a/addons/hr/static/src/models/messaging/messaging.js b/addons/hr/static/src/models/messaging/messaging.js
new file mode 100644
index 00000000000..435761a91ef
--- /dev/null
+++ b/addons/hr/static/src/models/messaging/messaging.js
@@ -0,0 +1,36 @@
+odoo.define('hr/static/src/models/messaging/messaging.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+
+registerInstancePatchModel('mail.messaging', 'hr/static/src/models/messaging/messaging.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @param {integer} [param0.employeeId]
+ */
+ async getChat({ employeeId }) {
+ if (employeeId) {
+ const employee = this.env.models['hr.employee'].insert({ id: employeeId });
+ return employee.getChat();
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async openProfile({ id, model }) {
+ if (model === 'hr.employee' || model === 'hr.employee.public') {
+ const employee = this.env.models['hr.employee'].insert({ id });
+ return employee.openProfile();
+ }
+ return this._super(...arguments);
+ },
+});
+
+});
diff --git a/addons/hr/static/src/models/partner/partner.js b/addons/hr/static/src/models/partner/partner.js
new file mode 100644
index 00000000000..cbbcb987dd2
--- /dev/null
+++ b/addons/hr/static/src/models/partner/partner.js
@@ -0,0 +1,63 @@
+odoo.define('hr/static/src/models/partner/partner.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+registerInstancePatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Checks whether this partner has a related employee and links them if
+ * applicable.
+ */
+ async checkIsEmployee() {
+ await this.async(() => this.env.models['hr.employee'].performRpcSearchRead({
+ context: { active_test: false },
+ domain: [['user_partner_id', '=', this.id]],
+ fields: ['user_id', 'user_partner_id'],
+ }));
+ this.update({ hasCheckedEmployee: true });
+ },
+ /**
+ * When a partner is an employee, its employee profile contains more useful
+ * information to know who he is than its partner profile.
+ *
+ * @override
+ */
+ async openProfile() {
+ // limitation of patch, `this._super` becomes unavailable after `await`
+ const _super = this._super.bind(this, ...arguments);
+ if (!this.employee && !this.hasCheckedEmployee) {
+ await this.async(() => this.checkIsEmployee());
+ }
+ if (this.employee) {
+ return this.employee.openProfile();
+ }
+ return _super();
+ },
+});
+
+registerFieldPatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ /**
+ * Employee related to this partner. It is computed through
+ * the inverse relation and should be considered read-only.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'partner',
+ }),
+ /**
+ * Whether an attempt was already made to fetch the employee corresponding
+ * to this partner. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedEmployee: attr({
+ default: false,
+ }),
+});
+
+});
diff --git a/addons/hr/static/src/models/user/user.js b/addons/hr/static/src/models/user/user.js
new file mode 100644
index 00000000000..a0482d50deb
--- /dev/null
+++ b/addons/hr/static/src/models/user/user.js
@@ -0,0 +1,18 @@
+odoo.define('hr/static/src/models/user/user.js', function (require) {
+'use strict';
+
+const {
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { one2one } = require('mail/static/src/model/model_field.js');
+
+registerFieldPatchModel('mail.user', 'hr/static/src/models/user/user.js', {
+ /**
+ * Employee related to this user.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'user',
+ }),
+});
+
+});
diff --git a/addons/hr/static/tests/helpers/mock_models.js b/addons/hr/static/tests/helpers/mock_models.js
new file mode 100644
index 00000000000..d4ae7c86035
--- /dev/null
+++ b/addons/hr/static/tests/helpers/mock_models.js
@@ -0,0 +1,34 @@
+odoo.define('hr/static/tests/helpers/mock_models.js', function (require) {
+'use strict';
+
+const MockModels = require('mail/static/tests/helpers/mock_models.js');
+
+MockModels.patch('hr/static/tests/helpers/mock_models.js', T =>
+ class extends T {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static generateData() {
+ const data = super.generateData(...arguments);
+ Object.assign(data, {
+ 'hr.employee.public': {
+ fields: {
+ display_name: { string: "Name", type: "char" },
+ user_id: { string: "User", type: "many2one", relation: 'res.users' },
+ user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
+ },
+ records: [],
+ },
+ });
+ return data;
+ }
+
+ }
+);
+
+});
diff --git a/addons/hr/static/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/many2one_avatar_employee_tests.js
index 06ea5a74f07..c1eef47b182 100644
--- a/addons/hr/static/tests/many2one_avatar_employee_tests.js
+++ b/addons/hr/static/tests/many2one_avatar_employee_tests.js
@@ -1,7 +1,12 @@
odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) {
"use strict";
-const { afterEach, beforeEach, start } = require('mail/static/src/utils/test_utils.js');
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
const FormView = require('web.FormView');
const KanbanView = require('web.KanbanView');
@@ -20,7 +25,7 @@ QUnit.module('hr', {}, function () {
Object.assign(this.data, {
'foo': {
fields: {
- employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee' },
+ employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' },
},
records: [
{ id: 1, employee_id: 11 },
@@ -29,26 +34,17 @@ QUnit.module('hr', {}, function () {
{ id: 4, employee_id: 23 },
],
},
- 'hr.employee': {
- fields: {
- display_name: { string: "Name", type: "char" },
- user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
- },
- records: [{
- id: 11,
- name: "Mario",
- user_partner_id: 11,
- }, {
- id: 7,
- name: "Luigi",
- user_partner_id: 12,
- }, {
- id: 23,
- name: "Yoshi",
- user_partner_id: 13,
- }],
- },
});
+ this.data['hr.employee.public'].records.push(
+ { id: 11, name: "Mario", user_id: 11, user_partner_id: 11 },
+ { id: 7, name: "Luigi", user_id: 12, user_partner_id: 12 },
+ { id: 23, name: "Yoshi", user_id: 13, user_partner_id: 13 }
+ );
+ this.data['res.users'].records.push(
+ { id: 11, partner_id: 11 },
+ { id: 12, partner_id: 12 },
+ { id: 13, partner_id: 13 }
+ );
this.data['res.partner'].records.push(
{ id: 11, display_name: "Mario" },
{ id: 12, display_name: "Luigi" },
@@ -61,9 +57,10 @@ QUnit.module('hr', {}, function () {
});
QUnit.test('many2one_avatar_employee widget in list view', async function (assert) {
- assert.expect(4);
+ assert.expect(11);
const { widget: list } = await start({
+ hasChatWindow: true,
hasView: true,
View: ListView,
model: 'foo',
@@ -79,18 +76,59 @@ QUnit.module('hr', {}, function () {
assert.strictEqual(list.$('.o_data_cell span').text(), 'MarioLuigiMarioYoshi');
- await dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'));
- await dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar'));
- await dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'));
-
-
- assert.verifySteps([
- 'read hr.employee 11',
- // 'call service openDMChatWindow 1',
- 'read hr.employee 7',
- // 'call service openDMChatWindow 2',
- // 'call service openDMChatWindow 1',
- ]);
+ // click on first employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ ['read hr.employee.public 11'],
+ "first employee should have been read to find its partner"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 'should have opened chat window'
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatWindowHeader_name').textContent,
+ "Mario",
+ 'chat window should be with clicked employee'
+ );
+
+ // click on second employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar')
+ ));
+ assert.verifySteps(
+ ['read hr.employee.public 7'],
+ "second employee should have been read to find its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ 'should have opened second chat window'
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_ChatWindowHeader_name')[1].textContent,
+ "Luigi",
+ 'chat window should be with clicked employee'
+ );
+
+ // click on third employee (same as first)
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ [],
+ "employee should not have been read again because we already know its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ "should still have only 2 chat windows because third is the same partner as first"
+ );
list.destroy();
});
@@ -117,18 +155,19 @@ QUnit.module('hr', {}, function () {
assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), '');
assert.containsN(kanban, '.o_m2o_avatar', 4);
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0)').data('src'), '/web/image/hr.employee/11/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee/7/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee/11/image_128');
- assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee/23/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee.public/7/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee.public/23/image_128');
kanban.destroy();
});
QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
- assert.expect(5);
+ assert.expect(6);
- this.data['hr.employee'].records[0].user_partner_id = false;
+ this.data['hr.employee.public'].records[0].user_id = false;
+ this.data['hr.employee.public'].records[0].user_partner_id = false;
const { widget: form } = await start({
hasView: true,
View: FormView,
@@ -156,50 +195,19 @@ QUnit.module('hr', {}, function () {
assert.verifySteps([
'read foo 1',
- 'read hr.employee 11',
- 'display notification "You can only chat with employees that have a dedicated user"',
+ 'read hr.employee.public 11',
]);
- form.destroy();
- });
-
- QUnit.test('many2one_avatar_employee: click on self', async function (assert) {
- assert.expect(5);
-
- this.data.currentPartnerId = 11;
- const { widget: form } = await start({
- hasView: true,
- View: FormView,
- model: 'foo',
- data: this.data,
- arch: '<form><field name="employee_id" widget="many2one_avatar_employee"/></form>',
- mockRPC(route, args) {
- if (args.method === 'read') {
- assert.step(`read ${args.model} ${args.args[0]}`);
- }
- return this._super(...arguments);
- },
- session: {
- partner_id: this.data.currentPartnerId,
- },
- res_id: 1,
- });
-
- mock.intercept(form, 'call_service', (ev) => {
- if (ev.data.service === 'notification') {
- assert.step(`display notification "${ev.data.args[0].message}"`);
- }
- }, true);
-
- assert.strictEqual(form.$('.o_field_widget[name=employee_id]').text().trim(), 'Mario');
-
- await dom.click(form.$('.o_m2o_avatar'));
-
- assert.verifySteps([
- 'read foo 1',
- 'read hr.employee 11',
- 'display notification "You cannot chat with yourself"',
- ]);
+ assert.containsOnce(
+ document.body,
+ '.toast .o_notification_content',
+ "should display a toast notification after failing to open chat"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "You can only chat with employees that have a dedicated user.",
+ "should display the correct information in the notification"
+ );
form.destroy();
});
diff --git a/addons/hr/views/hr_templates.xml b/addons/hr/views/hr_templates.xml
index d959b96506b..5d4bcd9da3e 100644
--- a/addons/hr/views/hr_templates.xml
+++ b/addons/hr/views/hr_templates.xml
@@ -4,15 +4,24 @@
<template id="assets_backend" name="hr assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/hr/static/src/scss/hr.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/hr/static/src/bugfix/bugfix.scss"/>
+
+ <script type="text/javascript" src="/hr/static/src/bugfix/bugfix.js"/>
<script type="text/javascript" src="/hr/static/src/js/chat.js"></script>
<script type="text/javascript" src="/hr/static/src/js/language.js"></script>
<script type="text/javascript" src="/hr/static/src/js/many2one_avatar_employee.js"></script>
<script type="text/javascript" src="/hr/static/src/js/standalone_m2o_avatar_employee.js"></script>
+ <script type="text/javascript" src="/hr/static/src/models/employee/employee.js"/>
+ <script type="text/javascript" src="/hr/static/src/models/messaging/messaging.js"/>
+ <script type="text/javascript" src="/hr/static/src/models/partner/partner.js"/>
+ <script type="text/javascript" src="/hr/static/src/models/user/user.js"/>
</xpath>
</template>
<template id="qunit_suite" name="hr tests" inherit_id="web.qunit_suite_tests">
<xpath expr="." position="inside">
+ <script type="text/javascript" src="/hr/static/src/bugfix/bugfix_tests.js"/>
+ <script type="text/javascript" src="/hr/static/tests/helpers/mock_models.js"/>
<script type="text/javascript" src="/hr/static/tests/many2one_avatar_employee_tests.js"></script>
<script type="text/javascript" src="/hr/static/tests/standalone_m2o_avatar_employee_tests.js"></script>
</xpath>
diff --git a/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
index dd9d08b606c..25e71660ebe 100644
--- a/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
+++ b/addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -11,12 +11,9 @@ registerInstancePatchModel('mail.messaging_notification_handler', 'im_livechat/s
/**
* @override
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {integer} param0.partner_id
*/
- _handleNotificationChannelTypingStatus(data) {
- const { channelId, partner_id } = data;
+ _handleNotificationChannelTypingStatus(channelId, data) {
+ const { partner_id } = data;
const channel = this.env.models['mail.thread'].findFromIdentifyingData({
id: channelId,
model: 'mail.channel',
@@ -32,7 +29,7 @@ registerInstancePatchModel('mail.messaging_notification_handler', 'im_livechat/s
} else {
partnerId = partner_id;
}
- this._super(Object.assign(data, {
+ this._super(channelId, Object.assign(data, {
partner_id: partnerId,
}));
},
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js
index 4652da4975b..7f9b53d2670 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.js
+++ b/addons/mail/static/src/components/chat_window/chat_window.js
@@ -167,20 +167,15 @@ class ChatWindow extends Component {
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
- _onAutocompleteSelect(ev, ui) {
- const partnerId = ui.item.id;
- const partner = this.env.models['mail.partner'].find(partner => partner.id === partnerId);
- const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat');
- if (chat) {
- chat.open({ chatWindowMode: 'from_new_message' });
- } else {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- autoselectChatWindowMode: 'from_new_message',
- partnerId,
- type: 'chat',
- });
+ async _onAutocompleteSelect(ev, ui) {
+ const chat = await this.env.messaging.getChat({ partnerId: ui.item.id });
+ if (!chat) {
+ return;
}
+ this.env.messaging.chatWindowManager.openThread(chat, {
+ makeActive: true,
+ replaceNewMessage: true,
+ });
}
/**
@@ -225,6 +220,12 @@ class ChatWindow extends Component {
if (this.chatWindow.isFocused) {
return;
}
+ if (isEventHandled(ev, 'Message.authorOpenChat')) {
+ return;
+ }
+ if (isEventHandled(ev, 'Message.authorOpenProfile')) {
+ return;
+ }
this.chatWindow.focus();
}
diff --git a/addons/mail/static/src/components/chat_window/chat_window.xml b/addons/mail/static/src/components/chat_window/chat_window.xml
index d711f267ac7..f92a32cf2fe 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.xml
+++ b/addons/mail/static/src/components/chat_window/chat_window.xml
@@ -29,7 +29,6 @@
hasComposerCurrentPartnerAvatar="false"
hasComposerSendButton="env.messaging.device.isMobile"
hasSquashCloseMessages="chatWindow.thread.model !== 'mail.box'"
- haveMessagesAuthorRedirect="chatWindow.thread.channel_type !== 'chat'"
threadViewLocalId="chatWindow.threadView.localId"
t-on-focusin="_onFocusinThread"
t-ref="thread"
diff --git a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
index b598eeee4b4..c6ec0b74e9f 100644
--- a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
+++ b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js
@@ -123,9 +123,7 @@ class ChatWindowHiddenMenu extends Component {
*/
_onClickedChatWindow(ev) {
const chatWindow = ev.detail.chatWindow;
- chatWindow.makeVisible();
- chatWindow.unfold();
- chatWindow.focus();
+ chatWindow.makeActive();
this.env.messaging.chatWindowManager.closeHiddenMenu();
}
diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
index 3306dbed905..efba64cecde 100644
--- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
+++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
@@ -255,6 +255,128 @@ QUnit.test('chat window new message: fold', async function (assert) {
);
});
+QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) {
+ /**
+ * InnerWith computation uses following info:
+ * ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method)
+ * (others: @see mail/static/src/models/chat_window_manager/chat_window_manager.js:visual)
+ *
+ * - chat window width: 325px
+ * - start/end/between gap width: 10px/10px/5px
+ * - hidden menu width: 200px
+ * - global width: 1920px
+ *
+ * Enough space for 3 visible chat windows:
+ * 10 + 325 + 5 + 325 + 5 + 325 + 10 = 1000 < 1920
+ */
+ assert.expect(11);
+
+ this.data['res.partner'].records.push({ id: 131, name: "Partner 131" });
+ this.data['res.users'].records.push({ partner_id: 131 });
+ this.data['mail.channel'].records.push(
+ { is_minimized: true },
+ { is_minimized: true },
+ );
+ const imSearchDef = makeDeferred();
+ await this.start({
+ env: {
+ browser: {
+ innerWidth: 1920,
+ },
+ },
+ async mockRPC(route, args) {
+ const res = await this._super(...arguments);
+ if (args.method === 'im_search') {
+ imSearchDef.resolve();
+ }
+ return res;
+ }
+ });
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 2,
+ "should have 2 chat windows initially"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should not have any 'new message' chat window initially"
+ );
+
+ // open "new message" chat window
+ await afterNextRender(() =>
+ document.querySelector(`.o_MessagingMenu_toggler`).click()
+ );
+ await afterNextRender(() =>
+ document.querySelector(`.o_MessagingMenu_newMessageButton`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should have 'new message' chat window after clicking 'new message' in messaging menu"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindow',
+ 3,
+ "should have 3 chat window after opening 'new message' chat window",
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindow_newMessageFormInput',
+ "'new message' chat window should have new message form input"
+ );
+ assert.hasClass(
+ document.querySelector('.o_ChatWindow[data-visible-index="2"]'),
+ 'o-new-message',
+ "'new message' chat window should be the last chat window initially",
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_ChatWindow[data-visible-index="2"] .o_ChatWindowHeader_commandShiftRight').click()
+ );
+ assert.hasClass(
+ document.querySelector('.o_ChatWindow[data-visible-index="1"]'),
+ 'o-new-message',
+ "'new message' chat window should have moved to the middle after clicking shift previous",
+ );
+
+ // search for a user in "new message" autocomplete
+ document.execCommand('insertText', false, "131");
+ document.querySelector(`.o_ChatWindow_newMessageFormInput`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ChatWindow_newMessageFormInput`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ // Wait for search RPC to be resolved. The following await lines are
+ // necessary because autocomplete is an external lib therefore it is not
+ // possible to use `afterNextRender`.
+ await imSearchDef;
+ await nextAnimationFrame();
+ const link = document.querySelector('.ui-autocomplete .ui-menu-item a');
+ assert.ok(
+ link,
+ "should have autocomplete suggestion after typing on 'new message' input"
+ );
+ assert.strictEqual(
+ link.textContent,
+ "Partner 131",
+ "autocomplete suggestion should target the partner matching search term"
+ );
+
+ await afterNextRender(() => link.click());
+ assert.containsNone(
+ document.body,
+ '.o_ChatWindow.o-new-message',
+ "should have removed the 'new message' chat window after selecting a partner"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeader_name').textContent,
+ "Partner 131",
+ "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle"
+ );
+});
+
QUnit.test('chat window: basic rendering', async function (assert) {
assert.expect(11);
@@ -1351,7 +1473,7 @@ QUnit.test('chat window: switch on TAB', async function (assert) {
);
});
-QUnit.test('chat window: TAB cycle with 3 open chat windows', async function (assert) {
+QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) {
/**
* InnerWith computation uses following info:
* ([mocked] global window width: @see `mail/static/src/utils/test_utils.js:start()` method)
diff --git a/addons/mail/static/src/components/chatter/chatter.js b/addons/mail/static/src/components/chatter/chatter.js
index 61fbb78d619..cd1f8f8720b 100644
--- a/addons/mail/static/src/components/chatter/chatter.js
+++ b/addons/mail/static/src/components/chatter/chatter.js
@@ -37,19 +37,18 @@ class Chatter extends Component {
attachments: 1,
},
});
- this._threadRef = useRef('thread');
+ /**
+ * Reference of the composer. Useful to focus it.
+ */
+ this._composerRef = useRef('composer');
}
mounted() {
- if (this.chatter.thread) {
- this._notifyRendered();
- }
+ this._update();
}
patched() {
- if (this.chatter.thread) {
- this._notifyRendered();
- }
+ this._update();
}
//--------------------------------------------------------------------------
@@ -77,6 +76,22 @@ class Chatter extends Component {
});
}
+ /**
+ * @private
+ */
+ _update() {
+ if (this.chatter.thread) {
+ this._notifyRendered();
+ }
+ if (this.chatter.isDoFocus) {
+ this.chatter.update({ isDoFocus: false });
+ const composer = this._composerRef.comp;
+ if (composer) {
+ composer.focus();
+ }
+ }
+ }
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
diff --git a/addons/mail/static/src/components/chatter/chatter.xml b/addons/mail/static/src/components/chatter/chatter.xml
index b83c9ad733f..a48396f813c 100644
--- a/addons/mail/static/src/components/chatter/chatter.xml
+++ b/addons/mail/static/src/components/chatter/chatter.xml
@@ -39,10 +39,8 @@
class="o_Chatter_thread"
hasComposer="false"
hasScrollAdjust="chatter.hasMessageListScrollAdjust"
- haveMessagesAuthorRedirect="true"
order="'desc'"
threadViewLocalId="chatter.threadView.localId"
- t-ref="thread"
/>
</t>
</t>
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
index afb7d811eae..0b23ee4ec22 100644
--- a/addons/mail/static/src/components/composer/composer.js
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -121,7 +121,7 @@ class Composer extends Component {
if (this.env.messaging.device.isMobile) {
this.el.scrollIntoView();
}
- this.composer.focus();
+ this._textInputRef.comp.focus();
}
/**
@@ -288,7 +288,7 @@ class Composer extends Component {
ev.stopPropagation();
this._textInputRef.comp.saveStateInStore();
this.composer.insertIntoTextInput(ev.detail.unicode);
- this.composer.focus();
+ this.focus();
}
/**
@@ -323,7 +323,7 @@ class Composer extends Component {
if (ev.key === 'Escape') {
if (this._emojisPopoverRef.comp) {
this._emojisPopoverRef.comp.close();
- this.composer.focus();
+ this.focus();
markEventHandled(ev, 'Composer.closeEmojisPopover');
}
}
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
index 53616e112e9..6a0943f9ca8 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.js
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
@@ -82,6 +82,10 @@ class ComposerTextInput extends Component {
return this.env._t("Write something...");
}
+ focus() {
+ this._textareaRef.el.focus();
+ }
+
focusout() {
this.saveStateInStore();
this._textareaRef.el.blur();
@@ -155,10 +159,6 @@ class ComposerTextInput extends Component {
this.composer.textInputCursorEnd
);
this._updateHeight();
- if (this.composer.isDoFocus) {
- this._textareaRef.el.focus();
- this.composer.update({ isDoFocus: false });
- }
}
/**
@@ -273,7 +273,7 @@ class ComposerTextInput extends Component {
if (this.composer.activeSuggestedPartner) {
this.composer.insertMentionedPartner(this.composer.activeSuggestedPartner);
this.composer.closeMentionSuggestions();
- this.composer.focus();
+ this.focus();
}
}
break;
@@ -322,7 +322,7 @@ class ComposerTextInput extends Component {
_onPartnerMentionSuggestionClicked(ev) {
this.composer.insertMentionedPartner(ev.detail.partner);
this.composer.closeMentionSuggestions();
- this.composer.focus();
+ this.focus();
}
/**
diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js
index 83aee1e201e..4c8c4a58ab7 100644
--- a/addons/mail/static/src/components/discuss/discuss.js
+++ b/addons/mail/static/src/components/discuss/discuss.js
@@ -47,12 +47,13 @@ class Discuss extends Component {
});
this._updateLocalStoreProps();
/**
- * Reference of the thread. Useful to update scroll position correctly
- * on patch. AKU TODO: this made sense when composer was outside of
- * thread, but this may no longer be necessary??
+ * Reference of the composer. Useful to focus it.
*/
- this._threadRef = useRef('thread');
-
+ this._composerRef = useRef('composer');
+ /**
+ * Reference of the ThreadView. Useful to focus it.
+ */
+ this._threadViewRef = useRef('threadView');
// bind since passed as props
this._onMobileAddItemHeaderInputSelect = this._onMobileAddItemHeaderInputSelect.bind(this);
this._onMobileAddItemHeaderInputSource = this._onMobileAddItemHeaderInputSource.bind(this);
@@ -66,6 +67,7 @@ class Discuss extends Component {
this.discuss.openInitThread();
}
this._updateLocalStoreProps();
+ this._update();
}
patched() {
@@ -83,6 +85,7 @@ class Discuss extends Component {
}
this._activeThreadCache = this.discuss.threadView.threadCache;
this._updateLocalStoreProps();
+ this._update();
}
willUnmount() {
@@ -142,6 +145,24 @@ class Discuss extends Component {
/**
* @private
*/
+ _update() {
+ if (this.discuss.isDoFocus) {
+ this.discuss.update({ isDoFocus: false });
+ const composer = this._composerRef.comp;
+ if (composer) {
+ composer.focus();
+ } else {
+ const threadView = this._threadViewRef.comp;
+ if (threadView) {
+ threadView.focus();
+ }
+ }
+ }
+ }
+
+ /**
+ * @private
+ */
_updateLocalStoreProps() {
/**
* Locally tracked store props `activeThreadCache`.
@@ -251,16 +272,6 @@ class Discuss extends Component {
/**
* @private
* @param {CustomEvent} ev
- * @param {Object} ev.detail
- * @param {mail.thread} ev.detail.thread
- */
- _onSelectThread(ev) {
- this.discuss.threadView.update({ thread: [['link', ev.detail.thread]] });
- }
-
- /**
- * @private
- * @param {CustomEvent} ev
*/
_onThreadRendered(ev) {
this.trigger('o-update-control-panel');
diff --git a/addons/mail/static/src/components/discuss/discuss.xml b/addons/mail/static/src/components/discuss/discuss.xml
index 94234d0fc9b..d88b4796d9c 100644
--- a/addons/mail/static/src/components/discuss/discuss.xml
+++ b/addons/mail/static/src/components/discuss/discuss.xml
@@ -58,7 +58,6 @@
hasComposerThreadTyping="true"
hasMessageCheckbox="true"
hasSquashCloseMessages="discuss.thread.model !== 'mail.box'"
- haveMessagesAuthorRedirect="discuss.thread.channel_type !== 'chat'"
haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox"
haveMessagesReplyIcon="discuss.thread === env.messaging.inbox"
selectedMessageLocalId="
@@ -66,6 +65,7 @@
discuss.replyingToMessage.localId"
threadViewLocalId="discuss.threadView.localId"
t-on-o-rendered="_onThreadRendered"
+ t-ref="threadView"
/>
</t>
<t t-if="!discuss.thread and (!env.messaging.device.isMobile or discuss.activeMobileNavbarTabId === 'mailbox')">
@@ -77,7 +77,6 @@
<NotificationList
class="o_Discuss_notificationList"
filter="discuss.activeMobileNavbarTabId"
- t-on-o-select-thread="_onSelectThread"
/>
</t>
<t t-if="env.messaging.device.isMobile and !discuss.isReplyingToMessage">
@@ -96,6 +95,7 @@
hasDiscardButton="true"
hasThreadName="true"
t-on-o-message-posted="_onReplyingToMessageMessagePosted"
+ t-ref="composer"
/>
</t>
</t>
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
index 8c437ce5b18..3f9e93d1de8 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -2116,7 +2116,7 @@ QUnit.test('message origin redirect to channel', async function (assert) {
});
QUnit.test('redirect to author (open chat)', async function (assert) {
- assert.expect(9);
+ assert.expect(7);
// expected correspondent, with a random unique id that will be used to link
// partner to chat and a random name that will be asserted in the test
@@ -2143,13 +2143,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
id: 100,
model: 'mail.channel',
res_id: 1,
- },
- {
- author_id: this.data.currentPartnerId,
- channel_ids: [1],
- id: 101,
- model: 'mail.channel',
- res_id: 1,
}
);
await this.start({
@@ -2187,8 +2180,8 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
);
assert.strictEqual(
document.querySelectorAll(`.o_Discuss_thread .o_Message`).length,
- 2,
- "should have 2 messages"
+ 1,
+ "should have 1 message"
);
const msg1 = document.querySelector(`
.o_Discuss_thread
@@ -2196,12 +2189,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
this.env.models['mail.message'].find(message => message.id === 100).localId
}"]
`);
- const msg2 = document.querySelector(`
- .o_Discuss_thread
- .o_Message[data-message-local-id="${
- this.env.models['mail.message'].find(message => message.id === 101).localId
- }"]
- `);
assert.strictEqual(
msg1.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
1,
@@ -2211,15 +2198,6 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
msg1.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
"message1 should have redirect to author"
);
- assert.strictEqual(
- msg2.querySelectorAll(`:scope .o_Message_authorAvatar`).length,
- 1,
- "message2 should have author image"
- );
- assert.notOk(
- msg2.querySelector(`:scope .o_Message_authorAvatar`).classList.contains('o_redirect'),
- "message2 should not have redirect to author (self-author)"
- );
await afterNextRender(() =>
msg1.querySelector(`:scope .o_Message_authorAvatar`).click()
diff --git a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
index cea920a45f9..c2185a60dc0 100644
--- a/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
+++ b/addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js
@@ -74,7 +74,7 @@ class DiscussMobileMailboxSelection extends Component {
*/
_onClick(ev) {
const { mailbox } = ev.currentTarget.dataset;
- this.discuss.threadView.update({ thread: [['link', mailbox]] });
+ mailbox.open();
}
}
diff --git a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
index daaa8b44b5d..2b84762c9dd 100644
--- a/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
+++ b/addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
@@ -117,7 +117,7 @@ class DiscussSidebarItem extends Component {
* @param {MouseEvent} ev
*/
_onClick(ev) {
- this.discuss.threadView.update({ thread: [['link', this.thread]] });
+ this.thread.open();
}
/**
diff --git a/addons/mail/static/src/components/follower/follower.js b/addons/mail/static/src/components/follower/follower.js
index f446c098914..dc3e395f03c 100644
--- a/addons/mail/static/src/components/follower/follower.js
+++ b/addons/mail/static/src/components/follower/follower.js
@@ -43,10 +43,7 @@ class Follower extends Component {
_onClickDetails(ev) {
ev.preventDefault();
ev.stopPropagation();
- this.env.messaging.openDocument({
- id: this.follower.resId,
- model: this.follower.resModel,
- });
+ this.follower.openProfile();
}
/**
diff --git a/addons/mail/static/src/components/follower/follower_tests.js b/addons/mail/static/src/components/follower/follower_tests.js
index d93cb24b6c2..a742e899e6b 100644
--- a/addons/mail/static/src/components/follower/follower_tests.js
+++ b/addons/mail/static/src/components/follower/follower_tests.js
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower/follower_tests.js', function (r
const components = {
Follower: require('mail/static/src/components/follower/follower.js'),
};
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
const {
afterEach,
afterNextRender,
@@ -141,8 +142,8 @@ QUnit.test('click on channel follower details', async function (assert) {
assert.step('do_action');
assert.strictEqual(
payload.action.res_id,
- 1,
- "The redirect action should redirect to the right res id (1)"
+ 10,
+ "The redirect action should redirect to the right res id (10)"
);
assert.strictEqual(
payload.action.res_model,
@@ -155,7 +156,8 @@ QUnit.test('click on channel follower details', async function (assert) {
"The redirect action should be of type 'ir.actions.act_window'"
);
});
-
+ this.data['res.partner'].records.push({ id: 100 });
+ this.data['mail.channel'].records.push({ id: 10 });
await this.start({
env: { bus },
});
@@ -164,7 +166,7 @@ QUnit.test('click on channel follower details', async function (assert) {
model: 'res.partner',
});
const follower = await this.env.models['mail.follower'].create({
- channel: [['insert', { id: 1, model: 'mail.channel', name: "channel" }]],
+ channel: [['insert', { id: 10, model: 'mail.channel', name: "channel" }]],
followedThread: [['link', thread]],
id: 2,
isActive: true,
@@ -192,6 +194,7 @@ QUnit.test('click on channel follower details', async function (assert) {
QUnit.test('click on partner follower details', async function (assert) {
assert.expect(7);
+ const openFormDef = makeDeferred();
const bus = new Bus();
bus.on('do-action', null, payload => {
assert.step('do_action');
@@ -210,8 +213,9 @@ QUnit.test('click on partner follower details', async function (assert) {
"ir.actions.act_window",
"The redirect action should be of type 'ir.actions.act_window'"
);
+ openFormDef.resolve();
});
-
+ this.data['res.partner'].records.push({ id: 100 });
await this.start({
env: { bus },
});
@@ -243,6 +247,7 @@ QUnit.test('click on partner follower details', async function (assert) {
);
document.querySelector('.o_Follower_details').click();
+ await openFormDef;
assert.verifySteps(
['do_action'],
"clicking on follower should redirect to partner form view"
@@ -302,6 +307,7 @@ QUnit.test('click on edit follower', async function (assert) {
QUnit.test('edit follower and close subtype dialog', async function (assert) {
assert.expect(6);
+ this.data['res.partner'].records.push({ id: 100 });
await this.start({
hasDialog: true,
async mockRPC(route, args) {
diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js
index 157f3dc2acd..7d314647bb2 100644
--- a/addons/mail/static/src/components/message/message.js
+++ b/addons/mail/static/src/components/message/message.js
@@ -11,6 +11,7 @@ const components = {
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const { markEventHandled } = require('mail/static/src/utils/utils.js');
const { timeFromNow } = require('mail.utils');
const { _lt } = require('web.core');
@@ -156,19 +157,18 @@ class Message extends Component {
}
/**
- * Determine whether author redirect feature is enabled on message.
- * Click on message author should redirect to author.
+ * Determines whether author open chat feature is enabled on message.
*
* @returns {boolean}
*/
- get hasAuthorRedirect() {
- if (!this.props.hasAuthorRedirect) {
- return false;
- }
+ get hasAuthorOpenChat() {
if (!this.message.author) {
return false;
}
- if (this.message.author === this.env.messaging.currentPartner) {
+ if (
+ this.message.originThread &&
+ this.message.originThread.correspondent === this.message.author
+ ) {
return false;
}
return true;
@@ -237,6 +237,13 @@ class Message extends Component {
}
/**
+ * @returns {string}
+ */
+ get OPEN_CHAT() {
+ return this.env._t("Open chat");
+ }
+
+ /**
* Make this message viewable in its enclosing scroll environment (usually
* message list).
*
@@ -245,7 +252,7 @@ class Message extends Component {
* @param {string} [param0.block='end']
* @returns {Promise}
*/
- async scrollIntoView({ behavior='auto', block='end' }={}) {
+ async scrollIntoView({ behavior = 'auto', block = 'end' } = {}) {
this.el.scrollIntoView({
behavior,
block,
@@ -419,23 +426,33 @@ class Message extends Component {
* @param {MouseEvent} ev
*/
_onClick(ev) {
+ if (ev.target.closest('.o_channel_redirect')) {
+ this.env.messaging.openProfile({
+ id: Number(ev.target.dataset.oeId),
+ model: 'mail.channel',
+ });
+ // avoid following dummy href
+ ev.preventDefault();
+ return;
+ }
if (ev.target.closest('.o_mention')) {
- this.env.messaging.redirect({
+ this.env.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
+ // avoid following dummy href
ev.preventDefault();
return;
}
if (ev.target.closest('.o_mail_redirect')) {
- this.env.messaging.redirect({
+ this.env.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
+ // avoid following dummy href
ev.preventDefault();
return;
}
- ev.stopPropagation();
this.state.isClicked = !this.state.isClicked;
}
@@ -443,17 +460,24 @@ class Message extends Component {
* @private
* @param {MouseEvent} ev
*/
- _onClickAuthor(ev) {
- if (!this.hasAuthorRedirect) {
+ _onClickAuthorAvatar(ev) {
+ if (!this.hasAuthorOpenChat) {
return;
}
+ markEventHandled(ev, 'Message.authorOpenChat');
+ this.message.author.openChat();
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickAuthorName(ev) {
if (!this.message.author) {
return;
}
- this.env.messaging.redirect({
- id: this.message.author.id,
- model: this.message.author.model,
- });
+ markEventHandled(ev, 'Message.authorOpenProfile');
+ this.message.author.openProfile();
}
/**
@@ -514,11 +538,9 @@ class Message extends Component {
* @param {MouseEvent} ev
*/
_onClickOriginThread(ev) {
+ // avoid following dummy href
ev.preventDefault();
- this.env.messaging.redirect({
- id: this.message.originThread.id,
- model: this.message.originThread.model,
- });
+ this.message.originThread.open();
}
/**
@@ -581,7 +603,6 @@ class Message extends Component {
Object.assign(Message, {
components,
defaultProps: {
- hasAuthorRedirect: false,
hasCheckbox: false,
hasMarkAsReadIcon: false,
hasReplyIcon: false,
@@ -594,7 +615,6 @@ Object.assign(Message, {
optional: true,
validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop),
},
- hasAuthorRedirect: Boolean,
hasCheckbox: Boolean,
hasMarkAsReadIcon: Boolean,
hasReplyIcon: Boolean,
diff --git a/addons/mail/static/src/components/message/message.xml b/addons/mail/static/src/components/message/message.xml
index 7739c79337a..b0a41947c88 100644
--- a/addons/mail/static/src/components/message/message.xml
+++ b/addons/mail/static/src/components/message/message.xml
@@ -18,7 +18,7 @@
<div class="o_Message_sidebar" t-att-class="{ 'o-message-squashed': props.isSquashed }">
<t t-if="!props.isSquashed">
<div class="o_Message_authorAvatarContainer o_Message_sidebarItem">
- <img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: hasAuthorRedirect, o_redirect: hasAuthorRedirect }" t-att-src="avatar" t-on-click="_onClickAuthor" t-att-title="hasAuthorRedirect ? 'Open chat' : ''" alt=""/>
+ <img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: hasAuthorOpenChat, o_redirect: hasAuthorOpenChat }" t-att-src="avatar" t-on-click="_onClickAuthorAvatar" t-att-title="hasAuthorOpenChat ? OPEN_CHAT : ''" alt="Avatar"/>
<t t-if="message.author and message.author.im_status">
<PartnerImStatusIcon
class="o_Message_partnerImStatusIcon"
@@ -58,7 +58,7 @@
<t t-if="!props.isSquashed">
<div class="o_Message_header">
<t t-if="message.author">
- <div class="o_Message_authorName" t-att-class="hasAuthorRedirect ? 'o_Message_authorRedirect o_redirect' : ''" t-on-click="_onClickAuthor" t-att-title="hasAuthorRedirect ? 'Open chat' : ''">
+ <div class="o_Message_authorName o_Message_authorRedirect o_redirect" t-on-click="_onClickAuthorName" title="Open profile">
<t t-esc="message.author.nameOrDisplayName"/>
</div>
</t>
diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js
index 0cdf4893305..966bff9a5ba 100644
--- a/addons/mail/static/src/components/message_list/message_list.js
+++ b/addons/mail/static/src/components/message_list/message_list.js
@@ -141,9 +141,6 @@ class MessageList extends Component {
case 'more-messages-loaded':
this._adjustFromMoreMessagesLoaded(hint);
break;
- default:
- this.threadView.markComponentHintProcessed(hint);
- break;
}
}
this._willPatchSnapshot = undefined;
@@ -552,7 +549,6 @@ Object.assign(MessageList, {
hasMessageCheckbox: false,
hasScrollAdjust: true,
hasSquashCloseMessages: false,
- haveMessagesAuthorRedirect: false,
haveMessagesMarkAsReadIcon: false,
haveMessagesReplyIcon: false,
order: 'asc',
@@ -560,7 +556,6 @@ Object.assign(MessageList, {
props: {
hasMessageCheckbox: Boolean,
hasSquashCloseMessages: Boolean,
- haveMessagesAuthorRedirect: Boolean,
haveMessagesMarkAsReadIcon: Boolean,
haveMessagesReplyIcon: Boolean,
hasScrollAdjust: Boolean,
diff --git a/addons/mail/static/src/components/message_list/message_list.xml b/addons/mail/static/src/components/message_list/message_list.xml
index a678f7a7199..0a87558e839 100644
--- a/addons/mail/static/src/components/message_list/message_list.xml
+++ b/addons/mail/static/src/components/message_list/message_list.xml
@@ -65,7 +65,6 @@
t-att-class="{
'o-has-message-selection': props.selectedMessageLocalId,
}"
- hasAuthorRedirect="props.haveMessagesAuthorRedirect"
hasMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
hasCheckbox="props.hasMessageCheckbox"
hasReplyIcon="props.haveMessagesReplyIcon"
diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.js b/addons/mail/static/src/components/messaging_menu/messaging_menu.js
index ac4472d8209..4e5ede8f635 100644
--- a/addons/mail/static/src/components/messaging_menu/messaging_menu.js
+++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.js
@@ -33,13 +33,6 @@ class MessagingMenu extends Component {
};
});
- /**
- * Reference of the new message input in mobile. Useful to include it
- * and autocomplete menu as "inside" the messaging menu, to prevent
- * closing the messaging menu otherwise.
- */
- this._mobileNewMessageInputRef = useRef('mobileNewMessageInput');
-
// bind since passed as props
this._onMobileNewMessageInputSelect = this._onMobileNewMessageInputSelect.bind(this);
this._onMobileNewMessageInputSource = this._onMobileNewMessageInputSource.bind(this);
@@ -121,20 +114,14 @@ class MessagingMenu extends Component {
// in mobile: keeps the messaging menu open in background
// TODO: maybe need to move this to a mobile component?
// task-2089887
- if (
- this.env.messaging.device.isMobile &&
- this.env.messaging.chatWindowManager.hasVisibleChatWindows
- ) {
+ if (this.env.messaging.device.isMobile) {
return;
}
// ignore click inside the menu
if (this.el.contains(ev.target)) {
return;
}
- const input = this._mobileNewMessageInputRef.comp;
- if (input && input.contains(ev.target)) {
- return;
- }
+ // in all other cases: close the messaging menu when clicking outside
this.messagingMenu.close();
}
@@ -194,21 +181,7 @@ class MessagingMenu extends Component {
* @param {integer} ui.item.id
*/
_onMobileNewMessageInputSelect(ev, ui) {
- const partnerId = ui.item.id;
- const partner = this.env.models['mail.partner'].find(partner => partner.id === partnerId);
- const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat');
- if (chat) {
- chat.open();
- } else {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- partnerId,
- type: 'chat',
- });
- }
- if (!this.env.messaging.device.isMobile) {
- this.messagingMenu.close();
- }
+ this.env.messaging.openChat({ partnerId: ui.item.id });
}
/**
@@ -246,20 +219,6 @@ class MessagingMenu extends Component {
this.messagingMenu.update({ activeTabId: ev.detail.tabId });
}
- /**
- * @private
- * @param {CustomEvent} ev
- * @param {Object} ev.detail
- * @param {mail.thread} ev.detail.thread
- */
- _onSelectThread(ev) {
- ev.stopPropagation();
- ev.detail.thread.open();
- if (!this.env.messaging.device.isMobile) {
- this.messagingMenu.close();
- }
- }
-
}
Object.assign(MessagingMenu, {
diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.xml b/addons/mail/static/src/components/messaging_menu/messaging_menu.xml
index b743c7ca92d..fc779231dd8 100644
--- a/addons/mail/static/src/components/messaging_menu/messaging_menu.xml
+++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.xml
@@ -46,7 +46,6 @@
select="_onMobileNewMessageInputSelect"
source="_onMobileNewMessageInputSource"
t-on-o-hide="_onHideMobileNewMessage"
- t-ref="mobileNewMessageInput"
/>
</t>
</div>
@@ -54,7 +53,6 @@
class="o_MessagingMenu_notificationList"
t-att-class="{ 'o-mobile': env.messaging.device.isMobile }"
filter="messagingMenu.activeTabId"
- t-on-o-select-thread="_onSelectThread"
/>
<t t-if="env.messaging.device.isMobile">
<MobileMessagingNavbar
diff --git a/addons/mail/static/src/components/notification_group/notification_group.js b/addons/mail/static/src/components/notification_group/notification_group.js
index 4020a610dc6..65cfcea3076 100644
--- a/addons/mail/static/src/components/notification_group/notification_group.js
+++ b/addons/mail/static/src/components/notification_group/notification_group.js
@@ -61,6 +61,9 @@ class NotificationGroup extends Component {
return;
}
this.group.openDocuments();
+ if (!this.env.messaging.device.isMobile) {
+ this.env.messaging.messagingMenu.close();
+ }
}
/**
@@ -69,6 +72,9 @@ class NotificationGroup extends Component {
*/
_onClickMarkAsRead(ev) {
this.group.openCancelAction();
+ if (!this.env.messaging.device.isMobile) {
+ this.env.messaging.messagingMenu.close();
+ }
}
}
diff --git a/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js
index fcab0251524..aa1d36593ad 100644
--- a/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js
+++ b/addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js
@@ -364,6 +364,7 @@ QUnit.test('different mail.channel are not grouped', async function (assert) {
// they are linked to different channels, even though the model is the same.
assert.expect(6);
+ this.data['mail.channel'].records.push({ id: 31 }, { id: 32 });
this.data['mail.message'].records.push(
// first message that is expected to have a failure
{
diff --git a/addons/mail/static/src/components/notification_request/notification_request.js b/addons/mail/static/src/components/notification_request/notification_request.js
index cb1b59ccbaa..69fb5beab2f 100644
--- a/addons/mail/static/src/components/notification_request/notification_request.js
+++ b/addons/mail/static/src/components/notification_request/notification_request.js
@@ -74,9 +74,7 @@ class NotificationRequest extends Component {
if (def) {
def.then(this._handleResponseNotificationPermission.bind(this));
}
-
- const device = this.env.messaging.device;
- if (!device.isMobile) {
+ if (!this.env.messaging.device.isMobile) {
this.env.messaging.messagingMenu.close();
}
}
diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
index efcd8f5d86e..24486cfc93c 100644
--- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
+++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js
@@ -105,7 +105,10 @@ class ThreadNeedactionPreview extends Component {
// handled in `_onClickMarkAsRead`
return;
}
- this.trigger('o-select-thread', { thread: this.thread });
+ this.thread.open();
+ if (!this.env.messaging.device.isMobile) {
+ this.env.messaging.messagingMenu.close();
+ }
}
/**
diff --git a/addons/mail/static/src/components/thread_preview/thread_preview.js b/addons/mail/static/src/components/thread_preview/thread_preview.js
index 614201e821d..6e30005225c 100644
--- a/addons/mail/static/src/components/thread_preview/thread_preview.js
+++ b/addons/mail/static/src/components/thread_preview/thread_preview.js
@@ -97,7 +97,10 @@ class ThreadPreview extends Component {
// handled in `_onClickMarkAsRead`
return;
}
- this.trigger('o-select-thread', { thread: this.thread });
+ this.thread.open();
+ if (!this.env.messaging.device.isMobile) {
+ this.env.messaging.messagingMenu.close();
+ }
}
/**
diff --git a/addons/mail/static/src/components/thread_view/thread_view.js b/addons/mail/static/src/components/thread_view/thread_view.js
index 7b1680f31a8..395fac68474 100644
--- a/addons/mail/static/src/components/thread_view/thread_view.js
+++ b/addons/mail/static/src/components/thread_view/thread_view.js
@@ -132,7 +132,6 @@ Object.assign(ThreadView, {
hasComposer: false,
hasMessageCheckbox: false,
hasSquashCloseMessages: false,
- haveMessagesAuthorRedirect: false,
haveMessagesMarkAsReadIcon: false,
haveMessagesReplyIcon: false,
order: 'asc',
@@ -168,7 +167,6 @@ Object.assign(ThreadView, {
optional: true,
},
hasSquashCloseMessages: Boolean,
- haveMessagesAuthorRedirect: Boolean,
haveMessagesMarkAsReadIcon: Boolean,
haveMessagesReplyIcon: Boolean,
order: {
diff --git a/addons/mail/static/src/components/thread_view/thread_view.xml b/addons/mail/static/src/components/thread_view/thread_view.xml
index 30844135e65..eead567fa1e 100644
--- a/addons/mail/static/src/components/thread_view/thread_view.xml
+++ b/addons/mail/static/src/components/thread_view/thread_view.xml
@@ -15,7 +15,6 @@
hasMessageCheckbox="props.hasMessageCheckbox"
hasScrollAdjust="props.hasScrollAdjust"
hasSquashCloseMessages="props.hasSquashCloseMessages"
- haveMessagesAuthorRedirect="props.haveMessagesAuthorRedirect"
haveMessagesMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
haveMessagesReplyIcon="props.haveMessagesReplyIcon"
order="props.order"
diff --git a/addons/mail/static/src/components/thread_view/thread_view_tests.js b/addons/mail/static/src/components/thread_view/thread_view_tests.js
index 06832473b99..c8f41632ae3 100644
--- a/addons/mail/static/src/components/thread_view/thread_view_tests.js
+++ b/addons/mail/static/src/components/thread_view/thread_view_tests.js
@@ -304,7 +304,7 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
"Channel should have been fetched but not seen yet"
);
- await afterNextRender(() => thread.composer.focus());
+ await afterNextRender(() => document.querySelector('.o_ComposerTextInput_textarea').focus());
assert.verifySteps(
['rpc:channel_seen'],
"Channel should have been marked as seen after threadView got the focus"
@@ -355,6 +355,8 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com
const threadView = this.env.models['mail.thread_view'].create({ thread: [['link', thread]] });
await this.createThreadViewComponent(threadView, { hasComposer: true });
+
+ document.querySelector('.o_ComposerTextInput_textarea').focus();
const notifications = [
[['myDB', 'mail.channel', 100], {
channelId: 100,
diff --git a/addons/mail/static/src/js/many2one_avatar_user.js b/addons/mail/static/src/js/many2one_avatar_user.js
index 3a4e04089f5..6a5b527004a 100644
--- a/addons/mail/static/src/js/many2one_avatar_user.js
+++ b/addons/mail/static/src/js/many2one_avatar_user.js
@@ -4,27 +4,18 @@ odoo.define('mail.Many2OneAvatarUser', function (require) {
// This module defines an extension of the Many2OneAvatar widget, which is
// integrated with the messaging system. The Many2OneAvatarUser is designed
// to display people, and when the avatar of those people is clicked, it
- // opens a DM chat window with the corresponding partner (the messaging
- // system is based on model 'res.partner').
+ // opens a DM chat window with the corresponding user.
//
- // This widget is supported on many2one fields pointing to 'res.users'. When
- // the user is clicked, we fetch the partner id of the given user (field
- // 'partner_id'), and we open a chat window with this partner.
+ // This widget is supported on many2one fields pointing to 'res.users'.
//
// Usage:
// <field name="user_id" widget="many2one_avatar_user"/>
//
// The widget is designed to be extended, to support many2one fields pointing
- // to other models than 'res.users'. Those models must have a many2one field
- // pointing to 'res.partner'. Ideally, the many2one should be false if the
- // corresponding partner isn't associated with a user, otherwise it will open
- // chat windows with partners that won't be able to read your messages and
- // reply.
+ // to other models than 'res.users'.
- const { _t } = require('web.core');
const fieldRegistry = require('web.field_registry');
const { Many2OneAvatar } = require('web.relational_fields');
- const session = require('web.session');
const { Component } = owl;
@@ -32,71 +23,17 @@ odoo.define('mail.Many2OneAvatarUser', function (require) {
events: Object.assign({}, Many2OneAvatar.prototype.events, {
'click .o_m2o_avatar': '_onAvatarClicked',
}),
- // Maps record ids to promises that resolve with the corresponding partner ids.
- // Used as a cache shared between all instances of this widget.
- partnerIds: {},
// This widget is only supported on many2ones pointing to 'res.users'
- supportedModel: 'res.users',
+ supportedModels: ['res.users'],
init() {
this._super(...arguments);
- if (this.supportedModel !== this.field.relation) {
- throw new Error(`This widget is only supported on many2one fields pointing to ${this.supportedModel}`);
+ if (!this.supportedModels.includes(this.field.relation)) {
+ throw new Error(`This widget is only supported on many2one fields pointing to ${JSON.stringify(this.supportedModels)}`);
}
if (this.mode === 'readonly') {
this.className += ' o_clickable_m2o_avatar';
}
- this.partnerField = 'partner_id'; // field to read on 'res.users' to get the partner id
- },
-
- //----------------------------------------------------------------------
- // Private
- //----------------------------------------------------------------------
-
- /**
- * Displays a warning when we can't open a DM chat window with the partner
- * corresponding to the clicked record. On model 'res.users', it only
- * happens when the user clicked on himself. This can be overridden by
- * extensions of this widget, for instance to handle the case where the
- * user clicked on a record whose partner isn't associated with any user.
- *
- * @private
- */
- _displayWarning() {
- this.displayNotification({
- message: _t('You cannot chat with yourself'),
- type: 'info',
- });
- },
- /**
- * @private
- * @param {number} resId
- * @returns {string} the key to use in the partnerIds cache
- */
- _getCacheKey(resId) {
- return `${this.field.relation}_${resId}`;
- },
- /**
- * For a given record id of the comodel, returns the corresponding
- * partner id.
- *
- * @param {number} resId
- * @returns {Promise<integer|false>}
- */
- _resIdToPartnerId(resId) {
- const key = this._getCacheKey(resId);
- if (!this.partnerIds[key]) {
- const params = {
- method: 'read',
- model: this.field.relation,
- args: [resId, [this.partnerField]],
- };
- this.partnerIds[key] = this._rpc(params).then(recs => {
- const partner = recs[0][this.partnerField];
- return partner && partner[0];
- });
- }
- return this.partnerIds[key];
},
//----------------------------------------------------------------------
@@ -105,27 +42,15 @@ odoo.define('mail.Many2OneAvatarUser', function (require) {
/**
* When the avatar is clicked, open a DM chat window with the
- * corresponding partner. If the user clicked on himself, open a blank
- * thread window, for the sake of consistency.
+ * corresponding user.
*
* @private
* @param {MouseEvent} ev
*/
async _onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
- let partnerId;
- if (this.field.relation !== 'res.users' || this.value.res_id !== session.uid) {
- partnerId = await this._resIdToPartnerId(this.value.res_id);
- }
- if (partnerId && partnerId !== session.partner_id) {
- const env = Component.env;
- const partner = env.models['mail.partner'].insert({
- id: partnerId,
- });
- partner.openChat();
- } else {
- this._displayWarning(partnerId);
- }
+ const env = Component.env;
+ await env.messaging.openChat({ userId: this.value.res_id });
}
});
diff --git a/addons/mail/static/src/models/chat_window/chat_window.js b/addons/mail/static/src/models/chat_window/chat_window.js
index ab1aa214750..a4db3e638de 100644
--- a/addons/mail/static/src/models/chat_window/chat_window.js
+++ b/addons/mail/static/src/models/chat_window/chat_window.js
@@ -47,7 +47,7 @@ function factory(dependencies) {
expand() {
if (this.thread) {
- this.thread.openExpanded();
+ this.thread.open({ expanded: true });
}
}
@@ -82,9 +82,23 @@ function factory(dependencies) {
}
/**
- * Assume that this chat window was hidden before-hand.
+ * Makes this chat window active, which consists of making it visible,
+ * unfolding it, and focusing it.
+ */
+ makeActive() {
+ this.makeVisible();
+ this.unfold();
+ this.focus();
+ }
+
+ /**
+ * Makes this chat window visible by swapping it with the last visible
+ * chat window, or do nothing if it is already visible.
*/
makeVisible() {
+ if (this.isVisible) {
+ return;
+ }
const lastVisible = this.manager.lastVisible;
this.manager.swap(this, lastVisible);
}
@@ -161,6 +175,17 @@ function factory(dependencies) {
/**
* @private
+ * @returns {boolean}
+ */
+ _computeIsVisible() {
+ if (!this.manager) {
+ return false;
+ }
+ return this.manager.allOrderedVisible.includes(this);
+ }
+
+ /**
+ * @private
* @returns {string}
*/
_computeName() {
@@ -316,6 +341,17 @@ function factory(dependencies) {
],
default: false,
}),
+ /**
+ * Whether this chat window is visible or not. Should be considered
+ * read-only. Setting this value manually will not make it visible.
+ * @see `makeVisible`
+ */
+ isVisible: attr({
+ compute: '_computeIsVisible',
+ dependencies: [
+ 'managerAllOrderedVisible',
+ ],
+ }),
manager: many2one('mail.chat_window_manager', {
inverse: 'chatWindows',
}),
diff --git a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
index 7cdf35ba734..8be11eba453 100644
--- a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
+++ b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js
@@ -70,6 +70,19 @@ function factory(dependencies) {
this.update({ isHiddenMenuOpen: false });
}
+ /**
+ * Closes all chat windows related to the given thread.
+ *
+ * @param {mail.thread} thread
+ */
+ closeThread(thread) {
+ for (const chatWindow of this.chatWindows) {
+ if (chatWindow.thread === thread) {
+ chatWindow.close();
+ }
+ }
+ }
+
openHiddenMenu() {
this.update({ isHiddenMenuOpen: true });
}
@@ -81,18 +94,17 @@ function factory(dependencies) {
manager: [['link', this]],
});
}
- newMessageChatWindow.makeVisible();
- newMessageChatWindow.unfold();
- newMessageChatWindow.focus();
+ newMessageChatWindow.makeActive();
}
/**
* @param {mail.thread} thread
* @param {Object} [param1={}]
- * @param {string} [param1.mode]
+ * @param {boolean} [param1.makeActive=false]
+ * @param {boolean} [param1.replaceNewMessage=false]
*/
- openThread(thread, { mode } = {}) {
- let chatWindow = this.env.models['mail.chat_window'].find(chatWindow =>
+ openThread(thread, { makeActive = false, replaceNewMessage = false } = {}) {
+ let chatWindow = this.chatWindows.find(chatWindow =>
chatWindow.thread === thread
);
if (!chatWindow) {
@@ -103,20 +115,15 @@ function factory(dependencies) {
}]],
});
}
- if (mode === 'last_visible' && !chatWindow.isVisible) {
- chatWindow.makeVisible();
- chatWindow.unfold();
- chatWindow.focus();
+ if (thread.foldState === 'closed') {
+ thread.update({ pendingFoldState: 'open' });
}
- if (mode === 'from_new_message') {
- if (!this.newMessageChatWindow) {
- throw new Error('Cannot open thread in chat window in mode "from_new_message" without any new message chat window');
- }
+ if (replaceNewMessage && this.newMessageChatWindow) {
this.swap(chatWindow, this.newMessageChatWindow);
this.newMessageChatWindow.close();
- chatWindow.makeVisible();
- chatWindow.unfold();
- chatWindow.focus();
+ }
+ if (makeActive) {
+ chatWindow.makeActive();
}
}
diff --git a/addons/mail/static/src/models/chatter/chatter.js b/addons/mail/static/src/models/chatter/chatter.js
index bfd5b8e96bb..836aac94060 100644
--- a/addons/mail/static/src/models/chatter/chatter.js
+++ b/addons/mail/static/src/models/chatter/chatter.js
@@ -36,6 +36,10 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
+ focus() {
+ this.update({ isDoFocus: true });
+ }
+
async refresh() {
const thread = this.thread;
if (!thread || thread.isTemporary) {
@@ -76,18 +80,14 @@ function factory(dependencies) {
showLogNote() {
this.update({ isComposerVisible: true });
- this.thread.composer.update({
- isDoFocus: true,
- isLog: true,
- });
+ this.thread.composer.update({ isLog: true });
+ this.focus();
}
showSendMessage() {
this.update({ isComposerVisible: true });
- this.thread.composer.update({
- isDoFocus: true,
- isLog: false,
- });
+ this.thread.composer.update({ isLog: false });
+ this.focus();
}
toggleActivityBoxVisibility() {
@@ -310,6 +310,12 @@ function factory(dependencies) {
default: false,
dependencies: ['threadId'],
}),
+ /**
+ * Determine whether this chatter should be focused at next render.
+ */
+ isDoFocus: attr({
+ default: false,
+ }),
isShowingAttachmentsLoading: attr({
default: false,
}),
diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js
index b04430ddc5e..2be5919204b 100644
--- a/addons/mail/static/src/models/composer/composer.js
+++ b/addons/mail/static/src/models/composer/composer.js
@@ -38,10 +38,6 @@ function factory(dependencies) {
}
}
- focus() {
- this.update({ isDoFocus: true });
- }
-
/**
* Inserts text content in text input based on selection.
*
@@ -606,9 +602,6 @@ function factory(dependencies) {
],
default: false,
}),
- isDoFocus: attr({
- default: false,
- }),
/**
* If true composer will log a note, else a comment will be posted.
*/
diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js
index 10d92315cec..2dae66f811f 100644
--- a/addons/mail/static/src/models/discuss/discuss.js
+++ b/addons/mail/static/src/models/discuss/discuss.js
@@ -38,24 +38,35 @@ function factory(dependencies) {
this.update({ isOpen: false });
}
+ focus() {
+ this.update({ isDoFocus: true });
+ }
+
/**
* @param {Event} ev
* @param {Object} ui
* @param {Object} ui.item
* @param {integer} ui.item.id
*/
- handleAddChannelAutocompleteSelect(ev, ui) {
+ async handleAddChannelAutocompleteSelect(ev, ui) {
+ const name = this.addingChannelValue;
+ this.clearIsAddingItem();
if (ui.item.special) {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- name: this.addingChannelValue,
- public: ui.item.special,
- type: 'channel',
- });
+ const channel = await this.async(() =>
+ this.env.models['mail.thread'].performRpcCreateChannel({
+ name,
+ privacy: ui.item.special,
+ })
+ );
+ channel.open();
} else {
- this.env.models['mail.thread'].joinChannel(ui.item.id, { autoselect: true });
+ const channel = await this.async(() =>
+ this.env.models['mail.thread'].performRpcJoinChannel({
+ channelId: ui.item.id,
+ })
+ );
+ channel.open();
}
- this.clearIsAddingItem();
}
/**
@@ -106,20 +117,7 @@ function factory(dependencies) {
* @param {integer} ui.item.id
*/
handleAddChatAutocompleteSelect(ev, ui) {
- const partnerId = ui.item.id;
- const partner = this.env.models['mail.partner'].find(partner =>
- partner.id === partnerId
- );
- const chat = partner.correspondentThreads.find(thread => thread.channel_type === 'chat');
- if (chat) {
- this.threadView.update({ thread: [['link', chat]] });
- } else {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- partnerId,
- type: 'chat',
- });
- }
+ this.env.messaging.openChat({ partnerId: ui.item.id });
this.clearIsAddingItem();
}
@@ -160,11 +158,31 @@ function factory(dependencies) {
if (!thread) {
return;
}
+ thread.open();
+ }
+
+
+ /**
+ * Opens the given thread in Discuss, and opens Discuss if necessary.
+ *
+ * @param {mail.thread} thread
+ */
+ async openThread(thread) {
this.threadView.update({
stringifiedDomain: '[]',
thread: [['link', thread]],
});
- thread.open({ resetDiscussDomain: true });
+ this.focus();
+ if (!this.isOpen) {
+ this.env.bus.trigger('do-action', {
+ action: 'mail.action_discuss',
+ options: {
+ active_id: this.threadToActiveId(this),
+ clear_breadcrumbs: false,
+ on_reverse_breadcrumb: () => this.close(),
+ },
+ });
+ }
}
/**
@@ -189,7 +207,7 @@ function factory(dependencies) {
this.replyingToMessageOriginThreadComposer.update({
isLog: !message.is_discussion && !message.is_notification
});
- this.replyingToMessageOriginThreadComposer.focus();
+ this.focus();
}
/**
@@ -382,6 +400,12 @@ function factory(dependencies) {
dependencies: ['isOpen'],
}),
/**
+ * Determine whether this discuss should be focused at next render.
+ */
+ isDoFocus: attr({
+ default: false,
+ }),
+ /**
* Whether the discuss app is open or not. Useful to determine
* whether the discuss or chat window logic should be applied.
*/
diff --git a/addons/mail/static/src/models/follower/follower.js b/addons/mail/static/src/models/follower/follower.js
index e5be843d7ce..6fb83649e2e 100644
--- a/addons/mail/static/src/models/follower/follower.js
+++ b/addons/mail/static/src/models/follower/follower.js
@@ -64,6 +64,16 @@ function factory(dependencies) {
}
/**
+ * Opens the most appropriate view that is a profile for this follower.
+ */
+ async openProfile() {
+ if (this.partner) {
+ return this.partner.openProfile();
+ }
+ return this.channel.openProfile();
+ }
+
+ /**
* Remove this follower from its related thread.
*/
async remove() {
diff --git a/addons/mail/static/src/models/messaging/messaging.js b/addons/mail/static/src/models/messaging/messaging.js
index c8c286c7db3..c215d2e5774 100644
--- a/addons/mail/static/src/models/messaging/messaging.js
+++ b/addons/mail/static/src/models/messaging/messaging.js
@@ -16,6 +16,17 @@ function factory(dependencies) {
return super._willDelete(...arguments);
}
+ /**
+ * Starts messaging and related records.
+ */
+ async start() {
+ this._handleGlobalWindowFocus = this._handleGlobalWindowFocus.bind(this);
+ this.env.services['bus_service'].on('window_focus', null, this._handleGlobalWindowFocus);
+ await this.async(() => this.initializer.start());
+ this.notificationHandler.start();
+ this.update({ isInitialized: true });
+ }
+
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
@@ -32,12 +43,53 @@ function factory(dependencies) {
/**
* 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 {mail.thread|undefined}
+ */
+ async getChat({ partnerId, userId }) {
+ if (userId) {
+ const user = this.env.models['mail.user'].insert({ id: userId });
+ return user.getChat();
+ }
+ if (partnerId) {
+ const partner = this.env.models['mail.partner'].insert({ id: partnerId });
+ return partner.getChat();
+ }
+ }
+
+ /**
+ * 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 `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(person, options) {
+ const chat = await this.async(() => this.getChat(person));
+ if (!chat) {
+ return;
+ }
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the form view of the record with provided id and model.
*
* @param {Object} param0
* @param {integer} param0.id
* @param {string} param0.model
*/
- openDocument({ id, model }) {
+ async openDocument({ id, model }) {
this.env.bus.trigger('do-action', {
action: {
type: 'ir.actions.act_window',
@@ -46,79 +98,47 @@ function factory(dependencies) {
res_id: id,
},
});
- this.messagingMenu.close();
+ if (this.env.messaging.device.isMobile) {
+ // messaging menu has a higher z-index than views so it must
+ // be closed to ensure the visibility of the view
+ this.env.messaging.messagingMenu.close();
+ }
}
/**
- * Handles redirection to a model and id. Try to handle it in the context
- * of messaging (e.g. open chat if this is a user), otherwise fallback to
- * opening form view of record.
+ * 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
- * FIXME needs to be tested and maybe refactored (see task-2244279)
*/
- async redirect({ id, model }) {
- if (model === 'mail.channel') {
- const channel = this.env.models['mail.thread'].find(thread =>
- thread.id === id &&
- thread.model === 'mail.channel'
- );
- if (!channel || !channel.isPinned) {
- this.env.models['mail.thread'].joinChannel(id, { autoselect: true });
- return;
- }
- channel.open();
- } else if (model === 'res.partner') {
- if (id === this.currentPartner.id) {
- this.openDocument({
- model: 'res.partner',
- id,
- });
- return;
- }
+ async openProfile({ id, model }) {
+ if (model === 'res.partner') {
const partner = this.env.models['mail.partner'].insert({ id });
- if (!partner.user) {
- await this.async(() => partner.checkIsUser());
- }
- if (!partner.user) {
- // partner is not a user, open document instead
- this.openDocument({
- model: 'res.partner',
- id: partner.id,
- });
- return;
+ return partner.openProfile();
+ }
+ if (model === 'res.users') {
+ const user = this.env.models['mail.user'].insert({ id });
+ return user.openProfile();
+ }
+ if (model === 'mail.channel') {
+ let channel = this.env.models['mail.thread'].findFromIdentifyingData({ id, model: 'mail.channel' });
+ if (!channel) {
+ channel = (await this.async(() =>
+ this.env.models['mail.thread'].performRpcChannelInfo({ ids: [id] })
+ ))[0];
}
- const chat = partner.correspondentThreads.find(thread =>
- thread.channel_type === 'chat'
- );
- if (!chat) {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- partnerId: id,
- type: 'chat',
+ if (!channel) {
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only open the profile of existing channels."),
+ type: 'warning',
});
return;
}
- chat.open();
- } else {
- this.openDocument({
- model,
- id,
- });
+ return channel.openProfile();
}
- }
-
- /**
- * Start messaging and related records.
- */
- async start() {
- this._handleGlobalWindowFocus = this._handleGlobalWindowFocus.bind(this);
- this.env.services['bus_service'].on('window_focus', null, this._handleGlobalWindowFocus);
- await this.async(() => this.initializer.start());
- this.notificationHandler.start();
- this.update({ isInitialized: true });
+ return this.env.messaging.openDocument({ id, model });
}
//----------------------------------------------------------------------
diff --git a/addons/mail/static/src/models/messaging/messaging_tests.js b/addons/mail/static/src/models/messaging/messaging_tests.js
index a5d57bf79bd..b306fbb17f3 100644
--- a/addons/mail/static/src/models/messaging/messaging_tests.js
+++ b/addons/mail/static/src/models/messaging/messaging_tests.js
@@ -21,6 +21,102 @@ QUnit.module('messaging_tests.js', {
afterEach() {
afterEach(this);
},
+}, function () {
+
+QUnit.test('openChat: display notification for partner without user', async function (assert) {
+ assert.expect(2);
+
+ this.data['res.partner'].records.push({ id: 14 });
+ await this.start();
+
+ await this.env.messaging.openChat({ partnerId: 14 });
+ assert.containsOnce(
+ document.body,
+ '.toast .o_notification_content',
+ "should display a toast notification after failing to open chat"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "You can only chat with partners that have a dedicated user.",
+ "should display the correct information in the notification"
+ );
+});
+
+QUnit.test('openChat: display notification for wrong user', async function (assert) {
+ assert.expect(2);
+
+ await this.start();
+
+ // user id not in this.data
+ await this.env.messaging.openChat({ userId: 14 });
+ assert.containsOnce(
+ document.body,
+ '.toast .o_notification_content',
+ "should display a toast notification after failing to open chat"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "You can only chat with existing users.",
+ "should display the correct information in the notification"
+ );
+});
+
+QUnit.test('openChat: open new chat for user', async function (assert) {
+ assert.expect(3);
+
+ this.data['res.partner'].records.push({ id: 14 });
+ this.data['res.users'].records.push({ id: 11, partner_id: 14 });
+ await this.start();
+
+ const existingChat = this.env.models['mail.thread'].find(thread =>
+ thread.channel_type === 'chat' &&
+ thread.correspondent &&
+ thread.correspondent.id === 14 &&
+ thread.model === 'mail.channel' &&
+ thread.public === 'private'
+ );
+ assert.notOk(existingChat, 'a chat should not exist with the target partner initially');
+
+ await this.env.messaging.openChat({ partnerId: 14 });
+ const chat = this.env.models['mail.thread'].find(thread =>
+ thread.channel_type === 'chat' &&
+ thread.correspondent &&
+ thread.correspondent.id === 14 &&
+ thread.model === 'mail.channel' &&
+ thread.public === 'private'
+ );
+ assert.ok(chat, 'a chat should exist with the target partner');
+ assert.strictEqual(chat.threadViews.length, 1, 'the chat should be displayed in a `mail.thread_view`');
+});
+
+QUnit.test('openChat: open existing chat for user', async function (assert) {
+ assert.expect(5);
+
+ this.data['res.partner'].records.push({ id: 14 });
+ this.data['res.users'].records.push({ id: 11, partner_id: 14 });
+ this.data['mail.channel'].records.push({
+ channel_type: "chat",
+ id: 10,
+ members: [this.data.currentPartnerId, 14],
+ public: 'private',
+ });
+ await this.start();
+ const existingChat = this.env.models['mail.thread'].find(thread =>
+ thread.channel_type === 'chat' &&
+ thread.correspondent &&
+ thread.correspondent.id === 14 &&
+ thread.model === 'mail.channel' &&
+ thread.public === 'private'
+ );
+ assert.ok(existingChat, 'a chat should initially exist with the target partner');
+ assert.strictEqual(existingChat.threadViews.length, 0, 'the chat should not be displayed in a `mail.thread_view`');
+
+ await this.env.messaging.openChat({ partnerId: 14 });
+ assert.ok(existingChat, 'a chat should still exist with the target partner');
+ assert.strictEqual(existingChat.id, 10, 'the chat should be the existing chat');
+ assert.strictEqual(existingChat.threadViews.length, 1, 'the chat should now be displayed in a `mail.thread_view`');
+});
+
});
});
diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
index d89992d8a4b..29923af1e39 100644
--- a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
+++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -78,9 +78,7 @@ function factory(dependencies) {
case 'ir.needaction':
return this._handleNotificationNeedaction(message);
case 'mail.channel':
- return this._handleNotificationChannel(Object.assign({
- channelId: id,
- }, message));
+ return this._handleNotificationChannel(id, message);
case 'res.partner':
if (id !== this.env.messaging.currentPartner.id) {
// ignore broadcast to other partners
@@ -97,16 +95,15 @@ function factory(dependencies) {
/**
* @private
+ * @param {integer} channelId
* @param {Object} data
- * @param {integer} data.channelId
* @param {string} [data.info]
* @param {boolean} [data.is_typing]
* @param {integer} [data.last_message_id]
* @param {integer} [data.partner_id]
*/
- async _handleNotificationChannel(data) {
+ async _handleNotificationChannel(channelId, data) {
const {
- channelId,
id,
info,
is_typing,
@@ -115,41 +112,37 @@ function factory(dependencies) {
} = data;
switch (info) {
case 'channel_fetched':
- return this._handleNotificationChannelFetched({
+ return this._handleNotificationChannelFetched(channelId, {
id,
- channelId,
last_message_id,
partner_id,
});
case 'channel_seen':
- return this._handleNotificationChannelSeen({
+ return this._handleNotificationChannelSeen(channelId, {
id,
- channelId,
last_message_id,
partner_id,
});
case 'typing_status':
- return this._handleNotificationChannelTypingStatus({
- channelId,
+ return this._handleNotificationChannelTypingStatus(channelId, {
is_typing,
partner_id,
});
default:
- return this._handleNotificationChannelMessage(data);
+ return this._handleNotificationChannelMessage(channelId, data);
}
}
/**
* @private
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {integer} param0.id
- * @param {integer} param0.last_message_id
- * @param {integer} param0.partner_id
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {integer} param1.id
+ * @param {integer} param1.last_message_id
+ * @param {integer} param1.partner_id
*/
- async _handleNotificationChannelFetched({
+ async _handleNotificationChannelFetched(channelId, {
id,
- channelId,
last_message_id,
partner_id,
}) {
@@ -183,21 +176,18 @@ function factory(dependencies) {
/**
* @private
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {...Object} param0.data
- * @param {Array} [param0.data.author_id]
- * @param {integer} param0.data.author_id[0]
- * @param {integer[]} param0.data.channel_ids
+ * @param {integer} channelId
+ * @param {Object} messageData
*/
- async _handleNotificationChannelMessage(param0) {
- const channelId = param0.channelId;
- const convertedData = this.env.models['mail.message'].convertData(param0);
-
- const oldMessage = this.env.models['mail.message'].find(message =>
- message.id === convertedData.id
- );
- // locally save old value, as insert would overwrite them
+ async _handleNotificationChannelMessage(channelId, messageData) {
+ let channel = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: channelId,
+ model: 'mail.channel',
+ });
+ const wasChannelExisting = !!channel;
+ const convertedData = this.env.models['mail.message'].convertData(messageData);
+ const oldMessage = this.env.models['mail.message'].findFromIdentifyingData(convertedData);
+ // locally save old values, as insert would overwrite them
const oldMessageModerationStatus = (
oldMessage && oldMessage.moderation_status
);
@@ -205,16 +195,21 @@ function factory(dependencies) {
oldMessage && oldMessage.isModeratedByCurrentPartner
);
- const message = this.env.models['mail.message'].insert(convertedData);
-
- // join the corresponding channel if necessary
- const channel = this.env.models['mail.thread'].findFromIdentifyingData({
- id: channelId,
- model: 'mail.channel',
- });
- if (!channel || !channel.isPinned) {
- this.env.models['mail.thread'].joinChannel(channelId);
+ // Fetch missing info from channel before going further. Inserting
+ // a channel with incomplete info can lead to issues. This is in
+ // particular the case with the `uuid` field that is assumed
+ // "required" by the rest of the code and is necessary for some
+ // features such as chat windows.
+ if (!channel) {
+ channel = (await this.async(() =>
+ this.env.models['mail.thread'].performRpcChannelInfo({ ids: [channelId] })
+ ))[0];
}
+ if (!channel.isPinned) {
+ channel.update({ isPendingPinned: true });
+ }
+
+ const message = this.env.models['mail.message'].insert(convertedData);
// If the message was already known: nothing else should be done,
// except if it was pending moderation by the current partner, then
@@ -236,34 +231,49 @@ function factory(dependencies) {
return;
}
- // If the channel wasn't known, joining the channel already updated
- // the counter.
- if (!channel) {
- return;
- }
-
// Message from mailing channel should not make a notification in
// Odoo for users with notification "Handled by Email".
// Channel has been marked as read server-side in this case, so
// it should not display a notification by incrementing the
// unread counter.
if (
- channel &&
channel.mass_mailing &&
this.env.session.notification_type === 'email'
) {
return;
}
+ // In all other cases: update counter and notify if necessary
- // In all other cases: update counter and notify if out of focus.
- const isOdooFocused = this.env.services['bus_service'].isOdooFocused();
- if (!isOdooFocused) {
- this._notifyNewChannelMessageWhileOutOfFocus({ channel, message });
- }
- if (channel.model === 'mail.channel') {
+ // Chat from OdooBot is considered disturbing and should only be
+ // shown on the menu, but no notification and no thread open.
+ const isChatWithOdooBot = (
+ channel.correspondent &&
+ channel.correspondent === this.env.messaging.partnerRoot
+ );
+ if (!isChatWithOdooBot) {
+ // Notify if out of focus
+ const isOdooFocused = this.env.services['bus_service'].isOdooFocused();
+ if (!isOdooFocused) {
+ this._notifyNewChannelMessageWhileOutOfFocus({
+ channel,
+ message,
+ });
+ }
channel.markAsFetched();
+ // (re)open chat on receiving new message
+ if (channel.channel_type !== 'channel') {
+ this.env.messaging.chatWindowManager.openThread(channel);
+ }
+ }
+
+ // If the channel wasn't known its correct counter was fetched at
+ // the start of the method, no need update it here.
+ if (!wasChannelExisting) {
+ return;
}
- channel.update({ message_unread_counter: channel.message_unread_counter + 1 });
+ channel.update({
+ message_unread_counter: channel.message_unread_counter + 1,
+ });
// manually force recompute of counter
this.messaging.messagingMenu.update();
}
@@ -273,14 +283,13 @@ function factory(dependencies) {
* last message seen. Useful in order to track last message seen.
*
* @private
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {integer} param0.id
- * @param {integer} param0.last_message_id
- * @param {integer} param0.partner_id
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {integer} param1.id
+ * @param {integer} param1.last_message_id
+ * @param {integer} param1.partner_id
*/
- async _handleNotificationChannelSeen({
- channelId,
+ async _handleNotificationChannelSeen(channelId, {
id,
last_message_id,
partner_id,
@@ -328,16 +337,12 @@ function factory(dependencies) {
/**
* @private
- * @param {Object} param0
- * @param {integer} param0.channelId
- * @param {boolean} param0.is_typing
- * @param {integer} param0.partner_id
+ * @param {integer} channelId
+ * @param {Object} param1
+ * @param {boolean} param1.is_typing
+ * @param {integer} param1.partner_id
*/
- _handleNotificationChannelTypingStatus({
- channelId,
- is_typing,
- partner_id,
- }) {
+ _handleNotificationChannelTypingStatus(channelId, { is_typing, partner_id }) {
const channel = this.env.models['mail.thread'].findFromIdentifyingData({
id: channelId,
model: 'mail.channel',
@@ -456,7 +461,9 @@ function factory(dependencies) {
* @param {string} data.uuid
*/
_handleNotificationPartnerChannel(data) {
- const convertedData = this.env.models['mail.thread'].convertData(data);
+ const convertedData = this.env.models['mail.thread'].convertData(
+ Object.assign({ model: 'mail.channel' }, data)
+ );
let channel = this.env.models['mail.thread'].findFromIdentifyingData(convertedData);
const wasCurrentPartnerMember = (
@@ -686,12 +693,18 @@ function factory(dependencies) {
* @param {integer} param0.partner_id
* @param {string} param0.title
*/
- _handleNotificationPartnerUserConnection({ message, partner_id, title }) {
+ async _handleNotificationPartnerUserConnection({ message, partner_id, title }) {
+ // If the current user invited a new user, and the new user is
+ // connecting for the first time while the current user is present
+ // then open a chat for the current user with the new user.
this.env.services['bus_service'].sendNotification(title, message);
- const partner = this.env.models['mail.partner'].insert({
- id: partner_id,
- });
- partner.openChat();
+ const chat = await this.async(() =>
+ this.env.messaging.getChat({ partnerId: partner_id }
+ ));
+ if (!chat) {
+ return;
+ }
+ this.env.messaging.chatWindowManager.openThread(chat);
}
/**
diff --git a/addons/mail/static/src/models/notification_group/notification_group.js b/addons/mail/static/src/models/notification_group/notification_group.js
index 415350332cc..89111a4ed74 100644
--- a/addons/mail/static/src/models/notification_group/notification_group.js
+++ b/addons/mail/static/src/models/notification_group/notification_group.js
@@ -2,7 +2,7 @@ odoo.define('mail/static/src/models/notification_group/notification_group.js', f
'use strict';
const { registerNewModel } = require('mail/static/src/model/model_core.js');
-const { attr, one2many } = require('mail/static/src/model/model_field.js');
+const { attr, many2one, one2many } = require('mail/static/src/model/model_field.js');
function factory(dependencies) {
@@ -16,21 +16,18 @@ function factory(dependencies) {
* Opens the view that allows to cancel all notifications of the group.
*/
openCancelAction() {
- const device = this.env.messaging.device;
- if (this.notification_type === 'email') {
- this.env.bus.trigger('do-action', {
- action: 'mail.mail_resend_cancel_action',
- options: {
- additional_context: {
- default_model: this.res_model,
- unread_counter: this.notifications.length,
- },
- },
- });
- }
- if (!device.isMobile) {
- this.env.messaging.messagingMenu.close();
+ if (this.notification_type !== 'email') {
+ return;
}
+ this.env.bus.trigger('do-action', {
+ action: 'mail.mail_resend_cancel_action',
+ options: {
+ additional_context: {
+ default_model: this.res_model,
+ unread_counter: this.notifications.length,
+ },
+ },
+ });
}
/**
@@ -38,19 +35,8 @@ function factory(dependencies) {
* all the records in the group.
*/
openDocuments() {
- if (this.res_id) {
- if (this.res_model === 'mail.channel') {
- const channel = this.env.models['mail.thread'].insert({
- id: this.res_id,
- model: this.res_model,
- });
- channel.open();
- } else {
- this.env.messaging.openDocument({
- id: this.res_id,
- model: this.res_model,
- });
- }
+ if (this.thread) {
+ this.thread.open();
} else {
this._openDocuments();
}
@@ -61,6 +47,20 @@ function factory(dependencies) {
//----------------------------------------------------------------------
/**
+ * @private
+ * @returns {mail.thread|undefined}
+ */
+ _computeThread() {
+ if (this.res_id) {
+ return [['insert', {
+ id: this.res_id,
+ model: this.res_model,
+ }]];
+ }
+ return [['unlink']];
+ }
+
+ /**
* @override
*/
static _createRecordLocalId(data) {
@@ -73,21 +73,23 @@ function factory(dependencies) {
* @private
*/
_openDocuments() {
- const device = this.env.messaging.device;
- if (this.notification_type === 'email') {
- this.env.bus.trigger('do-action', {
- action: {
- name: this.env._t("Mail Failures"),
- type: 'ir.actions.act_window',
- view_mode: 'kanban,list,form',
- views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
- target: 'current',
- res_model: this.res_model,
- domain: [['message_has_error', '=', true]],
- },
- });
+ if (this.notification_type !== 'email') {
+ return;
}
- if (!device.isMobile) {
+ this.env.bus.trigger('do-action', {
+ action: {
+ name: this.env._t("Mail Failures"),
+ type: 'ir.actions.act_window',
+ view_mode: 'kanban,list,form',
+ views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ target: 'current',
+ res_model: this.res_model,
+ domain: [['message_has_error', '=', true]],
+ },
+ });
+ if (this.env.messaging.device.isMobile) {
+ // messaging menu has a higher z-index than views so it must
+ // be closed to ensure the visibility of the view
this.env.messaging.messagingMenu.close();
}
}
@@ -102,6 +104,16 @@ function factory(dependencies) {
res_id: attr(),
res_model: attr(),
res_model_name: attr(),
+ /**
+ * Related thread when the notification group concerns a single thread.
+ */
+ thread: many2one('mail.thread', {
+ compute: '_computeThread',
+ dependencies: [
+ 'res_id',
+ 'res_model',
+ ],
+ })
};
NotificationGroup.modelName = 'mail.notification_group';
diff --git a/addons/mail/static/src/models/partner/partner.js b/addons/mail/static/src/models/partner/partner.js
index 91b3d0f482b..0b08451efcd 100644
--- a/addons/mail/static/src/models/partner/partner.js
+++ b/addons/mail/static/src/models/partner/partner.js
@@ -125,31 +125,73 @@ function factory(dependencies) {
this._loopFetchImStatus();
}
+ /**
+ * Checks whether this partner has a related user and links them if
+ * applicable.
+ */
async checkIsUser() {
const userIds = await this.async(() => this.env.services.rpc({
model: 'res.users',
method: 'search',
args: [[['partner_id', '=', this.id]]],
+ kwargs: {
+ context: { active_test: false },
+ },
}));
- if (userIds.length) {
+ this.update({ hasCheckedUser: true });
+ if (userIds.length > 0) {
this.update({ user: [['insert', { id: userIds[0] }]] });
}
}
/**
- * Opens an existing or new chat.
+ * Gets the chat between the user of this partner and the current user.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @returns {mail.thread|undefined}
*/
- openChat() {
- const chat = this.correspondentThreads.find(thread => thread.channel_type === 'chat');
- if (chat) {
- chat.open();
- } else {
- this.env.models['mail.thread'].createChannel({
- autoselect: true,
- partnerId: this.id,
- type: 'chat',
+ async getChat() {
+ if (!this.user && !this.hasCheckedUser) {
+ await this.async(() => this.checkIsUser());
+ }
+ // prevent chatting with non-users
+ if (!this.user) {
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only chat with partners that have a dedicated user."),
+ type: 'info',
});
+ return;
+ }
+ return this.user.getChat();
+ }
+
+ /**
+ * Opens a chat between the user of this partner and the current user
+ * and returns it.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @param {Object} [options] forwarded to @see `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(options) {
+ const chat = await this.async(() => this.getChat());
+ if (!chat) {
+ return;
}
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the most appropriate view that is a profile for this partner.
+ */
+ async openProfile() {
+ return this.env.messaging.openDocument({
+ id: this.id,
+ model: 'res.partner',
+ });
}
//----------------------------------------------------------------------
@@ -235,6 +277,13 @@ function factory(dependencies) {
failureNotifications: one2many('mail.notification', {
related: 'messagesAsAuthor.failureNotifications',
}),
+ /**
+ * Whether an attempt was already made to fetch the user corresponding
+ * to this partner. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedUser: attr({
+ default: false,
+ }),
id: attr(),
im_status: attr(),
memberThreads: many2many('mail.thread', {
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index d34d295223a..efd33f9a0ce 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -133,6 +133,9 @@ function factory(dependencies) {
*/
static convertData(data) {
const data2 = {};
+ if ('model' in data) {
+ data2.model = data.model;
+ }
if ('channel_type' in data) {
data2.channel_type = data.channel_type;
data2.model = 'mail.channel';
@@ -249,103 +252,140 @@ function factory(dependencies) {
}
/**
- * Create a channel, which is a special kind of thread on model
- * 'mail.channel' with multiple members.
+ * Load the previews of the specified threads. Basically, it fetches the
+ * last messages, since they are used to display inline content of them.
+ *
+ * @static
+ * @param {mail.thread[]} threads
+ */
+ static async loadPreviews(threads) {
+ const channelIds = threads.reduce((list, thread) => {
+ if (thread.model === 'mail.channel') {
+ return list.concat(thread.id);
+ }
+ return list;
+ }, []);
+ const channelPreviews = await this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'channel_fetch_preview',
+ args: [channelIds],
+ }, { shadow: true });
+ this.env.models['mail.message'].insert(channelPreviews.filter(p => p.last_message).map(
+ channelPreview => this.env.models['mail.message'].convertData(channelPreview.last_message)
+ ));
+ }
+
+ /**
+ * Performs the `channel_info` RPC on `mail.channel`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {integer[]} param0.ids list of id of channels
+ * @returns {mail.thread[]}
+ */
+ static async performRpcChannelInfo({ ids }) {
+ const channelInfos = await this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'channel_info',
+ args: [ids],
+ });
+ const channels = this.env.models['mail.thread'].insert(
+ channelInfos.map(channelInfo => this.env.models['mail.thread'].convertData(channelInfo))
+ );
+ // manually force recompute of counter
+ this.env.messaging.messagingMenu.update();
+ return channels;
+ }
+
+ /**
+ * Performs the `channel_create` RPC on `mail.channel`.
*
* @static
* @param {Object} param0
- * @param {boolean} [param0.autoselect=false] if set, when channel
- * has been created, it auto-open the channel. This opens in discuss
- * or chat window, depending on whether discuss is open or not.
- * @param {string} [param0.autoselectChatWindowMode]
* @param {string} param0.name
- * @param {integer} [param0.partnerId]
- * @param {string} [param0.public]
- * @param {string} param0.type
- */
- static async createChannel({
- autoselect = false,
- autoselectChatWindowMode,
- name,
- partnerId,
- public: publicStatus,
- type,
- }) {
+ * @param {string} [param0.privacy]
+ * @returns {mail.thread} the created channel
+ */
+ static async performRpcCreateChannel({ name, privacy }) {
const device = this.env.messaging.device;
- // TODO FIX: potential duplicate chat task-2276490
const data = await this.env.services.rpc({
model: 'mail.channel',
- method: type === 'chat' ? 'channel_get' : 'channel_create',
- args: type === 'chat' ? [[partnerId]] : [name, publicStatus],
+ method: 'channel_create',
+ args: [name, privacy],
kwargs: {
context: Object.assign({}, this.env.session.user_content, {
+ // optimize the return value by avoiding useless queries
+ // in non-mobile devices
isMobile: device.isMobile,
}),
- }
+ },
});
- const thread = this.insert(this.convertData(data));
- if (autoselect) {
- thread.open({ chatWindowMode: autoselectChatWindowMode });
- }
+ return this.env.models['mail.thread'].insert(
+ this.env.models['mail.thread'].convertData(data)
+ );
}
/**
- * Join a channel. This channel may not yet exists in the store.
+ * Performs the `channel_get` RPC on `mail.channel`.
+ *
+ * `openChat` is preferable in business code because it will avoid the
+ * RPC if the chat already exists.
*
* @static
- * @param {integer} channelId
- * @param {Object} [param1={}]
- * @param {boolean} [param1.autoselect=false]
- */
- static async joinChannel(channelId, { autoselect = false } = {}) {
- let channel = this.find(thread =>
- thread.id === channelId &&
- thread.model === 'mail.channel'
- );
- if (channel && channel.isPinned) {
- return;
- }
+ * @param {Object} param0
+ * @param {integer[]} param0.partnerIds
+ * @param {boolean} [param0.pinForCurrentPartner]
+ * @returns {mail.thread|undefined} the created or existing chat
+ */
+ static async performRpcCreateChat({ partnerIds, pinForCurrentPartner }) {
+ const device = this.env.messaging.device;
+ // TODO FIX: potential duplicate chat task-2276490
const data = await this.env.services.rpc({
model: 'mail.channel',
- method: 'channel_join_and_get_info',
- args: [[channelId]]
+ method: 'channel_get',
+ kwargs: {
+ context: Object.assign({}, this.env.session.user_content, {
+ // optimize the return value by avoiding useless queries
+ // in non-mobile devices
+ isMobile: device.isMobile,
+ }),
+ partners_to: partnerIds,
+ pin: pinForCurrentPartner,
+ },
});
- // We just joined the channel because of the previous rpc
- // the main assumption here is that we didn't have the channel
- // in memory. If we did though, clear the pending state and
- // let the server's data be the master
- const convertedData = Object.assign(
- { isPendingPinned: undefined },
- this.convertData(data),
- );
- channel = this.insert(convertedData);
- if (autoselect) {
- channel.open({ resetDiscussDomain: true });
+ if (!data) {
+ return;
}
+ return this.env.models['mail.thread'].insert(
+ this.env.models['mail.thread'].convertData(data)
+ );
}
/**
- * Load the previews of the specified threads. Basically, it fetches the
- * last messages, since they are used to display inline content of them.
+ * Performs the `channel_join_and_get_info` RPC on `mail.channel`.
*
* @static
- * @param {mail.thread[]} threads
+ * @param {Object} param0
+ * @param {integer} param0.channelId
+ * @returns {mail.thread} the channel that was joined
*/
- static async loadPreviews(threads) {
- const channelIds = threads.reduce((list, thread) => {
- if (thread.model === 'mail.channel') {
- return list.concat(thread.id);
- }
- return list;
- }, []);
- const channelPreviews = await this.env.services.rpc({
+ static async performRpcJoinChannel({ channelId }) {
+ const device = this.env.messaging.device;
+ const data = await this.env.services.rpc({
model: 'mail.channel',
- method: 'channel_fetch_preview',
- args: [channelIds],
- }, { shadow: true });
- this.env.models['mail.message'].insert(channelPreviews.filter(p => p.last_message).map(
- channelPreview => this.env.models['mail.message'].convertData(channelPreview.last_message)
- ));
+ method: 'channel_join_and_get_info',
+ args: [[channelId]],
+ kwargs: {
+ context: Object.assign({}, this.env.session.user_content, {
+ // optimize the return value by avoiding useless queries
+ // in non-mobile devices
+ isMobile: device.isMobile,
+ }),
+ },
+ });
+ return this.env.models['mail.thread'].insert(
+ this.env.models['mail.thread'].convertData(data)
+ );
}
/**
@@ -453,7 +493,7 @@ function factory(dependencies) {
*
* Only makes sense if pendingFoldState is set to the desired value.
*/
- async notifyFoldStateToServer() {
+ notifyFoldStateToServer() {
// method is called from _updateAfter so it cannot be async
this.env.services.rpc({
model: 'mail.channel',
@@ -469,75 +509,67 @@ function factory(dependencies) {
* Notify server to leave the current channel. Useful for cross-tab
* and cross-device chat window state synchronization.
*
- * Only makes sense if pendingServerState is set to 'unpin'.
+ * Only makes sense if isPendingPinned is set to the desired value.
*/
- notifyUnPinToServer() {
+ notifyPinStateToServer() {
// method is called from _updateAfter so it cannot be async
- this.env.services.rpc({
- model: 'mail.channel',
- method: 'execute_command',
- args: [[this.id], 'leave']
- });
+ if (this.isPendingPinned) {
+ this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'channel_pin',
+ kwargs: {
+ uuid: this.uuid,
+ pinned: true,
+ },
+ }, { shadow: true });
+ } else {
+ this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'execute_command',
+ args: [[this.id], 'leave']
+ }, { shadow: true });
+ }
}
/**
- * Open provided thread, either in discuss app or as a chat window.
+ * Opens this thread either as form view, in discuss app, or as a chat
+ * window. The thread will be opened in an "active" matter, which will
+ * interrupt current user flow.
*
- * @param {Object} param0
- * @param {string} [param0.chatWindowMode='last_visible']
- * @param {boolean} [param0.resetDiscussDomain=false]
+ * @param {Object} [param0]
+ * @param {boolean} [param0.expanded=false]
*/
- open({ chatWindowMode = 'last_visible', resetDiscussDomain = false } = {}) {
- const device = this.env.messaging.device;
- const discuss = this.env.messaging.discuss;
- const messagingMenu = this.env.messaging.messagingMenu;
+ async open({ expanded = false } = {}) {
+ // check if thread must be opened in form view
if (!['mail.box', 'mail.channel'].includes(this.model)) {
- this.env.messaging.openDocument({
+ return this.env.messaging.openDocument({
id: this.id,
model: this.model,
});
}
+ // check if thread must be opened in discuss
+ const device = this.env.messaging.device;
+ const discuss = this.env.messaging.discuss;
if (
- (!device.isMobile && discuss.isOpen) ||
- (device.isMobile && this.model === 'mail.box')
+ (!device.isMobile && (discuss.isOpen || expanded)) ||
+ this.model === 'mail.box'
) {
- if (resetDiscussDomain) {
- discuss.threadView.update({ stringifiedDomain: '[]' });
- }
- discuss.threadView.update({ thread: [['link', this]] });
- } else {
- this.env.messaging.chatWindowManager.openThread(this, { mode: chatWindowMode });
- }
- if (!device.isMobile) {
- messagingMenu.close();
+ return discuss.openThread(this);
}
+ // thread must be opened in chat window
+ return this.env.messaging.chatWindowManager.openThread(this, {
+ makeActive: true,
+ });
}
/**
- * Open this thread in an expanded way, that is not in a chat window.
+ * Opens the most appropriate view that is a profile for this thread.
*/
- openExpanded() {
- const discuss = this.env.messaging.discuss;
- if (['mail.channel', 'mail.box'].includes(this.model)) {
- discuss.threadView.update({ thread: [['link', this]] });
- this.env.bus.trigger('do-action', {
- action: 'mail.action_discuss',
- options: {
- clear_breadcrumbs: false,
- active_id: discuss.threadToActiveId(this),
- on_reverse_breadcrumb: () => discuss.close(),
- },
- });
- } else {
- this.env.bus.trigger('do-action', {
- action: {
- type: 'ir.actions.act_window',
- res_model: this.model,
- views: [[false, 'form']],
- res_id: this.id,
- },
- });
- }
+ async openProfile() {
+ return this.env.messaging.openDocument({
+ id: this.id,
+ model: this.model,
+ });
}
/**
@@ -760,15 +792,6 @@ function factory(dependencies) {
/**
* @private
- * @returns {mail.chat_window[]}
- */
- _computeChatWindows() {
- const chatWindowThreadViews = this.threadViews.filter(threadView => !!threadView.chatWindow);
- return [['replace', chatWindowThreadViews.map(threadView => threadView.chatWindow)]];
- }
-
- /**
- * @private
* @returns {mail.partner}
*/
_computeCorrespondent() {
@@ -1088,10 +1111,10 @@ function factory(dependencies) {
this.update({ pendingFoldState: undefined });
}
if (
- this.isPendingPinned === false &&
+ this.isPendingPinned !== undefined &&
previous.isPendingPinned !== this.isPendingPinned
) {
- this.notifyUnPinToServer();
+ this.notifyPinStateToServer();
}
if (
this.isServerPinned === this.isPendingPinned
@@ -1110,16 +1133,14 @@ function factory(dependencies) {
// the process
return;
}
- if (this.foldState !== 'closed' && this.chatWindows.length === 0) {
- // condition to avoid crash during destroy
- if (this.env.messaging.chatWindowManager) {
- this.env.messaging.chatWindowManager.openThread(this);
- }
+ if (!this.env.messaging.chatWindowManager) {
+ // avoid crash during destroy
+ return;
}
- if (this.foldState === 'closed' && this.chatWindows.length > 0) {
- for (const chatWindow of this.chatWindows) {
- chatWindow.close();
- }
+ if (this.foldState !== 'closed') {
+ this.env.messaging.chatWindowManager.openThread(this);
+ } else {
+ this.env.messaging.chatWindowManager.closeThread(this);
}
}
@@ -1192,18 +1213,8 @@ function factory(dependencies) {
isCausal: true,
}),
channel_type: attr(),
- chatWindows: one2many('mail.chat_window', {
- compute: '_computeChatWindows',
- dependencies: ['threadViewsChatWindow'],
- }),
composer: one2one('mail.composer', {
- /**
- * FIXME With this, whenever client is aware of new thread, this will
- * (almost) always focus its composer when displayed. This shouldn't be
- * the case, instead auto-focus of composer is flow-specific.
- * See task-2277537
- */
- default: [['create', { isDoFocus: true }]],
+ default: [['create']],
inverse: 'thread',
isCausal: true,
}),
@@ -1475,9 +1486,6 @@ function factory(dependencies) {
threadViews: one2many('mail.thread_view', {
inverse: 'thread',
}),
- threadViewsChatWindow: many2many('mail.chat_window', {
- related: 'threadViews.chatWindow',
- }),
};
Thread.modelName = 'mail.thread';
diff --git a/addons/mail/static/src/models/user/user.js b/addons/mail/static/src/models/user/user.js
index b9bc72bf987..18b2c28e1ac 100644
--- a/addons/mail/static/src/models/user/user.js
+++ b/addons/mail/static/src/models/user/user.js
@@ -25,6 +25,66 @@ function factory(dependencies) {
//----------------------------------------------------------------------
/**
+ * @static
+ * @param {Object} data
+ * @returns {Object}
+ */
+ static convertData(data) {
+ const data2 = {};
+ if ('id' in data) {
+ data2.id = data.id;
+ }
+ if ('partner_id' in data) {
+ if (!data.partner_id) {
+ data2.partner = [['unlink']];
+ } else {
+ const partnerNameGet = data['partner_id'];
+ const partnerData = {
+ display_name: partnerNameGet[1],
+ id: partnerNameGet[0],
+ };
+ data2.partner = [['insert', partnerData]];
+ }
+ }
+ return data2;
+ }
+
+ /**
+ * Performs the `read` RPC on `res.users`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {string[]} param0.fields
+ * @param {integer[]} param0.ids
+ */
+ static async performRpcRead({ context, fields, ids }) {
+ const usersData = await this.env.services.rpc({
+ model: 'res.users',
+ method: 'read',
+ args: [ids],
+ kwargs: {
+ context,
+ fields,
+ },
+ });
+ return this.env.models['mail.user'].insert(usersData.map(userData =>
+ this.env.models['mail.user'].convertData(userData)
+ ));
+ }
+
+ /**
+ * Fetches the partner of this user.
+ */
+ async fetchPartner() {
+ return this.env.models['mail.user'].performRpcRead({
+ ids: [this.id],
+ fields: ['partner_id'],
+ context: { active_test: false },
+ });
+ }
+
+ /**
* @returns {string}
*/
nameOrDisplayName() {
@@ -35,6 +95,94 @@ function factory(dependencies) {
return partner.nameOrDisplayName;
}
+ /**
+ * Gets the chat between this user and the current user.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @returns {mail.thread|undefined}
+ */
+ async getChat() {
+ if (!this.partner) {
+ await this.async(() => this.fetchPartner());
+ }
+ if (!this.partner) {
+ // This user has been deleted from the server or never existed:
+ // - Validity of id is not verified at insert.
+ // - There is no bus notification in case of user delete from
+ // another tab or by another user.
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only chat with existing users."),
+ type: 'warning',
+ });
+ return;
+ }
+ // in other cases a chat would be valid, find it or try to create it
+ let chat = this.env.models['mail.thread'].find(thread =>
+ thread.channel_type === 'chat' &&
+ thread.correspondent === this.partner &&
+ thread.model === 'mail.channel' &&
+ thread.public === 'private'
+ );
+ if (!chat) {
+ chat = await this.async(() =>
+ this.env.models['mail.thread'].performRpcCreateChat({
+ partnerIds: [this.partner.id],
+ })
+ );
+ }
+ if (!chat) {
+ this.env.services['notification'].notify({
+ message: this.env._t("An unexpected error occurred during the creation of the chat."),
+ type: 'warning',
+ });
+ return;
+ }
+ return chat;
+ }
+
+ /**
+ * Opens a chat between this user and the current user and returns it.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @param {Object} [options] forwarded to @see `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(options) {
+ const chat = await this.async(() => this.getChat());
+ if (!chat) {
+ return;
+ }
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the most appropriate view that is a profile for this user.
+ * Because user is a rather technical model to allow login, it's the
+ * partner profile that contains the most useful information.
+ *
+ * @override
+ */
+ async openProfile() {
+ if (!this.partner) {
+ await this.async(() => this.fetchPartner());
+ }
+ if (!this.partner) {
+ // This user has been deleted from the server or never existed:
+ // - Validity of id is not verified at insert.
+ // - There is no bus notification in case of user delete from
+ // another tab or by another user.
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only open the profile of existing users."),
+ type: 'warning',
+ });
+ return;
+ }
+ return this.partner.openProfile();
+ }
+
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
diff --git a/addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js b/addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js
index 9338e1e9673..20297c855f1 100644
--- a/addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js
+++ b/addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js
@@ -36,7 +36,7 @@ QUnit.module('notification_alert_tests.js', {
QUnit.skip('notification_alert widget: display blocked notification alert', async function (assert) {
// FIXME: Test should work, but for some reasons OWL always flags the
// component as not mounted, even though it is in the DOM and it's state
- // is good for rendering...
+ // is good for rendering... task-227947
assert.expect(1);
await this.start({
diff --git a/addons/mail/static/tests/many2one_avatar_user_tests.js b/addons/mail/static/tests/many2one_avatar_user_tests.js
index a94ada54eb8..7010cc9a7e5 100644
--- a/addons/mail/static/tests/many2one_avatar_user_tests.js
+++ b/addons/mail/static/tests/many2one_avatar_user_tests.js
@@ -3,7 +3,6 @@ odoo.define('mail.Many2OneAvatarUserTests', function (require) {
const { afterEach, beforeEach, start } = require('mail/static/src/utils/test_utils.js');
-const FormView = require('web.FormView');
const KanbanView = require('web.KanbanView');
const ListView = require('web.ListView');
const { Many2OneAvatarUser } = require('mail.Many2OneAvatarUser');
@@ -91,45 +90,6 @@ QUnit.module('mail', {}, function () {
list.destroy();
});
- QUnit.test('many2one_avatar_user widget: click on self', async function (assert) {
- assert.expect(4);
-
- const { widget: form } = await start({
- hasView: true,
- View: FormView,
- model: 'foo',
- data: this.data,
- arch: '<form><field name="user_id" widget="many2one_avatar_user"/></form>',
- mockRPC(route, args) {
- if (args.method === 'read') {
- assert.step(`read ${args.model} ${args.args[0]}`);
- }
- return this._super(...arguments);
- },
- session: {
- uid: 23,
- },
- res_id: 4,
- });
-
- mock.intercept(form, 'call_service', (ev) => {
- if (ev.data.service === 'notification') {
- assert.step(`display notification "${ev.data.args[0].message}"`);
- }
- }, true);
-
- assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Yoshi');
-
- await dom.click(form.$('.o_m2o_avatar'));
-
- assert.verifySteps([
- 'read foo 4',
- 'display notification "You cannot chat with yourself"',
- ]);
-
- form.destroy();
- });
-
QUnit.test('many2one_avatar_user widget in kanban view', async function (assert) {
assert.expect(6);
diff --git a/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js b/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js
index 4118b5228ef..9b06fbbaa1c 100644
--- a/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js
+++ b/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js
@@ -12,9 +12,6 @@ registerInstancePatchModel('mail.messaging_initializer', 'mail_bot/static/src/mo
* @private
*/
async _initializeOdooBot() {
- await this.async(() => new Promise(resolve =>
- this.env.browser.setTimeout(resolve, 2 * 60 * 1000)
- ));
const data = await this.async(() => this.env.services.rpc({
model: 'mail.channel',
method: 'init_odoobot',
diff --git a/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer_tests.js b/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer_tests.js
index 4121cc5a277..bfb97067e90 100644
--- a/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer_tests.js
+++ b/addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer_tests.js
@@ -24,10 +24,10 @@ QUnit.module('messaging_initializer_tests.js', {
});
-QUnit.test('OdooBot initialized after 2 minutes', async function (assert) {
+QUnit.test('OdooBot initialized at init', async function (assert) {
// TODO this test should be completed in combination with
// implementing _mockMailChannelInitOdooBot task-2300480
- assert.expect(3);
+ assert.expect(2);
await this.start({
env: {
@@ -35,7 +35,6 @@ QUnit.test('OdooBot initialized after 2 minutes', async function (assert) {
odoobot_initialized: false,
},
},
- hasTimeControl: true,
async mockRPC(route, args) {
if (args.method === 'init_odoobot') {
assert.step('init_odoobot');
@@ -44,16 +43,9 @@ QUnit.test('OdooBot initialized after 2 minutes', async function (assert) {
},
});
- await this.env.testUtils.advanceTime(119 * 1000);
- assert.verifySteps(
- [],
- "should not have initialized OdooBot after 1 minute 59 seconds"
- );
-
- await this.env.testUtils.advanceTime(1 * 1000);
assert.verifySteps(
['init_odoobot'],
- "should have initialized OdooBot after 2 minutes"
+ "should have initialized OdooBot at init"
);
});
diff --git a/addons/sms/static/src/models/notification_group/notification_group.js b/addons/sms/static/src/models/notification_group/notification_group.js
index 151d794b86c..9e7c38d2325 100644
--- a/addons/sms/static/src/models/notification_group/notification_group.js
+++ b/addons/sms/static/src/models/notification_group/notification_group.js
@@ -15,18 +15,18 @@ registerInstancePatchModel('mail.notification_group', 'sms/static/src/models/not
* @override
*/
openCancelAction() {
- if (this.notification_type === 'sms') {
- this.env.bus.trigger('do-action', {
- action: 'sms.sms_cancel_action',
- options: {
- additional_context: {
- default_model: this.res_model,
- unread_counter: this.notifications.length,
- },
- },
- });
+ if (this.notification_type !== 'sms') {
+ return this._super(...arguments);
}
- return this._super(...arguments);
+ this.env.bus.trigger('do-action', {
+ action: 'sms.sms_cancel_action',
+ options: {
+ additional_context: {
+ default_model: this.res_model,
+ unread_counter: this.notifications.length,
+ },
+ },
+ });
},
//--------------------------------------------------------------------------
@@ -37,20 +37,25 @@ registerInstancePatchModel('mail.notification_group', 'sms/static/src/models/not
* @override
*/
_openDocuments() {
- if (this.notification_type === 'sms') {
- this.env.bus.trigger('do-action', {
- action: {
- name: this.env._t("SMS Failures"),
- type: 'ir.actions.act_window',
- view_mode: 'kanban,list,form',
- views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
- target: 'current',
- res_model: this.res_model,
- domain: [['message_has_sms_error', '=', true]],
- },
- });
+ if (this.notification_type !== 'sms') {
+ return this._super(...arguments);
+ }
+ this.env.bus.trigger('do-action', {
+ action: {
+ name: this.env._t("SMS Failures"),
+ type: 'ir.actions.act_window',
+ view_mode: 'kanban,list,form',
+ views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ target: 'current',
+ res_model: this.res_model,
+ domain: [['message_has_sms_error', '=', true]],
+ },
+ });
+ if (this.env.messaging.device.isMobile) {
+ // messaging menu has a higher z-index than views so it must
+ // be closed to ensure the visibility of the view
+ this.messagingMenu.close();
}
- return this._super(...arguments);
},
});
diff --git a/addons/snailmail/static/src/models/notification_group/notification_group.js b/addons/snailmail/static/src/models/notification_group/notification_group.js
index c63ebe4c066..3fff33cbb01 100644
--- a/addons/snailmail/static/src/models/notification_group/notification_group.js
+++ b/addons/snailmail/static/src/models/notification_group/notification_group.js
@@ -15,18 +15,18 @@ registerInstancePatchModel('mail.notification_group', 'snailmail/static/src/mode
* @override
*/
openCancelAction() {
- if (this.notification_type === 'snail') {
- this.env.bus.trigger('do-action', {
- action: 'snailmail.snailmail_letter_cancel_action',
- options: {
- additional_context: {
- default_model: this.res_model,
- unread_counter: this.notifications.length,
- },
- },
- });
+ if (this.notification_type !== 'snail') {
+ return this._super(...arguments);
}
- return this._super(...arguments);
+ this.env.bus.trigger('do-action', {
+ action: 'snailmail.snailmail_letter_cancel_action',
+ options: {
+ additional_context: {
+ default_model: this.res_model,
+ unread_counter: this.notifications.length,
+ },
+ },
+ });
},
//--------------------------------------------------------------------------
@@ -37,20 +37,25 @@ registerInstancePatchModel('mail.notification_group', 'snailmail/static/src/mode
* @override
*/
_openDocuments() {
- if (this.notification_type === 'snail') {
- this.env.bus.trigger('do-action', {
- action: {
- name: this.env._t("Snailmail Failures"),
- type: 'ir.actions.act_window',
- view_mode: 'kanban,list,form',
- views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
- target: 'current',
- res_model: this.res_model,
- domain: [['message_ids.snailmail_error', '=', true]],
- },
- });
+ if (this.notification_type !== 'snail') {
+ return this._super(...arguments);
+ }
+ this.env.bus.trigger('do-action', {
+ action: {
+ name: this.env._t("Snailmail Failures"),
+ type: 'ir.actions.act_window',
+ view_mode: 'kanban,list,form',
+ views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ target: 'current',
+ res_model: this.res_model,
+ domain: [['message_ids.snailmail_error', '=', true]],
+ },
+ });
+ if (this.env.messaging.device.isMobile) {
+ // messaging menu has a higher z-index than views so it must
+ // be closed to ensure the visibility of the view
+ this.env.messaging.messagingMenu.close();
}
- return this._super(...arguments);
},
});