[IMP] im_livechat: go to oldest unread thread with tab

This PR adds the possibility to go the oldest unread livechat
thread when pressing tab on an empty composer.

part of task-3332872

closes odoo/odoo#125922

Signed-off-by: Matthieu Stockbauer (tsm) <tsm@odoo.com>
This commit is contained in:
tsm-odoo
2023-06-26 20:01:04 +02:00
parent 3157b8e50a
commit dc77bee17e
4 changed files with 338 additions and 1 deletions
@@ -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;
})
);
},
});
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="im_livechat.Composer" t-inherit="mail.Composer" t-inherit-mode="extension" owl="1">
<xpath expr="//Typing" position="replace">
<div class="d-flex justify-content-between">
<t>$0</t>
<span t-if="displayNextLivechatHint()" class="text-muted fst-italic form-text">Tab to next livechat</span>
</div>
</xpath>
</t>
</templates>
@@ -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;
},
});
@@ -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)");
});