summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Perais (lpe) <lpe@odoo.com>2020-06-15 11:07:54 +0000
committerAlexandre Kühn <aku@odoo.com>2020-06-26 00:54:54 +0000
commit257371bab52b009c027ead19857fbd865f2bc5b4 (patch)
tree997978f831cd0879b2b410ec3c79ccb156d7e725
parent5a30919826bd4072bf3e0e2915b9df271d58ee78 (diff)
[FIX] mail: discuss: unpin channel fixes
FIX: use the correct API of mail.channel.py to unfollow a channel i.e.: `execute_command_leave` FIX: unpinning a channel has immediate effect on the interface chatWindows are closed, and the inbox is displayed FIX: unpinning a channel destroys corresponding chatWindows across tabs It also unpins the channel in other tabs FIX: python sends is_pinned as part of the channel_info both at init messaging and in the main bus notification That way, we can keep track of the synchronization of the pin status between JS and python task-2275000 task-2267941 closes odoo/odoo#53703 X-original-commit: d4fcc64acb2fc0c6fd9df505380f665abd8b39c4 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
-rw-r--r--addons/im_livechat/static/src/components/discuss/discuss_tests.js1
-rw-r--r--addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js1
-rw-r--r--addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js1
-rw-r--r--addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js6
-rw-r--r--addons/mail/models/mail_channel.py4
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js14
-rw-r--r--addons/mail/static/src/components/composer/composer_tests.js6
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js11
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js274
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js47
-rw-r--r--addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js5
-rw-r--r--addons/mail/static/src/components/notification_list/notification_list_tests.js2
-rw-r--r--addons/mail/static/src/components/thread_icon/thread_icon_tests.js1
-rw-r--r--addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js5
-rw-r--r--addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js4
-rw-r--r--addons/mail/static/src/models/discuss/discuss.js34
-rw-r--r--addons/mail/static/src/models/messaging/messaging.js1
-rw-r--r--addons/mail/static/src/models/messaging_initializer/messaging_initializer.js14
-rw-r--r--addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js10
-rw-r--r--addons/mail/static/src/models/thread/thread.js113
-rw-r--r--addons/mail/static/src/utils/test_utils.js21
-rw-r--r--addons/mail/static/tests/helpers/mock_server.js48
-rw-r--r--addons/mail/views/assets.xml1
23 files changed, 576 insertions, 48 deletions
diff --git a/addons/im_livechat/static/src/components/discuss/discuss_tests.js b/addons/im_livechat/static/src/components/discuss/discuss_tests.js
index dc65fc8d4d7..cbe87437c7a 100644
--- a/addons/im_livechat/static/src/components/discuss/discuss_tests.js
+++ b/addons/im_livechat/static/src/components/discuss/discuss_tests.js
@@ -43,6 +43,7 @@ QUnit.test('livechat in the sidebar', async function (assert) {
channel_livechat: [{
channel_type: "livechat",
id: 1,
+ is_pinned: true,
livechat_visitor: {
country: false,
id: false,
diff --git a/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js b/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js
index 9edd905eabb..c55685ac23e 100644
--- a/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js
+++ b/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js
@@ -55,6 +55,7 @@ QUnit.test('livechat: public website visitor is typing', async function (assert)
channel_livechat: [{
channel_type: 'livechat',
id: 20,
+ is_pinned: true,
livechat_visitor: {
country: false,
id: false,
diff --git a/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js b/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
index 0fdae483eea..e057e156a70 100644
--- a/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
+++ b/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
@@ -59,6 +59,7 @@ QUnit.test('receive visitor typing status "is typing"', async function (assert)
channel_livechat: [{
channel_type: 'livechat',
id: 20,
+ is_pinned: true,
livechat_visitor: {
country: false,
id: false,
diff --git a/addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js b/addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js
index ab9fc72193c..5f356f8146d 100644
--- a/addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js
+++ b/addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js
@@ -17,11 +17,9 @@ registerInstancePatchModel('mail.messaging_initializer', 'im_livechat/static/src
this._super(initMessagingData);
const { channel_livechat = [] } = initMessagingData;
for (const data of channel_livechat) {
- this.env.models['mail.thread'].insert(Object.assign(
- {},
+ this.env.models['mail.thread'].insert(
this.env.models['mail.thread'].convertData(data),
- { isPinned: true }
- ));
+ );
}
},
});
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 23f63f4dad3..a7986c67612 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -281,6 +281,9 @@ class Channel(models.Model):
def _action_unfollow(self, partner):
channel_info = self.channel_info('unsubscribe')[0] # must be computed before leaving the channel (access rights)
result = self.write({'channel_partner_ids': [(3, partner.id)]})
+ # side effect of unsubscribe that wasn't taken into account because
+ # channel_info is called before actually unpinning the channel
+ channel_info['is_pinned'] = False
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner.id), channel_info)
if not self.email_send:
notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
@@ -592,6 +595,7 @@ class Channel(models.Model):
info['is_minimized'] = partner_channel.is_minimized
info['seen_message_id'] = partner_channel.seen_message_id.id
info['custom_channel_name'] = partner_channel.custom_channel_name
+ info['is_pinned'] = partner_channel.is_pinned
# add members infos
partner_ids = channel_partners.mapped('partner_id').ids
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 a4723f65201..1c51a669312 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
@@ -232,6 +232,7 @@ QUnit.test('chat window: basic rendering', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -332,6 +333,7 @@ QUnit.test('chat window: fold', async function (assert) {
channel_type: 'channel',
id: 20,
is_minimized: false,
+ is_pinned: true,
name: "General",
state: 'open',
uuid: 'channel-20-uuid',
@@ -439,6 +441,7 @@ QUnit.test('chat window: open / close', async function (assert) {
channel_type: 'channel',
id: 20,
is_minimized: false,
+ is_pinned: true,
name: "General",
state: 'open',
uuid: 'channel-20-uuid',
@@ -532,6 +535,7 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
channel_type: 'channel',
id: 20,
is_minimized: false,
+ is_pinned: true,
name: "General",
state: 'open',
uuid: 'channel-20-uuid',
@@ -583,6 +587,7 @@ QUnit.test('chat window: state conservation on toggle home menu', async function
channel_type: 'channel',
id: 20,
is_minimized: false,
+ is_pinned: true,
name: "General",
state: 'open',
uuid: 'channel-20-uuid',
@@ -731,6 +736,7 @@ QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]',
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
channel_direct_message: [{
@@ -740,6 +746,7 @@ QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]',
name: "Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -891,6 +898,7 @@ QUnit.test('open 2 chat windows: check shift operations are available', async fu
const channel = {
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
};
const chat = {
@@ -900,6 +908,7 @@ QUnit.test('open 2 chat windows: check shift operations are available', async fu
name: "Demo",
}],
id: 10,
+ is_pinned: true,
};
this.data['mail.channel'].records = [channel, chat];
Object.assign(this.data.initMessaging, {
@@ -1010,14 +1019,17 @@ QUnit.test('open 3 different chat windows: not enough screen width', async funct
channel_channel: [{
channel_type: "channel",
id: 1,
+ is_pinned: true,
name: "channel1",
}, {
channel_type: "channel",
id: 2,
+ is_pinned: true,
name: "channel2",
}, {
channel_type: "channel",
id: 3,
+ is_pinned: true,
name: "channel3",
}],
},
@@ -1172,10 +1184,12 @@ QUnit.test('chat window: switch on TAB', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 1,
+ is_pinned: true,
name: "channel1",
}, {
channel_type: "channel",
id: 2,
+ is_pinned: true,
name: "channel2",
}],
},
diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js
index c62242360a6..24bba449019 100644
--- a/addons/mail/static/src/components/composer/composer_tests.js
+++ b/addons/mail/static/src/components/composer/composer_tests.js
@@ -612,6 +612,7 @@ QUnit.test('composer text input cleared on message post', async function (assert
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -660,6 +661,7 @@ QUnit.test('composer with thread typing notification status', async function (as
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -691,6 +693,7 @@ QUnit.test('current partner notify is typing to other thread members', async fun
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -746,6 +749,7 @@ QUnit.test('current partner is typing should not translate on textual typing sta
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -809,6 +813,7 @@ QUnit.test('current partner notify no longer is typing to thread members after 5
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -871,6 +876,7 @@ QUnit.test('current partner notify is typing again to other members every 50s of
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js
index 40a5af69337..adda97827b5 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js
@@ -43,6 +43,7 @@ QUnit.test('moderated channel with pending moderation message', async function (
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -323,6 +324,7 @@ QUnit.test('accept pending moderation message', async function (assert) {
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -442,6 +444,7 @@ QUnit.test('reject pending moderation message (reject with explanation)', async
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -619,6 +622,7 @@ QUnit.test('discard pending moderation message (reject without explanation)', as
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -749,6 +753,7 @@ QUnit.test('send message in moderated channel', async function (assert) {
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -834,6 +839,7 @@ QUnit.test('sent message accepted in moderated channel', async function (assert)
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -921,6 +927,7 @@ QUnit.test('sent message rejected in moderated channel', async function (assert)
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -994,6 +1001,7 @@ QUnit.test('as moderator, pending moderation message accessibility', async funct
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -1059,6 +1067,7 @@ QUnit.test('as author, pending moderation message should appear in origin thread
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -1100,6 +1109,7 @@ QUnit.test('new pending moderation message posted by me', async function (assert
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
@@ -1161,6 +1171,7 @@ QUnit.test('new pending moderation message posted by someone else when moderator
channel_slots: {
channel_channel: [{
id: 20,
+ is_pinned: true,
channel_type: "channel",
name: "general",
moderation: true,
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
new file mode 100644
index 00000000000..55bcfd6080e
--- /dev/null
+++ b/addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js
@@ -0,0 +1,274 @@
+odoo.define('mail/static/src/components/discuss/tests/discuss_pinned_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach: utilsAfterEach,
+ afterNextRender,
+ beforeEach: utilsBeforeEach,
+ start: utilsStart,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_pinned_tests.js', {
+ beforeEach() {
+ utilsBeforeEach(this);
+
+ this.start = async params => {
+ let { env, widget } = await utilsStart(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ if (this.widget) {
+ this.widget.destroy();
+ }
+ utilsAfterEach(this);
+ },
+});
+
+QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) {
+ assert.expect(2);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ is_pinned: true,
+ name: "General",
+ }],
+ },
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 && thread.model === 'mail.channel'
+ ).localId
+ }"]`,
+ "1 channel is present in discuss sidebar and it is 'general'"
+ );
+});
+
+QUnit.test('sidebar: pinned channel 2: open pinned channel', async function (assert) {
+ assert.expect(1);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ is_pinned: true,
+ name: "General",
+ }],
+ },
+ });
+ await this.start();
+
+ const threadGeneral = this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 && thread.model === 'mail.channel'
+ );
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel #General is displayed in discuss"
+ );
+});
+
+QUnit.test('sidebar: pinned channel 3: open pinned channel and unpin it', async function (assert) {
+ assert.expect(8);
+
+ const channel = {
+ channel_type: "channel",
+ id: 20,
+ is_minimized: false,
+ is_pinned: true,
+ name: "General",
+ uuid: 'general',
+ state: 'closed',
+ };
+
+ this.data['mail.channel'].records = [channel];
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [channel],
+ },
+ });
+
+ const self = this;
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'execute_command') {
+ assert.step('execute_command');
+ assert.deepEqual(args.args[0], [20],
+ "The right id is sent to the server to remove"
+ );
+ assert.strictEqual(args.args[1], 'leave',
+ "The right command is sent to the server"
+ );
+ }
+ if (args.method === 'channel_fold') {
+ assert.step('channel_fold');
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ const threadGeneral = this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 && thread.model === 'mail.channel'
+ );
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.verifySteps([], "neither channel_fold nor execute_command are called yet");
+ await afterNextRender(() =>
+ document.querySelector('.o_DiscussSidebarItem_commandLeave').click()
+ );
+ assert.verifySteps(
+ [
+ 'channel_fold',
+ 'execute_command'
+ ],
+ "both channel_fold and execute_command have been called when unpinning a channel"
+ );
+ assert.containsNone(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel must have been removed from discuss sidebar"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+});
+
+QUnit.test('sidebar: unpin channel from bus', async function (assert) {
+ assert.expect(5);
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ id: 20,
+ is_pinned: true,
+ name: "General",
+ }],
+ },
+ });
+
+ await this.start();
+ const threadGeneral = this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 && thread.model === 'mail.channel'
+ );
+
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "1 channel is present in discuss sidebar and it is 'general'"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector(`.o_DiscussSidebarItem[data-thread-local-id="${
+ threadGeneral.localId
+ }"]`).click()
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel #General is opened in discuss"
+ );
+
+ // Simulate receiving a leave channel notification
+ // (e.g. from user interaction from another device or browser tab)
+ await afterNextRender(() => {
+ const notif = [
+ ["dbName", 'res.partner', this.env.messaging.currentPartner.id],
+ {
+ channel_type: 'channel',
+ id: 20,
+ info: 'unsubscribe',
+ name: "General",
+ public: 'public',
+ state: 'open',
+ }
+ ];
+ this.env.services.bus_service.trigger('notification', [notif]);
+ });
+ assert.containsOnce(
+ document.body,
+ `.o_Discuss_thread[data-thread-local-id="${this.env.messaging.inbox.localId}"]`,
+ "The Inbox is opened in discuss"
+ );
+ assert.containsNone(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel must have been removed from discuss sidebar"
+ );
+});
+
+QUnit.test('sidebar: channel group_based_subscription: mandatorily pinned', async function (assert) {
+ assert.expect(2);
+
+ // FIXME: The following is admittedly odd.
+ // Fixing it should entail a deeper reflexion on the group_based_subscription
+ // and is_pinned functionalities, especially in python.
+ // task-2284357
+
+ Object.assign(this.data.initMessaging, {
+ channel_slots: {
+ channel_channel: [{
+ channel_type: "channel",
+ group_based_subscription: true,
+ id: 20,
+ is_pinned: false,
+ name: "General",
+ }],
+ },
+ });
+ await this.start();
+ const threadGeneral = this.env.models['mail.thread'].find(thread =>
+ thread.id === 20 && thread.model === 'mail.channel'
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_DiscussSidebarItem[data-thread-local-id="${threadGeneral.localId}"]`,
+ "The channel #General is in discuss sidebar"
+ );
+ assert.containsNone(
+ document.body,
+ 'o_DiscussSidebarItem_commandLeave',
+ "The group_based_subscription channel is not unpinnable"
+ );
+});
+
+});
+});
+});
+
+});
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 994d75a8935..6b6d4b3d5db 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js
@@ -22,9 +22,6 @@ QUnit.module('discuss_tests.js', {
utilsBeforeEach(this);
this.start = async params => {
- if (this.widget) {
- this.widget.destroy();
- }
let { env, widget } = await utilsStart(Object.assign({}, params, {
autoOpenDiscuss: true,
data: this.data,
@@ -429,6 +426,7 @@ QUnit.test('sidebar: basic channel rendering', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -525,6 +523,7 @@ QUnit.test('sidebar: channel rendering with needaction counter', async function
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
message_needaction_counter: 10,
}],
@@ -567,12 +566,14 @@ QUnit.test('sidebar: public/private channel rendering', async function (assert)
channel_channel: [{
channel_type: "channel",
id: 100,
+ is_pinned: true,
name: "channel1",
public: 'public',
}],
channel_private_group: [{
channel_type: "channel",
id: 101,
+ is_pinned: true,
name: "channel2",
public: 'private',
}],
@@ -652,6 +653,7 @@ QUnit.test('sidebar: basic chat rendering', async function (assert) {
name: "Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -729,6 +731,7 @@ QUnit.test('sidebar: chat rendering with unread counter', async function (assert
name: "Demo",
}],
id: 10,
+ is_pinned: true,
message_unread_counter: 100,
}],
},
@@ -775,6 +778,7 @@ QUnit.test('sidebar: chat im_status rendering', async function (assert) {
name: "Partner1",
}],
id: 11,
+ is_pinned: true,
}, {
channel_type: "chat",
direct_partner: [{
@@ -783,6 +787,7 @@ QUnit.test('sidebar: chat im_status rendering', async function (assert) {
name: "Partner2",
}],
id: 12,
+ is_pinned: true,
}, {
channel_type: "chat",
direct_partner: [{
@@ -791,6 +796,7 @@ QUnit.test('sidebar: chat im_status rendering', async function (assert) {
name: "Partner3",
}],
id: 13,
+ is_pinned: true,
}],
},
});
@@ -896,6 +902,7 @@ QUnit.test('sidebar: chat custom name', async function (assert) {
name: "Marc Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -921,6 +928,7 @@ QUnit.test('sidebar: rename chat', async function (assert) {
name: "Marc Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -990,6 +998,7 @@ QUnit.test('default thread rendering', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1227,6 +1236,7 @@ QUnit.test('load single message from channel initially', async function (assert)
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1310,6 +1320,7 @@ QUnit.test('basic rendering of message', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1423,6 +1434,7 @@ QUnit.test('basic rendering of squashed message', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1610,6 +1622,7 @@ QUnit.test('load all messages from channel initially, less than fetch limit (29
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1683,6 +1696,7 @@ QUnit.test('load more messages from channel', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1807,6 +1821,7 @@ QUnit.test('auto-scroll to bottom of thread', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1864,6 +1879,7 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -1959,6 +1975,7 @@ QUnit.test('new messages separator', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
message_unread_counter: 0,
name: "General",
seen_message_id: 125,
@@ -2073,10 +2090,12 @@ QUnit.test('restore thread scroll position', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 1,
+ is_pinned: true,
name: "channel1",
}, {
channel_type: "channel",
id: 2,
+ is_pinned: true,
name: "channel2",
}],
},
@@ -2212,10 +2231,12 @@ QUnit.test('message origin redirect to channel', async function (assert) {
channel_channel: [{
channel_type: 'channel',
id: 1,
+ is_pinned: true,
name: "channel1",
}, {
channel_type: 'channel',
id: 2,
+ is_pinned: true,
name: "channel2",
}],
},
@@ -2435,6 +2456,7 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 1,
+ is_pinned: true,
name: "General",
}],
channel_direct_message: [{
@@ -2444,6 +2466,7 @@ QUnit.test('redirect to author (open chat)', async function (assert) {
name: "Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -2593,6 +2616,7 @@ QUnit.test('sidebar quick search', async function (assert) {
channelsData.push({
channel_type: 'channel',
id,
+ is_pinned: true,
name: `channel${id}`,
});
}
@@ -2668,6 +2692,7 @@ QUnit.test('basic control panel rendering', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -2748,6 +2773,7 @@ QUnit.test('inbox: mark all messages as read', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
message_needaction_counter: 2,
name: "General",
}],
@@ -2973,6 +2999,7 @@ QUnit.test('toggle_star message', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -3103,10 +3130,12 @@ QUnit.test('composer state: text save and restore', async function (assert) {
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
name: "General",
}, {
channel_type: 'channel',
id: 21,
+ is_pinned: true,
name: "Special",
}],
},
@@ -3168,10 +3197,12 @@ QUnit.test('composer state: attachments save and restore', async function (asser
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
name: "General",
}, {
channel_type: 'channel',
id: 21,
+ is_pinned: true,
name: "Special",
}],
},
@@ -3278,6 +3309,7 @@ QUnit.test('post a simple message', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -3465,6 +3497,7 @@ QUnit.test('mark channel as seen on last message visible', async function (asser
channel_channel: [{
channel_type: 'channel',
id: 10,
+ is_pinned: true,
message_unread_counter: 1,
name: "General",
}],
@@ -3828,6 +3861,7 @@ QUnit.test('load recent messages from thread (already loaded some old messages)'
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -4245,6 +4279,7 @@ QUnit.test('receive new channel message: out of odoo focus (notification, channe
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
message_unread_counter: 0,
name: "General",
}],
@@ -4298,9 +4333,11 @@ QUnit.test('receive new channel message: out of odoo focus (notification, chat)'
channel_type: "chat",
direct_partner: [{
id: 7,
+ is_pinned: true,
name: "Demo User",
}],
id: 10,
+ is_pinned: true,
message_unread_counter: 0,
}],
},
@@ -4353,6 +4390,7 @@ QUnit.test('receive new channel messages: out of odoo focus (tab title)', async
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
message_unread_counter: 0,
name: "General",
}],
@@ -4363,6 +4401,7 @@ QUnit.test('receive new channel messages: out of odoo focus (tab title)', async
name: "Demo User",
}],
id: 10,
+ is_pinned: true,
message_unread_counter: 0,
}],
},
@@ -4459,6 +4498,7 @@ QUnit.test('auto-focus composer on opening thread', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
message_unread_counter: 0,
name: "General",
}],
@@ -4469,6 +4509,7 @@ QUnit.test('auto-focus composer on opening thread', async function (assert) {
name: "Demo User",
}],
id: 10,
+ is_pinned: true,
message_unread_counter: 0,
}],
},
diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js b/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
index 4f070d7881c..abf13f541f4 100644
--- a/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
+++ b/addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js
@@ -455,6 +455,7 @@ QUnit.test('channel preview: basic rendering', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -565,6 +566,7 @@ QUnit.test('filtered previews', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
channel_direct_message: [{
@@ -574,6 +576,7 @@ QUnit.test('filtered previews', async function (assert) {
name: "Demo",
}],
id: 10,
+ is_pinned: true,
}],
},
});
@@ -760,6 +763,7 @@ QUnit.test('open chat window from preview', async function (assert) {
channel_channel: [{
channel_type: "channel",
id: 20,
+ is_pinned: true,
name: "General",
}],
},
@@ -795,6 +799,7 @@ QUnit.test('no code injection in message body preview', async function (assert)
channel_channel: [{
channel_type: "channel",
id: 1,
+ is_pinned: true,
name: "General",
}],
},
diff --git a/addons/mail/static/src/components/notification_list/notification_list_tests.js b/addons/mail/static/src/components/notification_list/notification_list_tests.js
index cc71ee2d0f2..4dd03f0e014 100644
--- a/addons/mail/static/src/components/notification_list/notification_list_tests.js
+++ b/addons/mail/static/src/components/notification_list/notification_list_tests.js
@@ -36,11 +36,13 @@ QUnit.module('notification_list_tests.js', {
channel_channel: [{
channel_type: 'channel',
id: 100,
+ is_pinned: true,
name: "Channel 2019",
message_unread_counter: 0,
}, {
channel_type: 'channel',
id: 200,
+ is_pinned: true,
name: "Channel 2020",
message_unread_counter: 0,
}],
diff --git a/addons/mail/static/src/components/thread_icon/thread_icon_tests.js b/addons/mail/static/src/components/thread_icon/thread_icon_tests.js
index c21aef4c393..7bd4076a1b4 100644
--- a/addons/mail/static/src/components/thread_icon/thread_icon_tests.js
+++ b/addons/mail/static/src/components/thread_icon/thread_icon_tests.js
@@ -61,6 +61,7 @@ QUnit.test('chat: correspondent is typing', async function (assert) {
name: "Demo",
}],
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
diff --git a/addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js b/addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
index 2d4665e4920..fce0363bc94 100644
--- a/addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
+++ b/addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
@@ -60,6 +60,7 @@ QUnit.test('receive other member typing status "is typing"', async function (ass
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -120,6 +121,7 @@ QUnit.test('receive other member typing status "is typing" then "no longer is ty
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -196,6 +198,7 @@ QUnit.test('assume other member typing status becomes "no longer is typing" afte
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -264,6 +267,7 @@ QUnit.test ('other member typing status "is typing" refreshes 60 seconds timer o
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
@@ -349,6 +353,7 @@ QUnit.test('receive several other members typing status "is typing"', async func
channel_channel: [{
channel_type: 'channel',
id: 20,
+ is_pinned: true,
members: [{
email: 'admin@odoo.com',
id: 3,
diff --git a/addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js b/addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js
index 259e877cb34..6276cf11ca5 100644
--- a/addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js
+++ b/addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js
@@ -323,7 +323,7 @@ QUnit.test('mark channel as fetched when a new message is loaded and as seen whe
// FIXME should not be needed, see task-2277537
composer: [['create']], // avoid initial focus
id: 100,
- isPinned: true, // just to avoid joinChannel to be called
+ isServerPinned: true, // just to avoid joinChannel to be called
members: [['insert', [
{
email: "john@example.com",
@@ -393,7 +393,7 @@ QUnit.test('mark channel as fetched and seen when a new message is loaded if com
});
const thread = this.env.models['mail.thread'].create({
id: 100,
- isPinned: true, // just to avoid joinChannel to be called
+ isServerPinned: true, // just to avoid joinChannel to be called
members: [['insert', [
{
email: "john@example.com",
diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js
index c5d6c3ecf91..77a7f0fca9a 100644
--- a/addons/mail/static/src/models/discuss/discuss.js
+++ b/addons/mail/static/src/models/discuss/discuss.js
@@ -272,6 +272,23 @@ function factory(dependencies) {
return [];
}
+ /**
+ * @private
+ */
+ _onChangeThreadIsPinned() {
+ let thread = this.thread;
+ // No thread, or thread is being removed
+ // so we display discuss the messaging's Inbox.
+ if (
+ (!thread || !thread.isPinned) &&
+ this.messaging
+ ) {
+ thread = this.messaging.inbox;
+ }
+ if (thread && this.threadViewer && thread !== this.thread) {
+ this.threadViewer.update({ thread: [['link', thread]] });
+ }
+ }
}
Discuss.fields = {
@@ -358,6 +375,9 @@ function factory(dependencies) {
default: false,
dependencies: ['replyingToMessage'],
}),
+ isThreadPinned: attr({
+ related: 'thread.isPinned',
+ }),
/**
* The menu_id of discuss app, received on mail/init_messaging and
* used to open discuss from elsewhere.
@@ -365,6 +385,20 @@ function factory(dependencies) {
menu_id: attr({
default: null,
}),
+ messaging: one2one('mail.messaging', {
+ inverse: 'discuss',
+ }),
+ /**
+ * When a thread changes, or some properties of it change
+ * Computes whether we should display it or change it
+ */
+ onChangeThreadIsPinned: attr({
+ compute: '_onChangeThreadIsPinned',
+ dependencies: [
+ 'isThreadPinned',
+ 'thread',
+ ],
+ }),
renamingThreads: one2many('mail.thread'),
replyingToMessage: one2one('mail.message', {
compute: '_computeReplyingToMessage',
diff --git a/addons/mail/static/src/models/messaging/messaging.js b/addons/mail/static/src/models/messaging/messaging.js
index eb81b69d0b9..17e9b1550a4 100644
--- a/addons/mail/static/src/models/messaging/messaging.js
+++ b/addons/mail/static/src/models/messaging/messaging.js
@@ -151,6 +151,7 @@ function factory(dependencies) {
}),
discuss: one2one('mail.discuss', {
autocreate: true,
+ inverse: 'messaging',
isCausal: true,
}),
/**
diff --git a/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js b/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
index 3cee3777d3a..5bcebff0af5 100644
--- a/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
+++ b/addons/mail/static/src/models/messaging_initializer/messaging_initializer.js
@@ -22,19 +22,19 @@ function factory(dependencies) {
this.messaging.update({
history: [['create', {
id: 'history',
- isPinned: true,
+ isServerPinned: true,
model: 'mail.box',
name: this.env._t("History"),
}]],
inbox: [['create', {
id: 'inbox',
- isPinned: true,
+ isServerPinned: true,
model: 'mail.box',
name: this.env._t("Inbox"),
}]],
starred: [['create', {
id: 'starred',
- isPinned: true,
+ isServerPinned: true,
model: 'mail.box',
name: this.env._t("Starred"),
}]],
@@ -144,11 +144,9 @@ function factory(dependencies) {
channel_private_group = [],
} = {}) {
for (const data of channel_channel.concat(channel_direct_message, channel_private_group)) {
- this.env.models['mail.thread'].insert(Object.assign(
- {},
+ this.env.models['mail.thread'].insert(
this.env.models['mail.thread'].convertData(data),
- { isPinned: true }
- ));
+ );
}
}
@@ -192,7 +190,7 @@ function factory(dependencies) {
moderation: [['create', {
counter: moderation_counter,
id: 'moderation',
- isPinned: true,
+ isServerPinned: true,
model: 'mail.box',
name: this.env._t("Moderation"),
}]],
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 0514bde027c..f92521f248e 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
@@ -453,11 +453,7 @@ function factory(dependencies) {
channel.members.includes(this.env.messaging.currentPartner)
);
- channel = this.env.models['mail.thread'].insert(Object.assign(
- { isPinned: true },
- convertedData,
- ));
-
+ channel = this.env.models['mail.thread'].insert(convertedData);
if (
channel.channel_type === 'channel' &&
data.info !== 'creation' &&
@@ -654,12 +650,14 @@ function factory(dependencies) {
owl.utils.escape(channel.name)
);
}
+ // We assume that arriving here the server has effectively
+ // unpinned the channel
+ channel.update({ isServerPinned: false });
this.env.services['notification'].notify({
message,
title: this.env._t("Unsubscribed"),
type: 'warning',
});
- channel.update({ isPinned: false });
}
/**
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index 06c41652ff8..2267d170f10 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -171,6 +171,16 @@ function factory(dependencies) {
if ('is_moderator' in data) {
data2.is_moderator = data.is_moderator;
}
+ if ('is_pinned' in data) {
+ data2.isServerPinned = data.is_pinned;
+ // FIXME: The following is admittedly odd.
+ // Fixing it should entail a deeper reflexion on the group_based_subscription
+ // and is_pinned functionalities, especially in python.
+ // task-2284357
+ if ('group_based_subscription' in data && data.group_based_subscription) {
+ data2.isServerPinned = true;
+ }
+ }
if ('mass_mailing' in data) {
data2.mass_mailing = data.mass_mailing;
}
@@ -298,10 +308,7 @@ function factory(dependencies) {
}),
}
});
- const thread = this.insert(Object.assign(
- { isPinned: true },
- this.convertData(data)
- ));
+ const thread = this.insert(this.convertData(data));
if (autoselect) {
thread.open({ chatWindowMode: autoselectChatWindowMode });
}
@@ -316,7 +323,7 @@ function factory(dependencies) {
* @param {boolean} [param1.autoselect=false]
*/
static async joinChannel(channelId, { autoselect = false } = {}) {
- const channel = this.find(thread =>
+ let channel = this.find(thread =>
thread.id === channelId &&
thread.model === 'mail.channel'
);
@@ -328,12 +335,17 @@ function factory(dependencies) {
method: 'channel_join_and_get_info',
args: [[channelId]]
});
- const thread = this.insert(Object.assign(
- { isPinned: true },
- this.convertData(data)
- ));
+ // 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) {
- thread.open({ resetDiscussDomain: true });
+ channel.open({ resetDiscussDomain: true });
}
}
@@ -483,6 +495,20 @@ 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'.
+ */
+ async notifyUnPinToServer() {
+ return this.async(() => this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'execute_command',
+ args: [[this.id], 'leave']
+ }));
+ }
+
+ /**
* Open provided thread, either in discuss app or as a chat window.
*
* @param {Object} param0
@@ -728,19 +754,11 @@ function factory(dependencies) {
/**
* Unsubscribe current user from provided channel.
*/
- async unsubscribe() {
- if (this.channel_type === 'mail.channel') {
- return this.async(() => this.env.services.rpc({
- model: 'mail.channel',
- method: 'action_unfollow',
- args: [[this.id]]
- }));
- }
- return this.async(() => this.env.services.rpc({
- model: 'mail.channel',
- method: 'channel_pin',
- args: [this.uuid, false]
- }));
+ unsubscribe() {
+ this.update({
+ pendingFoldState: 'closed',
+ isPendingPinned: false,
+ });
}
//----------------------------------------------------------------------
@@ -821,6 +839,14 @@ function factory(dependencies) {
/**
* @private
+ * @returns {boolean}
+ */
+ _computeIsPinned() {
+ return this.isPendingPinned !== undefined ? this.isPendingPinned : this.isServerPinned;
+ }
+
+ /**
+ * @private
* @returns {mail.message}
*/
_computeLastCurrentPartnerMessageSeenByEveryone() {
@@ -1059,6 +1085,17 @@ function factory(dependencies) {
) {
this.update({ pendingFoldState: undefined });
}
+ if (
+ this.isPendingPinned === false &&
+ previous.isPendingPinned !== this.isPendingPinned
+ ) {
+ this.notifyUnPinToServer();
+ }
+ if (
+ this.isServerPinned === this.isPendingPinned
+ ) {
+ this.update({ isPendingPinned: undefined });
+ }
// TODO FIXME prevent to open/close a channel on mobile when you
// open/close it on desktop (task-2267593)
@@ -1090,6 +1127,7 @@ function factory(dependencies) {
_updateBefore() {
return {
foldState: this.foldState,
+ isPendingPinned: this.isPendingPinned,
pendingFoldState: this.pendingFoldState,
};
}
@@ -1222,7 +1260,36 @@ function factory(dependencies) {
'messagingCurrentPartner',
],
}),
+ /**
+ * Determine if there is a pending pin state change, which is a change
+ * of pin state requested by the client but not yet confirmed by the
+ * server.
+ *
+ * This field can be updated to immediately change the pin state on the
+ * interface and to notify the server of the new state.
+ */
+ isPendingPinned: attr(),
+ /**
+ * Boolean that determines whether this thread is pinned
+ * in discuss and present in the messaging menu.
+ */
isPinned: attr({
+ compute: '_computeIsPinned',
+ dependencies: [
+ 'isPendingPinned',
+ 'isServerPinned',
+ ],
+ }),
+ /**
+ * Determine the last pin state known by the server, which is the pin
+ * state displayed after initialization or when the last pending
+ * pin state change was confirmed by the server.
+ *
+ * This field should be considered read only in most situations. Only
+ * the code handling pin state change from the server should typically
+ * update it.
+ */
+ isServerPinned: attr({
default: false,
}),
isTemporary: attr({
diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js
index 5b7cb029cc3..d5eec350e01 100644
--- a/addons/mail/static/src/utils/test_utils.js
+++ b/addons/mail/static/src/utils/test_utils.js
@@ -319,6 +319,16 @@ function beforeEach(self) {
string: "Id",
type: 'integer',
},
+ is_minimized: {
+ // In python this belongs to mail.channel.partner. Here for simplicity.
+ string: "isMinimized",
+ type: "boolean",
+ },
+ is_pinned: {
+ // In python this belongs to mail.channel.partner. Here for simplicity.
+ string: "isPinned",
+ type: "boolean",
+ },
message_unread_counter: {
string: "# unread messages",
type: 'integer',
@@ -328,7 +338,18 @@ function beforeEach(self) {
type: "char",
required: true,
},
+ state: {
+ // In python this belongs to mail.channel.partner. Here for simplicity.
+ string: "FoldState",
+ type: "char",
+ },
+ uuid: {
+ string: "UUID",
+ type: "char",
+ required: true,
+ },
},
+ records: [],
},
'mail.followers': {
fields: {
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index a3f7bb91b63..a87d2853635 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -50,7 +50,48 @@ MockServer.include({
return channel;
});
},
-
+ /**
+ * Simulate the 'channel_fold' route of of mail.channel
+ * In particular sends a notification on the bus
+ *
+ * @private
+ */
+ _mockChannelFold(args) {
+ const { state, uuid } = args.kwargs;
+ const channel = this.data['mail.channel'].records.find(c => c.uuid === uuid);
+ channel.state = state;
+ // Should use something of the form session.partner_id or env.messaging.currentPartner
+ // This is a limitation of the architecture of JS tests
+ const notifConfirmFold = [
+ ["dbName", 'res.partner', undefined],
+ Object.assign({}, channel)
+ ];
+ this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmFold]);
+ },
+ /**
+ * Simulate the 'execute_command' route of mail.channel
+ * In particular sends a notification on the bus
+ *
+ * @private
+ */
+ _mockExecuteCommand(args) {
+ const [ids, commandName] = args.args;
+ const channels = this.data['mail.channel'].records.filter(c => ids.includes(c.id));
+ if (commandName === 'leave') {
+ for (const channel of channels) {
+ channel.is_pinned = false;
+ // Should use something of the form session.partner_id or env.messaging.currentPartner
+ // This is a limitation of the architecture of JS tests
+ const notifConfirmUnpin = [
+ ["dbName", 'res.partner', undefined],
+ Object.assign({}, channel, { info: 'unsubscribe' })
+ ];
+ this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmUnpin]);
+ }
+ return;
+ }
+ throw new Error(`mail/mock_server: the route execute_command doesn't implement the command "${commandName}"`);
+ },
/**
* Simulate the '/mail/read_followers' route
*
@@ -318,7 +359,7 @@ MockServer.include({
return Promise.resolve(this._mockChannelFetchPreview(args));
}
if (args.method === 'channel_fold') {
- return;
+ return this._mockChannelFold(args);
}
if (args.method === 'channel_minimize') {
return Promise.resolve();
@@ -329,6 +370,9 @@ MockServer.include({
if (args.method === 'channel_fetched') {
return Promise.resolve();
}
+ if (args.method === 'execute_command') {
+ return this._mockExecuteCommand(args);
+ }
if (args.method === 'get_activity_data') {
return Promise.resolve(this._mockGetActivityData(args));
}
diff --git a/addons/mail/views/assets.xml b/addons/mail/views/assets.xml
index 6b4623a5416..a13c036cd8f 100644
--- a/addons/mail/views/assets.xml
+++ b/addons/mail/views/assets.xml
@@ -245,6 +245,7 @@
<script type="text/javascript" src="/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/composer/composer_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_moderation_tests.js"></script>
+ <script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_pinned_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/file_uploader/file_uploader_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/follow_button/follow_button_tests.js"></script>