diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/im_livechat/static/src/components | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/im_livechat/static/src/components')
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" + ); +}); + +}); +}); +}); + +}); |
