summaryrefslogtreecommitdiff
path: root/addons/im_livechat/static/src/components
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/im_livechat/static/src/components
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/im_livechat/static/src/components')
-rw-r--r--addons/im_livechat/static/src/components/chat_window_manager/chat_window_manager_tests.js73
-rw-r--r--addons/im_livechat/static/src/components/composer/composer.xml10
-rw-r--r--addons/im_livechat/static/src/components/composer/composer_tests.js68
-rw-r--r--addons/im_livechat/static/src/components/discuss/discuss.js29
-rw-r--r--addons/im_livechat/static/src/components/discuss/discuss_tests.js444
-rw-r--r--addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js78
-rw-r--r--addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml25
-rw-r--r--addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js26
-rw-r--r--addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js100
-rw-r--r--addons/im_livechat/static/src/components/notification_list/notification_list.js36
-rw-r--r--addons/im_livechat/static/src/components/thread_icon/thread_icon.xml19
-rw-r--r--addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js96
-rw-r--r--addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js28
-rw-r--r--addons/im_livechat/static/src/components/thread_preview/thread_preview.js28
-rw-r--r--addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js87
15 files changed, 1147 insertions, 0 deletions
diff --git a/addons/im_livechat/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/im_livechat/static/src/components/chat_window_manager/chat_window_manager_tests.js
new file mode 100644
index 00000000..8d782924
--- /dev/null
+++ b/addons/im_livechat/static/src/components/chat_window_manager/chat_window_manager_tests.js
@@ -0,0 +1,73 @@
+odoo.define('im_livechat/static/src/components/chat_window_manager/chat_window_manager_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('chat_window_manager', {}, function () {
+QUnit.module('chat_window_manager_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { afterEvent, env, widget } = await start(Object.assign(
+ { hasChatWindow: true, hasMessagingMenu: true },
+ params,
+ { data: this.data }
+ ));
+ this.debug = params && params.debug;
+ this.afterEvent = afterEvent;
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('closing a chat window with no message from admin side unpins it', async function (assert) {
+ assert.expect(1);
+
+ this.data['res.partner'].records.push({ id: 10, name: "Demo" });
+ this.data['res.users'].records.push({
+ id: 42,
+ partner_id: 10,
+ });
+ this.data['mail.channel'].records.push(
+ {
+ channel_type: "livechat",
+ id: 10,
+ is_pinned: true,
+ members: [this.data.currentPartnerId, 10],
+ uuid: 'channel-10-uuid',
+ },
+ );
+ await this.start();
+
+ await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
+ await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click());
+ await afterNextRender(() => document.querySelector(`.o_ChatWindowHeader_commandClose`).click());
+ const channels = await this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'read',
+ args: [10],
+ }, { shadow: true });
+ assert.strictEqual(
+ channels[0].is_pinned,
+ false,
+ 'Livechat channel should not be pinned',
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/composer/composer.xml b/addons/im_livechat/static/src/components/composer/composer.xml
new file mode 100644
index 00000000..52195220
--- /dev/null
+++ b/addons/im_livechat/static/src/components/composer/composer.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-inherit="mail.Composer" t-inherit-mode="extension">
+ <xpath expr="//*[hasclass('o_Composer_buttonAttachment')]" position="replace">
+ <t t-if="!composer.thread or composer.thread.channel_type !== 'livechat'">$0</t>
+ </xpath>
+ </t>
+
+</templates>
diff --git a/addons/im_livechat/static/src/components/composer/composer_tests.js b/addons/im_livechat/static/src/components/composer/composer_tests.js
new file mode 100644
index 00000000..b0559b98
--- /dev/null
+++ b/addons/im_livechat/static/src/components/composer/composer_tests.js
@@ -0,0 +1,68 @@
+odoo.define('im_livechat/static/src/components/composer/composer_tests.js', function (require) {
+'use strict';
+
+const components = {
+ Composer: require('mail/static/src/components/composer/composer.js'),
+};
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('composer', {}, function () {
+QUnit.module('composer_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createComposerComponent = async (composer, otherProps) => {
+ const ComposerComponent = components.Composer;
+ ComposerComponent.env = this.env;
+ this.component = new ComposerComponent(null, Object.assign({
+ composerLocalId: composer.localId,
+ }, otherProps));
+ delete ComposerComponent.env;
+ await afterNextRender(() => this.component.mount(this.widget.el));
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('livechat: no add attachment button', async function (assert) {
+ // Attachments are not yet supported in livechat, especially from livechat
+ // visitor PoV. This may likely change in the future with task-2029065.
+ assert.expect(2);
+
+ await this.start();
+ const thread = this.env.models['mail.thread'].create({
+ channel_type: 'livechat',
+ id: 10,
+ model: 'mail.channel',
+ });
+ await this.createComposerComponent(thread.composer);
+ assert.containsOnce(document.body, '.o_Composer', "should have a composer");
+ assert.containsNone(
+ document.body,
+ '.o_Composer_buttonAttachment',
+ "composer linked to livechat should not have a 'Add attachment' button"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/discuss/discuss.js b/addons/im_livechat/static/src/components/discuss/discuss.js
new file mode 100644
index 00000000..5b3c2664
--- /dev/null
+++ b/addons/im_livechat/static/src/components/discuss/discuss.js
@@ -0,0 +1,29 @@
+odoo.define('im_livechat/static/src/components/discuss/discuss.js', function (require) {
+'use strict';
+
+const components = {
+ Discuss: require('mail/static/src/components/discuss/discuss.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.Discuss, 'im_livechat/static/src/components/discuss/discuss.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ mobileNavbarTabs(...args) {
+ return [...this._super(...args), {
+ icon: 'fa fa-comments',
+ id: 'livechat',
+ label: this.env._t("Livechat"),
+ }];
+ }
+
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/discuss/discuss_tests.js b/addons/im_livechat/static/src/components/discuss/discuss_tests.js
new file mode 100644
index 00000000..4cfd207f
--- /dev/null
+++ b/addons/im_livechat/static/src/components/discuss/discuss_tests.js
@@ -0,0 +1,444 @@
+odoo.define('im_livechat/static/src/components/discuss/discuss_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ nextAnimationFrame,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('discuss', {}, function () {
+QUnit.module('discuss_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ autoOpenDiscuss: true,
+ data: this.data,
+ hasDiscuss: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('livechat in the sidebar: basic rendering', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ anonymous_name: "Visitor 11",
+ channel_type: 'livechat',
+ id: 11,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ });
+ await this.start();
+ assert.containsOnce(document.body, '.o_Discuss_sidebar',
+ "should have a sidebar section"
+ );
+ const groupLivechat = document.querySelector('.o_DiscussSidebar_groupLivechat');
+ assert.ok(groupLivechat,
+ "should have a channel group livechat"
+ );
+ const grouptitle = groupLivechat.querySelector('.o_DiscussSidebar_groupTitle');
+ assert.strictEqual(
+ grouptitle.textContent.trim(),
+ "Livechat",
+ "should have a channel group named 'Livechat'"
+ );
+ const livechat = groupLivechat.querySelector(`
+ .o_DiscussSidebarItem[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]
+ `);
+ assert.ok(
+ livechat,
+ "should have a livechat in sidebar"
+ );
+ assert.strictEqual(
+ livechat.textContent,
+ "Visitor 11",
+ "should have 'Visitor 11' as livechat name"
+ );
+});
+
+QUnit.test('livechat in the sidebar: existing user with country', async function (assert) {
+ assert.expect(3);
+
+ this.data['res.country'].records.push({
+ code: 'be',
+ id: 10,
+ name: "Belgium",
+ });
+ this.data['res.partner'].records.push({
+ country_id: 10,
+ id: 10,
+ name: "Jean",
+ });
+ this.data['mail.channel'].records.push({
+ channel_type: 'livechat',
+ id: 11,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, 10],
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should have a channel group livechat in the side bar"
+ );
+ const livechat = document.querySelector('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
+ assert.ok(
+ livechat,
+ "should have a livechat in sidebar"
+ );
+ assert.strictEqual(
+ livechat.textContent,
+ "Jean (Belgium)",
+ "should have user name and country as livechat name"
+ );
+});
+
+QUnit.test('do not add livechat in the sidebar on visitor opening his chat', async function (assert) {
+ assert.expect(2);
+
+ const currentUser = this.data['res.users'].records.find(user =>
+ user.id === this.data.currentUserId
+ );
+ currentUser.im_status = 'online';
+ this.data['im_livechat.channel'].records.push({
+ id: 10,
+ user_ids: [this.data.currentUserId],
+ });
+ await this.start();
+ assert.containsNone(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should not have any livechat in the sidebar initially"
+ );
+
+ // simulate livechat visitor opening his chat
+ await this.env.services.rpc({
+ route: '/im_livechat/get_session',
+ params: {
+ context: {
+ mockedUserId: false,
+ },
+ channel_id: 10,
+ },
+ });
+ await nextAnimationFrame();
+ assert.containsNone(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should still not have any livechat in the sidebar after visitor opened his chat"
+ );
+});
+
+QUnit.test('do not add livechat in the sidebar on visitor typing', async function (assert) {
+ assert.expect(2);
+
+ const currentUser = this.data['res.users'].records.find(user =>
+ user.id === this.data.currentUserId
+ );
+ currentUser.im_status = 'online';
+ this.data['im_livechat.channel'].records.push({
+ id: 10,
+ user_ids: [this.data.currentUserId],
+ });
+ this.data['mail.channel'].records.push({
+ channel_type: 'livechat',
+ id: 10,
+ is_pinned: false,
+ livechat_channel_id: 10,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.publicPartnerId, this.data.currentPartnerId],
+ });
+ await this.start();
+ assert.containsNone(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should not have any livechat in the sidebar initially"
+ );
+
+ // simulate livechat visitor typing
+ const channel = this.data['mail.channel'].records.find(channel => channel.id === 10);
+ await this.env.services.rpc({
+ route: '/im_livechat/notify_typing',
+ params: {
+ context: {
+ mockedPartnerId: this.publicPartnerId,
+ },
+ is_typing: true,
+ uuid: channel.uuid,
+ },
+ });
+ await nextAnimationFrame();
+ assert.containsNone(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should still not have any livechat in the sidebar after visitor started typing"
+ );
+});
+
+QUnit.test('add livechat in the sidebar on visitor sending first message', async function (assert) {
+ assert.expect(4);
+
+ const currentUser = this.data['res.users'].records.find(user =>
+ user.id === this.data.currentUserId
+ );
+ currentUser.im_status = 'online';
+ this.data['res.country'].records.push({
+ code: 'be',
+ id: 10,
+ name: "Belgium",
+ });
+ this.data['im_livechat.channel'].records.push({
+ id: 10,
+ user_ids: [this.data.currentUserId],
+ });
+ this.data['mail.channel'].records.push({
+ anonymous_name: "Visitor (Belgium)",
+ channel_type: 'livechat',
+ country_id: 10,
+ id: 10,
+ is_pinned: false,
+ livechat_channel_id: 10,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.publicPartnerId, this.data.currentPartnerId],
+ });
+ await this.start();
+ assert.containsNone(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should not have any livechat in the sidebar initially"
+ );
+
+ // simulate livechat visitor sending a message
+ const channel = this.data['mail.channel'].records.find(channel => channel.id === 10);
+ await afterNextRender(async () => this.env.services.rpc({
+ route: '/mail/chat_post',
+ params: {
+ context: {
+ mockedUserId: false,
+ },
+ uuid: channel.uuid,
+ message_content: "new message",
+ },
+ }));
+ assert.containsOnce(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should have a channel group livechat in the side bar after receiving first message"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat .o_DiscussSidebar_item',
+ "should have a livechat in the sidebar after receiving first message"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_DiscussSidebar_groupLivechat .o_DiscussSidebar_item').textContent,
+ "Visitor (Belgium)",
+ "should have visitor name and country as livechat name"
+ );
+});
+
+QUnit.test('livechats are sorted by last message date in the sidebar: most recent at the top', async function (assert) {
+ /**
+ * For simplicity the code that is covered in this test is considering
+ * messages to be more/less recent than others based on their ids instead of
+ * their actual creation date.
+ */
+ assert.expect(7);
+
+ this.data['mail.message'].records.push(
+ { id: 11, channel_ids: [11] }, // least recent message due to smaller id
+ { id: 12, channel_ids: [12] }, // most recent message due to higher id
+ );
+ this.data['mail.channel'].records.push(
+ {
+ anonymous_name: "Visitor 11",
+ channel_type: 'livechat',
+ id: 11,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ {
+ anonymous_name: "Visitor 12",
+ channel_type: 'livechat',
+ id: 12,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ );
+ await this.start();
+ const livechat11 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ });
+ const livechat12 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should have a channel group livechat"
+ );
+ const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
+ assert.strictEqual(
+ initialLivechats.length,
+ 2,
+ "should have 2 livechats in the sidebar"
+ );
+ assert.strictEqual(
+ initialLivechats[0].dataset.threadLocalId,
+ livechat12.localId,
+ "first livechat should be the one with the most recent message"
+ );
+ assert.strictEqual(
+ initialLivechats[1].dataset.threadLocalId,
+ livechat11.localId,
+ "second livechat should be the one with the least recent message"
+ );
+
+ // post a new message on the last channel
+ await afterNextRender(() => initialLivechats[1].click());
+ await afterNextRender(() => document.execCommand('insertText', false, "Blabla"));
+ await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
+ const livechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
+ assert.strictEqual(
+ livechats.length,
+ 2,
+ "should still have 2 livechats in the sidebar after posting a new message"
+ );
+ assert.strictEqual(
+ livechats[0].dataset.threadLocalId,
+ livechat11.localId,
+ "first livechat should now be the one on which the new message was posted"
+ );
+ assert.strictEqual(
+ livechats[1].dataset.threadLocalId,
+ livechat12.localId,
+ "second livechat should now be the one on which the message was not posted"
+ );
+});
+
+QUnit.test('livechats with no messages are sorted by creation date in the sidebar: most recent at the top', async function (assert) {
+ /**
+ * For simplicity the code that is covered in this test is considering
+ * channels to be more/less recent than others based on their ids instead of
+ * their actual creation date.
+ */
+ assert.expect(5);
+
+ this.data['mail.message'].records.push(
+ { id: 13, channel_ids: [13] },
+ );
+ this.data['mail.channel'].records.push(
+ {
+ anonymous_name: "Visitor 11",
+ channel_type: 'livechat',
+ id: 11, // least recent channel due to smallest id
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ {
+ anonymous_name: "Visitor 12",
+ channel_type: 'livechat',
+ id: 12, // most recent channel that does not have a message
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ {
+ anonymous_name: "Visitor 13",
+ channel_type: 'livechat',
+ id: 13, // most recent channel (but it has a message)
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ );
+ await this.start();
+ const livechat11 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ });
+ const livechat12 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 12,
+ model: 'mail.channel',
+ });
+ const livechat13 = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 13,
+ model: 'mail.channel',
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_DiscussSidebar_groupLivechat',
+ "should have a channel group livechat"
+ );
+ const initialLivechats = document.querySelectorAll('.o_DiscussSidebar_groupLivechat .o_DiscussSidebarItem');
+ assert.strictEqual(
+ initialLivechats.length,
+ 3,
+ "should have 3 livechats in the sidebar"
+ );
+ assert.strictEqual(
+ initialLivechats[0].dataset.threadLocalId,
+ livechat12.localId,
+ "first livechat should be the most recent channel without message"
+ );
+ assert.strictEqual(
+ initialLivechats[1].dataset.threadLocalId,
+ livechat11.localId,
+ "second livechat should be the second most recent channel without message"
+ );
+ assert.strictEqual(
+ initialLivechats[2].dataset.threadLocalId,
+ livechat13.localId,
+ "third livechat should be the channel with a message"
+ );
+});
+
+QUnit.test('invite button should be present on livechat', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push(
+ {
+ anonymous_name: "Visitor 11",
+ channel_type: 'livechat',
+ id: 11,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ },
+ );
+ await this.start({
+ discuss: {
+ params: {
+ default_active_id: 'mail.channel_11',
+ },
+ },
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_widget_Discuss_controlPanelButtonInvite',
+ "Invite button should be visible in control panel when livechat is active thread"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js
new file mode 100644
index 00000000..35070cb8
--- /dev/null
+++ b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js
@@ -0,0 +1,78 @@
+odoo.define('im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', function (require) {
+'use strict';
+
+const components = {
+ DiscussSidebar: require('mail/static/src/components/discuss_sidebar/discuss_sidebar.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Return the list of livechats that match the quick search value input.
+ *
+ * @returns {mail.thread[]}
+ */
+ quickSearchOrderedAndPinnedLivechatList() {
+ const allOrderedAndPinnedLivechats = this.env.models['mail.thread']
+ .all(thread =>
+ thread.channel_type === 'livechat' &&
+ thread.isPinned &&
+ thread.model === 'mail.channel'
+ ).sort((c1, c2) => {
+ // sort by: last message id (desc), id (desc)
+ if (c1.lastMessage && c2.lastMessage) {
+ return c2.lastMessage.id - c1.lastMessage.id;
+ }
+ // a channel without a last message is assumed to be a new
+ // channel just created with the intent of posting a new
+ // message on it, in which case it should be moved up.
+ if (!c1.lastMessage) {
+ return -1;
+ }
+ if (!c2.lastMessage) {
+ return 1;
+ }
+ return c2.id - c1.id;
+ });
+ if (!this.discuss.sidebarQuickSearchValue) {
+ return allOrderedAndPinnedLivechats;
+ }
+ const qsVal = this.discuss.sidebarQuickSearchValue.toLowerCase();
+ return allOrderedAndPinnedLivechats.filter(livechat => {
+ const nameVal = livechat.displayName.toLowerCase();
+ return nameVal.includes(qsVal);
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _useStoreCompareDepth() {
+ return Object.assign(this._super(...arguments), {
+ allOrderedAndPinnedLivechats: 1,
+ });
+ },
+ /**
+ * Override to include livechat channels on the sidebar.
+ *
+ * @override
+ */
+ _useStoreSelector(props) {
+ return Object.assign(this._super(...arguments), {
+ allOrderedAndPinnedLivechats: this.quickSearchOrderedAndPinnedLivechatList(),
+ });
+ },
+
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml
new file mode 100644
index 00000000..15c6fc35
--- /dev/null
+++ b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-inherit="mail.DiscussSidebar" t-inherit-mode="extension">
+ <xpath expr="//*[@name='root']" position="inside">
+ <t t-set="livechats" t-value="quickSearchOrderedAndPinnedLivechatList()"/>
+ <t t-if="livechats and livechats.length">
+ <div class="o_DiscussSidebar_group o_DiscussSidebar_groupLivechat">
+ <div class="o_DiscussSidebar_groupHeader">
+ <div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle">
+ Livechat
+ </div>
+ </div>
+ <div class="o_DiscussSidebar_list">
+ <t t-foreach="livechats" t-as="livechat" t-key="livechat.localId">
+ <DiscussSidebarItem
+ class="o_DiscussSidebar_item"
+ threadLocalId="livechat.localId"
+ />
+ </t>
+ </div>
+ </div>
+ </t>
+ </xpath>
+ </t>
+</templates>
diff --git a/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
new file mode 100644
index 00000000..b2273d99
--- /dev/null
+++ b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js
@@ -0,0 +1,26 @@
+odoo.define('im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', function (require) {
+'use strict';
+
+const components = {
+ DiscussSidebarItem: require('mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.DiscussSidebarItem, 'im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ hasUnpin(...args) {
+ const res = this._super(...args);
+ return res || this.thread.channel_type === 'livechat';
+ }
+
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js b/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js
new file mode 100644
index 00000000..03c164c5
--- /dev/null
+++ b/addons/im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js
@@ -0,0 +1,100 @@
+odoo.define('im_livechat/static/src/components/messaging_menu/messaging_menu_tests.js', function (require) {
+'use strict';
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('messaging_menu', {}, function () {
+QUnit.module('messaging_menu_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.start = async params => {
+ let { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ hasMessagingMenu: true,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('livechats should be in "chat" filter', async function (assert) {
+ assert.expect(7);
+
+ this.data['mail.channel'].records.push({
+ anonymous_name: "Visitor 11",
+ channel_type: 'livechat',
+ id: 11,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ });
+ await this.start();
+ assert.containsOnce(
+ document.body,
+ '.o_MessagingMenu',
+ "should have messaging menu"
+ );
+
+ await afterNextRender(() => document.querySelector('.o_MessagingMenu_toggler').click());
+ assert.containsOnce(
+ document.body,
+ '.o_MessagingMenu_tabButton[data-tab-id="all"]',
+ "should have a tab/filter 'all' in messaging menu"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_MessagingMenu_tabButton[data-tab-id="chat"]',
+ "should have a tab/filter 'chat' in messaging menu"
+ );
+ assert.hasClass(
+ document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="all"]'),
+ 'o-active',
+ "tab/filter 'all' of messaging menu should be active initially"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_ThreadPreview[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]`,
+ "livechat should be listed in 'all' tab/filter of messaging menu"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]').click()
+ );
+ assert.hasClass(
+ document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]'),
+ 'o-active',
+ "tab/filter 'chat' of messaging menu should become active after click"
+ );
+ assert.containsOnce(
+ document.body,
+ `.o_ThreadPreview[data-thread-local-id="${
+ this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ }).localId
+ }"]`,
+ "livechat should be listed in 'chat' tab/filter of messaging menu"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/notification_list/notification_list.js b/addons/im_livechat/static/src/components/notification_list/notification_list.js
new file mode 100644
index 00000000..c0ab76b2
--- /dev/null
+++ b/addons/im_livechat/static/src/components/notification_list/notification_list.js
@@ -0,0 +1,36 @@
+odoo.define('im_livechat/static/src/components/notification_list/notification_list.js', function (require) {
+'use strict';
+
+const components = {
+ NotificationList: require('mail/static/src/components/notification_list/notification_list.js'),
+};
+
+const { patch } = require('web.utils');
+
+components.NotificationList._allowedFilters.push('livechat');
+
+patch(components.NotificationList, 'im_livechat/static/src/components/notification_list/notification_list.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Override to include livechat channels.
+ *
+ * @override
+ */
+ _useStoreSelectorThreads(props) {
+ if (props.filter === 'livechat') {
+ return this.env.models['mail.thread'].all(thread =>
+ thread.channel_type === 'livechat' &&
+ thread.isPinned &&
+ thread.model === 'mail.channel'
+ );
+ }
+ return this._super(...arguments);
+ },
+
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml b/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml
new file mode 100644
index 00000000..81c93868
--- /dev/null
+++ b/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
+ <xpath expr="//*[@name='rootCondition']" position="inside">
+ <t t-elif="thread.channel_type === 'livechat'">
+ <t t-if="thread.orderedOtherTypingMembers.length > 0">
+ <ThreadTypingIcon
+ class="o_ThreadIcon_typing"
+ animation="'pulse'"
+ title="thread.typingStatusText"
+ />
+ </t>
+ <t t-else="">
+ <div class="fa fa-comments" title="Livechat"/>
+ </t>
+ </t>
+ </xpath>
+ </t>
+</templates>
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
new file mode 100644
index 00000000..f37e873e
--- /dev/null
+++ b/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js
@@ -0,0 +1,96 @@
+odoo.define('im_livechat/static/src/components/thread_icon/thread_icon_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadIcon: require('mail/static/src/components/thread_icon/thread_icon.js'),
+};
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('thread_icon', {}, function () {
+QUnit.module('thread_icon_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createThreadIcon = async thread => {
+ await createRootComponent(this, components.ThreadIcon, {
+ props: { threadLocalId: thread.localId },
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('livechat: public website visitor is typing', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ anonymous_name: "Visitor 20",
+ channel_type: 'livechat',
+ id: 20,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ await this.createThreadIcon(thread);
+ assert.containsOnce(
+ document.body,
+ '.o_ThreadIcon',
+ "should have thread icon"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ThreadIcon .fa.fa-comments',
+ "should have default livechat icon"
+ );
+
+ // simulate receive typing notification from livechat visitor "is typing"
+ await afterNextRender(() => {
+ const typingData = {
+ info: 'typing_status',
+ is_typing: true,
+ partner_id: this.env.messaging.publicPartner.id,
+ partner_name: this.env.messaging.publicPartner.name,
+ };
+ const notification = [[false, 'mail.channel', 20], typingData];
+ this.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ThreadIcon_typing',
+ "should have thread icon with visitor currently typing"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ThreadIcon_typing').title,
+ "Visitor 20 is typing...",
+ "title of icon should tell visitor is currently typing"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js
new file mode 100644
index 00000000..3cd95824
--- /dev/null
+++ b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js
@@ -0,0 +1,28 @@
+odoo.define('im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadNeedactionPreview: require('mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.ThreadNeedactionPreview, 'thread_needaction_preview', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ image(...args) {
+ if (this.thread.channel_type === 'livechat') {
+ return '/mail/static/src/img/smiley/avatar.jpg';
+ }
+ return this._super(...args);
+ }
+
+});
+
+});
diff --git a/addons/im_livechat/static/src/components/thread_preview/thread_preview.js b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js
new file mode 100644
index 00000000..ea752b56
--- /dev/null
+++ b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js
@@ -0,0 +1,28 @@
+odoo.define('im_livechat/static/src/components/thread_preview/thread_preview.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadPreview: require('mail/static/src/components/thread_preview/thread_preview.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.ThreadPreview, 'im_livechat/static/src/components/thread_preview/thread_preview.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ image(...args) {
+ if (this.thread.channel_type === 'livechat') {
+ return '/mail/static/src/img/smiley/avatar.jpg';
+ }
+ return this._super(...args);
+ }
+
+});
+
+});
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
new file mode 100644
index 00000000..6c05cd74
--- /dev/null
+++ b/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js
@@ -0,0 +1,87 @@
+odoo.define('im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadTextualTypingStatus: require('mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js'),
+};
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('im_livechat', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('thread_textual_typing_status', {}, function () {
+QUnit.module('thread_textual_typing_status_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createThreadTextualTypingStatusComponent = async thread => {
+ await createRootComponent(this, components.ThreadTextualTypingStatus, {
+ props: { threadLocalId: thread.localId },
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('receive visitor typing status "is typing"', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({
+ anonymous_name: "Visitor 20",
+ channel_type: 'livechat',
+ id: 20,
+ livechat_operator_id: this.data.currentPartnerId,
+ members: [this.data.currentPartnerId, this.data.publicPartnerId],
+ });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ await this.createThreadTextualTypingStatusComponent(thread);
+
+ assert.strictEqual(
+ document.querySelector('.o_ThreadTextualTypingStatus').textContent,
+ "",
+ "Should display no one is currently typing"
+ );
+
+ // simulate receive typing notification from livechat visitor "is typing"
+ await afterNextRender(() => {
+ const typingData = {
+ info: 'typing_status',
+ is_typing: true,
+ partner_id: this.env.messaging.publicPartner.id,
+ partner_name: this.env.messaging.publicPartner.name,
+ };
+ const notification = [[false, 'mail.channel', 20], typingData];
+ this.widget.call('bus_service', 'trigger', 'notification', [notification]);
+ });
+ assert.strictEqual(
+ document.querySelector('.o_ThreadTextualTypingStatus').textContent,
+ "Visitor 20 is typing...",
+ "Should display that visitor is typing"
+ );
+});
+
+});
+});
+});
+
+});