[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:
@@ -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/**/*',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user