diff --git a/addons/im_livechat/static/src/composer/composer_patch.js b/addons/im_livechat/static/src/composer/composer_patch.js index 4b0b0d90038..49c072e6020 100644 --- a/addons/im_livechat/static/src/composer/composer_patch.js +++ b/addons/im_livechat/static/src/composer/composer_patch.js @@ -8,4 +8,31 @@ patch(Composer.prototype, "im_livechat", { get allowUpload() { return this.thread?.type !== "livechat" && this._super(); }, + + onKeydown(ev) { + this._super(ev); + if ( + ev.key === "Tab" && + this.thread?.type === "livechat" && + !this.props.composer.textInputContent + ) { + const threadChanged = this.threadService.goToOldestUnreadLivechatThread(); + if (threadChanged) { + // prevent chat window from switching to the next thread: as + // we want to go to the oldest unread thread, not the next + // one. + ev.stopPropagation(); + } + } + }, + + displayNextLivechatHint() { + return ( + this.thread?.type === "livechat" && + !this.env.inChatWindow && + Object.values(this.store.discuss.livechat.threads).some((localId) => { + return localId !== this.thread.localId && this.store.threads[localId].isUnread; + }) + ); + }, }); diff --git a/addons/im_livechat/static/src/composer/composer_patch.xml b/addons/im_livechat/static/src/composer/composer_patch.xml new file mode 100644 index 00000000000..5983a475b37 --- /dev/null +++ b/addons/im_livechat/static/src/composer/composer_patch.xml @@ -0,0 +1,11 @@ + + + + +
+ $0 + Tab to next livechat +
+
+
+
diff --git a/addons/im_livechat/static/src/core/thread_service_patch.js b/addons/im_livechat/static/src/core/thread_service_patch.js index 35ec063edab..692db13d900 100644 --- a/addons/im_livechat/static/src/core/thread_service_patch.js +++ b/addons/im_livechat/static/src/core/thread_service_patch.js @@ -63,7 +63,38 @@ patch(ThreadService.prototype, "im_livechat", { this.store.discuss.livechat.threads.sort((localId_1, localId_2) => { const thread1 = this.store.threads[localId_1]; const thread2 = this.store.threads[localId_2]; - return thread2.lastInterestDateTime.ts - thread1.lastInterestDateTime.ts; + return thread2.lastInterestDateTime?.ts - thread1.lastInterestDateTime?.ts; }); }, + + /** + * @returns {boolean} Whether the livechat thread changed. + */ + goToOldestUnreadLivechatThread() { + const oldestUnreadThread = + this.store.threads[ + Object.values(this.store.discuss.livechat.threads) + .filter((localId) => this.store.threads[localId].isUnread) + .sort( + (localId_1, localId_2) => + this.store.threads[localId_1].lastInterestDateTime?.ts - + this.store.threads[localId_2].lastInterestDateTime?.ts + )[0] + ]; + if (!oldestUnreadThread) { + return false; + } + if (this.store.discuss.isActive) { + this.setDiscussThread(oldestUnreadThread); + return true; + } + const chatWindow = this.chatWindowService.insert({ thread: oldestUnreadThread }); + if (chatWindow.hidden) { + this.chatWindowService.makeVisible(chatWindow); + } else if (chatWindow.folded) { + this.chatWindowService.toggleFold(chatWindow); + } + this.chatWindowService.focus(chatWindow); + return true; + }, }); diff --git a/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js b/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js new file mode 100644 index 00000000000..56f21f748da --- /dev/null +++ b/addons/im_livechat/static/tests/go_to_oldest_unread_thread_tests.js @@ -0,0 +1,268 @@ +/* @odoo-module */ + +import { startServer } from "@bus/../tests/helpers/mock_python_environment"; + +import { Command } from "@mail/../tests/helpers/command"; +import { nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; +import { patchUiSize } from "@mail/../tests/helpers/patch_ui_size"; + +import { afterNextRender, insertText, start } from "@mail/../tests/helpers/test_utils"; + +QUnit.module("go to oldest unread livechat"); + +QUnit.test("tab on discuss composer goes to oldest unread livechat", async (assert) => { + const pyEnv = await startServer(); + const channelIds = pyEnv["discuss.channel"].create([ + { + anonymous_name: "Visitor 11", + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 1", + }, + { + anonymous_name: "Visitor 12", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 2", + }, + { + anonymous_name: "Visitor 13", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-01 10:00:00", + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 3", + }, + ]); + pyEnv["mail.message"].create([ + { + author_id: pyEnv.publicPartnerId, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[1], + }, + { + author_id: pyEnv.publicPartnerId, + body: "Hello", + model: "discuss.channel", + res_id: channelIds[2], + }, + ]); + + const { openDiscuss } = await start(); + await openDiscuss(channelIds[0]); + + assert.containsOnce($, ".o-mail-DiscussCategoryItem.o-active:contains(Visitor 11)"); + assert.containsOnce( + $, + ".o-mail-Composer-footer:contains(Tab to next livechat)" + ); + await afterNextRender(() => { + document.querySelector(".o-mail-Composer-input").focus(); + triggerHotkey("Tab"); + }); + assert.containsOnce($, ".o-mail-DiscussCategoryItem.o-active:contains(Visitor 13)"); + await afterNextRender(() => { + document.querySelector(".o-mail-Composer-input").focus(); + triggerHotkey("Tab"); + }); + assert.containsOnce($, ".o-mail-DiscussCategoryItem.o-active:contains(Visitor 12)"); +}); + +QUnit.test("switching to folded chat window unfolds it", async (assert) => { + const pyEnv = await startServer(); + pyEnv["discuss.channel"].create([ + { + anonymous_name: "Visitor 11", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + fold_state: "open", + is_minimized: true, + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 1", + }, + { + anonymous_name: "Visitor 12", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + fold_state: "folded", + is_minimized: true, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 2", + }, + ]); + await start(); + assert.containsOnce( + $, + ".o-mail-ChatWindow.o-folded .o-mail-ChatWindow-header:contains(Visitor 12)" + ); + await afterNextRender(() => { + $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); + triggerHotkey("Tab"); + }); + assert.containsOnce( + $, + ".o-mail-ChatWindow:not(.o-folded) .o-mail-ChatWindow-header:contains(Visitor 12)" + ); + assert.strictEqual( + document.activeElement, + $(".o-mail-ChatWindow:contains(Visitor 12) .o-mail-Composer-input")[0] + ); +}); + +QUnit.test("switching to hidden chat window unhides it", async (assert) => { + const pyEnv = await startServer(); + pyEnv["discuss.channel"].create([ + { + anonymous_name: "Visitor 11", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + is_minimized: true, + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 1", + }, + { + anonymous_name: "Visitor 12", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + is_minimized: true, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 2", + }, + { + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId, is_minimized: true }), + ], + }, + ]); + patchUiSize({ width: 900 }); // enough for 2 chat windows + await start(); + assert.containsNone( + $, + ".o-mail-ChatWindow.o-folded .o-mail-ChatWindow-header:contains(Visitor 12)" + ); + await afterNextRender(() => { + $(".o-mail-ChatWindow:contains(Visitor 11) .o-mail-Composer-input").trigger("focus"); + triggerHotkey("Tab"); + }); + assert.containsOnce( + $, + ".o-mail-ChatWindow:not(.o-folded) .o-mail-ChatWindow-header:contains(Visitor 12)" + ); + assert.strictEqual( + document.activeElement, + $(".o-mail-ChatWindow:contains(Visitor 12) .o-mail-Composer-input")[0] + ); +}); + +QUnit.test("tab on composer doesn't switch thread if user is typing", async (assert) => { + const pyEnv = await startServer(); + const channelIds = pyEnv["discuss.channel"].create([ + { + anonymous_name: "Visitor 11", + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 1", + }, + { + anonymous_name: "Visitor 12", + channel_member_ids: [ + Command.create({ + partner_id: pyEnv.currentPartnerId, + message_unread_counter: 1, + last_interest_dt: "2021-01-02 10:00:00", + }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 2", + }, + ]); + + const { openDiscuss } = await start(); + await openDiscuss(channelIds[0]); + await insertText(".o-mail-Composer-input", "Hello, "); + triggerHotkey("Tab"); + await nextTick(); + assert.containsOnce($, ".o-mail-DiscussCategoryItem.o-active:contains(Visitor 11)"); +}); + +QUnit.test("tab on composer doesn't switch thread if no unread thread", async (assert) => { + const pyEnv = await startServer(); + const channelIds = pyEnv["discuss.channel"].create([ + { + anonymous_name: "Visitor 11", + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 1", + }, + { + anonymous_name: "Visitor 12", + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: pyEnv.publicPartnerId }), + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + name: "Livechat 2", + }, + ]); + + const { openDiscuss } = await start(); + await openDiscuss(channelIds[0]); + document.querySelector(".o-mail-Composer-input").focus(); + triggerHotkey("Tab"); + await nextTick(); + assert.containsOnce($, ".o-mail-DiscussCategoryItem.o-active:contains(Visitor 11)"); +});