[IMP] im_livechat: improve feedback messages

This PR improve feedback messages:
- Visitor has left the conversation: as note
- Rating message: as note
- Rating message: Use same emojis as for customer
- Rating reason in same message as the emoji, without writing "reason"
- Feedback done in 1 rpc instead of 3

part of task-3332872

closes odoo/odoo#125925

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
tsm-odoo
2023-06-29 18:20:25 +02:00
parent 5da9a87a82
commit 9beb4668ca
13 changed files with 52 additions and 59 deletions
+1
View File
@@ -75,6 +75,7 @@ Help your customers with this chat, and analyse their feedback.
'im_livechat/static/src/chat_window/**/*',
'im_livechat/static/src/composer/**/*',
'im_livechat/static/src/core/**/*',
'im_livechat/static/src/core_ui/**/*',
'im_livechat/static/src/discuss_app/**/*',
'im_livechat/static/src/messaging_menu/**/*',
'im_livechat/static/src/web/**/*',
+17
View File
@@ -163,6 +163,22 @@ class LivechatController(http.Controller):
persisted=persisted
)
def _post_feedback_message(self, channel, rating, reason):
body = Markup('''
<div class="o_mail_notification o_hide_author">
%(rating)s: <img class="o_livechat_emoji_rating" src="%(rating_url)s" alt="rating"/><br> %(reason)s
</div>
''') % {
'rating': _('Rating'),
'rating_url': rating.rating_image_url,
'reason': reason,
}
channel.message_post(
body=body,
message_type='notification',
subtype_xmlid='mail.mt_comment'
)
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
def feedback(self, uuid, rate, reason=None, **kwargs):
channel = request.env['discuss.channel'].sudo().search([('uuid', '=', uuid)], limit=1)
@@ -189,6 +205,7 @@ class LivechatController(http.Controller):
else:
rating = channel.rating_ids[0]
rating.write(values)
self._post_feedback_message(channel, rating, reason)
return rating.id
return False
+8 -3
View File
@@ -120,7 +120,7 @@ class DiscussChannel(models.Model):
})
def _get_visitor_leave_message(self, operator=False, cancel=False):
return _('Visitor has left the conversation.')
return _('Visitor left the conversation.')
def _close_livechat_session(self, **kwargs):
""" Set deactivate the livechat channel and notify (the operator) the reason of closing the session."""
@@ -131,8 +131,13 @@ class DiscussChannel(models.Model):
if not self.message_ids:
return
# Notify that the visitor has left the conversation
self.message_post(author_id=self.env.ref('base.partner_root').id,
body=self._get_visitor_leave_message(**kwargs), message_type='comment', subtype_xmlid='mail.mt_comment')
self.message_post(
author_id=self.env.ref('base.partner_root').id,
body=Markup('<div class="o_mail_notification o_hide_author">%s</div>')
% self._get_visitor_leave_message(**kwargs),
message_type='notification',
subtype_xmlid='mail.mt_comment'
)
# Rating Mixin
@@ -0,0 +1,3 @@
.o_livechat_emoji_rating {
width: 1.5rem;
}
@@ -5,7 +5,6 @@ import { reactive } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { Deferred } from "@web/core/utils/concurrency";
import { sprintf } from "@web/core/utils/strings";
import { session } from "@web/session";
/**
@@ -21,12 +20,6 @@ export const RATING = Object.freeze({
BAD: 1,
});
export const RATING_TO_EMOJI = {
[RATING.GOOD]: "😊",
[RATING.OK]: "😐",
[RATING.BAD]: "😞",
};
export const SESSION_STATE = Object.freeze({
NONE: "NONE",
CREATED: "CREATED",
@@ -172,17 +165,7 @@ export class LivechatService {
* @param {string} reason
*/
async sendFeedback(uuid, rate, reason) {
await this.rpc("/im_livechat/feedback", { reason, rate, uuid });
await this.rpc("/im_livechat/chat_post", {
uuid,
message_content: sprintf(_t("Rating: %s"), RATING_TO_EMOJI[rate]),
});
if (reason) {
await this.rpc("/im_livechat/chat_post", {
uuid,
message_content: sprintf(_t("Rating reason: %s"), reason),
});
}
return this.rpc("/im_livechat/feedback", { reason, rate, uuid });
}
/**
@@ -2,7 +2,7 @@
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
import { RATING, RATING_TO_EMOJI } from "@im_livechat/embed/core/livechat_service";
import { RATING } from "@im_livechat/embed/core/livechat_service";
import { loadDefaultConfig, start } from "@im_livechat/../tests/embed/helper/test_utils";
import { afterNextRender, click, insertText } from "@mail/../tests/helpers/test_utils";
@@ -47,7 +47,6 @@ QUnit.test("Close without feedback", async (assert) => {
QUnit.test("Feedback with rating and comment", async (assert) => {
await startServer();
await loadDefaultConfig();
let messageCount = 0;
await start({
mockRPC(route, args) {
if (route === "/im_livechat/visitor_leave_session") {
@@ -55,18 +54,8 @@ QUnit.test("Feedback with rating and comment", async (assert) => {
}
if (route === "/im_livechat/feedback") {
assert.step(route);
}
if (route === "/im_livechat/chat_post") {
if (messageCount === 1) {
assert.strictEqual(
args.message_content,
`Rating: ${RATING_TO_EMOJI[RATING.GOOD]}`
);
} else if (messageCount === 2) {
assert.strictEqual(args.message_content, "Rating reason: Good job!");
}
messageCount++;
assert.step(route);
assert.ok(args.reason.includes("Good job!"));
assert.strictEqual(args.rate, RATING.GOOD);
}
},
});
@@ -74,18 +63,13 @@ QUnit.test("Feedback with rating and comment", async (assert) => {
assert.containsOnce($, ".o-mail-ChatWindow");
await insertText(".o-mail-Composer-input", "Hello World!");
await afterNextRender(() => triggerHotkey("Enter"));
assert.verifySteps(["/im_livechat/chat_post"]);
await click("[title='Close Chat Window']");
assert.verifySteps(["/im_livechat/visitor_leave_session"]);
await click(`img[data-alt="${RATING.GOOD}"]`);
await insertText("textarea[placeholder='Explain your note']", "Good job!");
await click("button:contains(Send)");
assert.containsOnce($, "p:contains(Thank you for your feedback)");
assert.verifySteps([
"/im_livechat/feedback",
"/im_livechat/chat_post",
"/im_livechat/chat_post",
]);
assert.verifySteps(["/im_livechat/feedback"]);
});
QUnit.test("Closing folded chat window should open it with feedback", async (assert) => {
@@ -71,6 +71,6 @@ patch(MockServer.prototype, "im_livechat/models/discuss_channel", {
* Simulates `_get_visitor_leave_message` on `discuss.channel`.
*/
_mockDiscussChannel_getVisitorLeaveMessage() {
return "Visitor has left the conversation.";
return "Visitor left the conversation.";
},
});
@@ -33,3 +33,9 @@
.o-mail-NotificationMessage p {
margin-bottom: 0;
}
.o-mail-NotificationMessage:has(.o_hide_author) {
& .o-mail-NotificationMessage-author {
display: none!important;
}
}
@@ -114,11 +114,11 @@
</t>
<t t-name="mail.NotificationMessage" owl="1">
<div class="o-mail-NotificationMessage mx-auto text-500 small px-3 text-center" t-on-click="onClickNotification" t-att-class="{
<div class="o-mail-NotificationMessage text-break mx-auto text-500 small px-3 text-center" t-on-click="onClickNotification" t-att-class="{
'mt-2': prevMsg and !prevMsg.isNotification,
}" t-ref="root">
<i t-if="msg.notificationIcon" t-attf-class="{{ msg.notificationIcon }} me-1"/>
<span t-if="!msg.body.includes(escape(msg.author.name))" class="d-inline mx-1" t-esc="msg.author.name"/> <t t-out="msg.body"/>
<span class="o-mail-NotificationMessage-author d-inline mx-1" t-if="msg.author and !msg.body.includes(escape(msg.author.name))" t-esc="msg.author.name"/> <t t-out="msg.body"/>
</div>
</t>
@@ -60,10 +60,10 @@ class DiscussChannel(models.Model):
def _get_visitor_leave_message(self, operator=False, cancel=False):
name = _('The visitor') if not self.livechat_visitor_id else self.livechat_visitor_id.display_name
if cancel:
message = _("""%s has started a conversation with %s.
message = _("""%s started a conversation with %s.
The chat request has been canceled.""") % (name, operator or _('an operator'))
else:
message = _('%s has left the conversation.', name)
message = _('%s left the conversation.', name)
return message
-6
View File
@@ -91,10 +91,4 @@ class TestLivechatCommon(tests.TransactionCase):
res_model_id = self.env['ir.model'].sudo().search([('model', '=', channel._name)], limit=1).id
rating = self.env['rating.rating'].search([('res_id', '=', channel.id), ('res_model_id', '=', res_model_id)])
self.assertEqual(rating.rating, rating_value, "The rating is not correct.")
message = "Rating: %s" % rating_to_emoji[rating_value]
if reason:
message += " \n%s" % reason
self._send_message(channel, visitor.display_name, message, author_id=False)
self.assertEqual(len(channel.message_ids), channel_messages_count + 1)
@@ -77,19 +77,19 @@ class TestLivechatBasicFlowHttpCase(tests.HttpCase, TestLivechatCommon):
channel._close_livechat_session()
self.assertEqual(len(channel.message_ids), 4)
self.assertEqual(channel.message_ids[0].author_id, self.env.ref('base.partner_root'), "Odoobot must be the sender of the 'has left the conversation' message.")
self.assertEqual(channel.message_ids[0].body, "<p>%s has left the conversation.</p>" % self.visitor.display_name)
self.assertEqual(channel.message_ids[0].author_id, self.env.ref('base.partner_root'), "Odoobot must be the sender of the 'left the conversation' message.")
self.assertIn("%s left the conversation." % self.visitor.display_name, channel.message_ids[0].body)
self.assertEqual(channel.livechat_active, False, "The livechat session must be inactive as the visitor sent his feedback.")
def test_basic_flow_without_rating(self):
channel = self._common_basic_flow()
# has left the conversation
# left the conversation
channel._close_livechat_session()
self.assertEqual(len(channel.message_ids), 3)
self.assertEqual(channel.message_ids[0].author_id, self.env.ref('base.partner_root'), "Odoobot must be the author the message.")
self.assertEqual(channel.message_ids[0].body, "<p>%s has left the conversation.</p>" % self.visitor.display_name)
self.assertEqual(channel.livechat_active, False, "The livechat session must be inactive since visitor has left the conversation.")
self.assertIn("%s left the conversation." % self.visitor.display_name, channel.message_ids[0].body)
self.assertEqual(channel.livechat_active, False, "The livechat session must be inactive since visitor left the conversation.")
def test_visitor_info_access_rights(self):
channel = self._common_basic_flow()
@@ -66,8 +66,8 @@ class TestLivechatRequestHttpCase(tests.HttpCase, TestLivechatCommon):
# Visitor Leave the conversation
channel._close_livechat_session()
self.assertEqual(len(channel.message_ids), 3)
self.assertEqual(channel.message_ids[0].author_id, self.env.ref('base.partner_root'), "Odoobot must be the sender of the 'has left the conversation' message.")
self.assertEqual(channel.message_ids[0].body, "<p>%s has left the conversation.</p>" % self.visitor.display_name)
self.assertEqual(channel.message_ids[0].author_id, self.env.ref('base.partner_root'), "Odoobot must be the sender of the 'left the conversation' message.")
self.assertIn("%s left the conversation." % self.visitor.display_name, channel.message_ids[0].body)
self.assertEqual(channel.livechat_active, False, "The livechat session must be inactive as the visitor sent his feedback.")
return channel