diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 1524a4dc8d4..7ee18094ce3 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -113,7 +113,7 @@ class LivechatController(http.Controller): ).get_response() @http.route('/im_livechat/get_session', type="json", auth='public', cors="*") - def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, **kwargs): + def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs): user_id = None country_id = None # if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session. @@ -141,7 +141,9 @@ class LivechatController(http.Controller): previous_operator_id=previous_operator_id, chatbot_script=chatbot_script, user_id=user_id, - country_id=country_id) + country_id=country_id, + persisted=persisted + ) @http.route('/im_livechat/feedback', type='json', auth='public', cors="*") def feedback(self, uuid, rate, reason=None, **kwargs): diff --git a/addons/im_livechat/i18n/im_livechat.pot b/addons/im_livechat/i18n/im_livechat.pot index 22466060e1a..17d3fcb924d 100644 --- a/addons/im_livechat/i18n/im_livechat.pot +++ b/addons/im_livechat/i18n/im_livechat.pot @@ -1177,6 +1177,13 @@ msgstr "" msgid "No history found" msgstr "" +#. module: im_livechat +#. odoo-javascript +#: code:addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml:0 +#, python-format +msgid "No operator available" +msgstr "" + #. module: im_livechat #. openerp-web #: code:addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml:0 diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 5ed749c10f5..9e8fa540626 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -161,14 +161,15 @@ class ImLivechatChannel(models.Model): 'name': name, } - def _open_livechat_mail_channel(self, anonymous_name, previous_operator_id=None, chatbot_script=None, user_id=None, country_id=None): - """ Return a mail.channel given a livechat channel. It creates one with a connected operator or with Odoobot as + def _open_livechat_mail_channel(self, anonymous_name, previous_operator_id=None, chatbot_script=None, user_id=None, country_id=None, persisted=True): + """ Return a livechat session. If the session is persisted, creates a mail.channel record with a connected operator or with Odoobot as an operator if a chatbot has been configured, or return false otherwise - :param anonymous_name : the name of the anonymous person of the channel + :param anonymous_name : the name of the anonymous person of the session :param previous_operator_id : partner_id.id of the previous operator that this visitor had in the past :param chatbot_script : chatbot script if there is one configured :param user_id : the id of the logged in visitor, if any - :param country_code : the country of the anonymous person of the channel + :param country_code : the country of the anonymous person of the session + :param persisted: whether or not the session should be persisted :type anonymous_name : str :return : channel header :rtype : dict @@ -192,13 +193,23 @@ class ImLivechatChannel(models.Model): if not user_operator and not chatbot_script: # no one available return False - - # create the session, and add the link with the given channel mail_channel_vals = self._get_livechat_mail_channel_vals(anonymous_name, user_operator, chatbot_script, user_id=user_id, country_id=country_id) - mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create(mail_channel_vals) - if user_operator: - mail_channel._broadcast([user_operator.partner_id.id]) - return mail_channel.sudo().channel_info()[0] + if persisted: + # create the session, and add the link with the given channel + mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create(mail_channel_vals) + if user_operator: + mail_channel._broadcast([user_operator.partner_id.id]) + return mail_channel.sudo().channel_info()[0] + else: + operator_partner_id = user_operator.partner_id if user_operator else chatbot_script.operator_partner_id + display_name = operator_partner_id.user_livechat_username or operator_partner_id.display_name + return { + 'name': mail_channel_vals['name'], + 'chatbot_current_step_id': mail_channel_vals['chatbot_current_step_id'], + 'state': 'open', + 'operator_pid': (operator_partner_id.id, display_name.replace(',', '')), + 'chatbot_script_id': chatbot_script.id if chatbot_script else None + } def _get_random_operator(self): """ Return a random operator from the available users of the channel that have the lowest number of active livechats. diff --git a/addons/im_livechat/static/src/legacy/models/public_livechat.js b/addons/im_livechat/static/src/legacy/models/public_livechat.js index fc2349c413c..aaeb6abc95b 100644 --- a/addons/im_livechat/static/src/legacy/models/public_livechat.js +++ b/addons/im_livechat/static/src/legacy/models/public_livechat.js @@ -289,6 +289,7 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { */ toData() { return { + chatbot_script_id: this.messaging.publicLivechatGlobal.publicLivechat.data.chatbot_script_id, folded: this.messaging.publicLivechatGlobal.publicLivechat.isFolded, id: this.messaging.publicLivechatGlobal.publicLivechat.id, message_unread_counter: this.messaging.publicLivechatGlobal.publicLivechat.unreadCounter, @@ -338,6 +339,11 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { * otherwise */ _notifyMyselfTyping(params) { + if (this.messaging.publicLivechatGlobal.publicLivechat.isTemporary) { + // channel is not created yet, it will be when first message is + // sent. Until then, do not notify visitor is typing. + return; + } return session.rpc('/im_livechat/notify_typing', { uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, is_typing: params.typing, diff --git a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js index 7bb6e80181d..bc0e9df8477 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js @@ -27,7 +27,12 @@ import LivechatButton from '@im_livechat/legacy/widgets/livechat_button'; _prepareGetSessionParameters() { const parameters = this._super(...arguments); - if (this.messaging.publicLivechatGlobal.chatbot.isActive) { + const { publicLivechat } = this.messaging.publicLivechatGlobal; + if (publicLivechat && publicLivechat.isTemporary && !publicLivechat.data.chatbot_script_id) { + return parameters; + } else if (publicLivechat && publicLivechat.data.chatbot_script_id) { + parameters.chatbot_script_id = publicLivechat.data.chatbot_script_id; + } else if (this.messaging.publicLivechatGlobal.chatbot.isActive) { parameters.chatbot_script_id = this.messaging.publicLivechatGlobal.chatbot.scriptId; } diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js index b1bae5d5f27..10a27b45aa9 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js @@ -52,10 +52,6 @@ const PublicLivechatWindow = Widget.extend({ } else { this._focusInput(); } - if (!config.device.isMobile) { - const margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; - this.$el.css(margin_dir, $.position.scrollbarWidth()); - } const def = this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.replace(this.$('.o_thread_window_content')).then(() => { this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.$el.on('scroll', this, this._debouncedOnScroll); }); @@ -115,6 +111,29 @@ const PublicLivechatWindow = Widget.extend({ renderHeader() { this.$header.html(qweb.render('im_livechat.legacy.PublicLivechatWindow.HeaderContent', { widget: this })); }, + + /** + * Render the chat window itself. + */ + renderChatWindow() { + this.renderElement(); + this.adjustPosition(); + }, + + /** + * Compute position of this chat window and apply corresponding styles to + * the underlying widget. + */ + adjustPosition() { + const cssProps = { bottom: 0 }; + cssProps[this.messaging.locale.textDirection === 'rtl' ? 'left' : 'right'] = 0; + if (!config.device.isMobile) { + const margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; + cssProps[margin_dir] = $.position.scrollbarWidth(); + } + this.$el.css(cssProps); + }, + /** * Replace the thread content with provided new content * @@ -138,7 +157,9 @@ const PublicLivechatWindow = Widget.extend({ folded = !this.messaging.publicLivechatGlobal.publicLivechat.isFolded; } this.messaging.publicLivechatGlobal.publicLivechat.update({ isFolded: folded }); - setCookie('im_livechat_session', unaccent(JSON.stringify(this.messaging.publicLivechatGlobal.publicLivechat.widget.toData()), true), 60 * 60, 'required'); + if (this.messaging.publicLivechatGlobal.publicLivechat.operator) { + setCookie('im_livechat_session', unaccent(JSON.stringify(this.messaging.publicLivechatGlobal.publicLivechat.widget.toData()), true), 60 * 60, 'required'); + } this.updateVisualFoldState(); }, /** @@ -198,6 +219,9 @@ const PublicLivechatWindow = Widget.extend({ } catch { await this.messaging.publicLivechatGlobal.livechatButtonView.sendMessage(messageData); // try again just in case } + if (!this.messaging.publicLivechatGlobal.publicLivechat.operator) { + return; + } this.messaging.publicLivechatGlobal.publicLivechat.widget.postMessage(messageData) .then(() => { this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml index 335101276b6..9beadceb47e 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml @@ -16,14 +16,19 @@ -
-
-
- + +
+
+
+ +
+
+
+

No operator available

- + diff --git a/addons/im_livechat/static/src/public_models/chatbot.js b/addons/im_livechat/static/src/public_models/chatbot.js index a20311bc955..55ca07f48e6 100644 --- a/addons/im_livechat/static/src/public_models/chatbot.js +++ b/addons/im_livechat/static/src/public_models/chatbot.js @@ -402,6 +402,13 @@ Model({ * display that restart button. */ compute() { + const { publicLivechat } = this.messaging.publicLivechatGlobal; + if (publicLivechat && !publicLivechat.operator) { + return false; + } + if (publicLivechat && !publicLivechat.data.chatbot_script_id) { + return false; + } return Boolean( !this.currentStep || ( diff --git a/addons/im_livechat/static/src/public_models/livechat_button_view.js b/addons/im_livechat/static/src/public_models/livechat_button_view.js index cfc62190bcf..a7f0551827a 100644 --- a/addons/im_livechat/static/src/public_models/livechat_button_view.js +++ b/addons/im_livechat/static/src/public_models/livechat_button_view.js @@ -2,8 +2,7 @@ import { attr, clear, one, Model } from '@mail/model'; -import {unaccent} from 'web.utils'; -import {getCookie, setCookie, deleteCookie} from 'web.utils.cookies'; +import {getCookie, deleteCookie} from 'web.utils.cookies'; Model({ name: 'LivechatButtonView', @@ -129,9 +128,7 @@ Model({ async openChatWindow() { this.messaging.publicLivechatGlobal.update({ chatWindow: {} }); await this.messaging.publicLivechatGlobal.chatWindow.widget.appendTo($('body')); - const cssProps = { bottom: 0 }; - cssProps[this.messaging.locale.textDirection === 'rtl' ? 'left' : 'right'] = 0; - this.messaging.publicLivechatGlobal.chatWindow.widget.$el.css(cssProps); + this.messaging.publicLivechatGlobal.chatWindow.widget.adjustPosition(); this.widget.$el.hide(); this._openChatWindowChatbot(); }, @@ -139,6 +136,12 @@ Model({ * @param {Object} message */ async sendMessage(message) { + if (this.messaging.publicLivechatGlobal.publicLivechat.isTemporary) { + await this.messaging.publicLivechatGlobal.publicLivechat.createLivechatChannel(); + if (!this.messaging.publicLivechatGlobal.publicLivechat.operator) { + return; + } + } await this._sendMessageChatbotBefore(); await this._sendMessage(message); this._sendMessageChatbotAfter(); @@ -194,7 +197,10 @@ Model({ this.messaging.publicLivechatGlobal.update({ messages: clear() }); def = this.messaging.rpc({ route: '/im_livechat/get_session', - params: this.widget._prepareGetSessionParameters(), + params: { + ...this.widget._prepareGetSessionParameters(), + persisted: false, + }, }, { silent: true }); } def.then((livechatData) => { @@ -223,15 +229,7 @@ Model({ this.widget._sendWelcomeMessage(); } this.messaging.publicLivechatGlobal.chatWindow.renderMessages(); - this.messaging.publicLivechatGlobal.update({ notificationHandler: {} }); - - setCookie('im_livechat_session', unaccent(JSON.stringify(this.messaging.publicLivechatGlobal.publicLivechat.widget.toData()), true), 60 * 60, 'required'); - setCookie('im_livechat_auto_popup', JSON.stringify(false), 60 * 60, 'optional'); - if (this.messaging.publicLivechatGlobal.publicLivechat.operator) { - const operatorPidId = this.messaging.publicLivechatGlobal.publicLivechat.operator.id; - const oneWeek = 7 * 24 * 60 * 60; - setCookie('im_livechat_previous_operator_pid', operatorPidId, oneWeek, 'optional'); - } + this.messaging.publicLivechatGlobal.publicLivechat.updateSessionCookie(); }); } }).then(() => { diff --git a/addons/im_livechat/static/src/public_models/public_livechat.js b/addons/im_livechat/static/src/public_models/public_livechat.js index bdb4578be97..55aa3aa0080 100644 --- a/addons/im_livechat/static/src/public_models/public_livechat.js +++ b/addons/im_livechat/static/src/public_models/public_livechat.js @@ -4,6 +4,9 @@ import PublicLivechat from '@im_livechat/legacy/models/public_livechat'; import { attr, clear, one, Model } from '@mail/model'; +import { unaccent } from 'web.utils'; +import { deleteCookie, setCookie } from 'web.utils.cookies'; + Model({ name: 'PublicLivechat', lifecycleHooks: { @@ -19,6 +22,38 @@ Model({ this.widget.destroy(); }, }, + recordMethods: { + async createLivechatChannel() { + const livechatData = await this.messaging.rpc({ + route: "/im_livechat/get_session", + params: this.messaging.publicLivechatGlobal.livechatButtonView.widget._prepareGetSessionParameters(), + }); + if (!livechatData || !livechatData.operator_pid) { + this.update({ data: clear() }); + deleteCookie("im_livechat_session"); + this.messaging.publicLivechatGlobal.chatWindow.widget.renderChatWindow(); + } else { + this.update({ data: livechatData }); + this.widget.data = livechatData; + this.updateSessionCookie(); + } + }, + updateSessionCookie() { + deleteCookie("im_livechat_session"); + setCookie( + "im_livechat_session", + unaccent(JSON.stringify(this.widget.toData()), true), + 60 * 60, + "required" + ); + setCookie("im_livechat_auto_popup", JSON.stringify(false), 60 * 60, "optional"); + if (this.operator) { + const operatorPidId = this.operator.id; + const oneWeek = 7 * 24 * 60 * 60; + setCookie("im_livechat_previous_operator_pid", operatorPidId, oneWeek, "optional"); + } + }, + }, fields: { data: attr(), id: attr({ @@ -32,13 +67,21 @@ Model({ isFolded: attr({ default: false, }), + isTemporary: attr({ + compute() { + if (!this.data || !this.data.id) { + return true; + } + return false; + }, + }), publicLivechatGlobalOwner: one('PublicLivechatGlobal', { identifying: true, inverse: 'publicLivechat', }), name: attr({ compute() { - if (!this.data) { + if (!this.data || !this.operator) { return clear(); } return this.data.name; diff --git a/addons/im_livechat/static/src/public_models/public_livechat_global.js b/addons/im_livechat/static/src/public_models/public_livechat_global.js index f1161b27b81..21f3a86c090 100644 --- a/addons/im_livechat/static/src/public_models/public_livechat_global.js +++ b/addons/im_livechat/static/src/public_models/public_livechat_global.js @@ -42,12 +42,13 @@ Model({ await this._willStartChatbot(); }, async _willStart() { - const cookie = getCookie('im_livechat_session'); - if (cookie) { - const channel = JSON.parse(cookie); + const strCookie = getCookie('im_livechat_session'); + const isSessionCookieAvailable = Boolean(strCookie); + const cookie = JSON.parse(strCookie || '{}'); + if (cookie.id) { const history = await this.messaging.rpc({ route: '/mail/chat_history', - params: { uuid: channel.uuid, limit: 100 }, + params: { uuid: cookie.uuid, limit: 100 }, }); history.reverse(); this.update({ history }); @@ -55,6 +56,8 @@ Model({ message.body = Markup(message.body); } this.update({ isAvailableForMe: true }); + } else if (isSessionCookieAvailable) { + this.update({ history: [], isAvailableForMe: true }); } else { const result = await this.messaging.rpc({ route: '/im_livechat/init', @@ -185,6 +188,9 @@ Model({ if (!this.publicLivechat) { return clear(); } + if (!this.publicLivechat.operator) { + return clear(); + } return this.lastMessage.authorId !== this.publicLivechat.operator.id; }, default: false, @@ -219,6 +225,12 @@ Model({ messages: many('PublicLivechatMessage'), notificationHandler: one('PublicLivechatGlobalNotificationHandler', { inverse: 'publicLivechatGlobalOwner', + compute() { + if (this.publicLivechat && !this.publicLivechat.isTemporary) { + return {}; + } + return clear(); + } }), options: attr({ default: {}, diff --git a/addons/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js b/addons/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js new file mode 100644 index 00000000000..0537a739eda --- /dev/null +++ b/addons/im_livechat/static/tests/tours/im_livechat_channel_creation_tour.js @@ -0,0 +1,32 @@ +/** @odoo-module */ + +import tour from "web_tour.tour"; + +const requestChatSteps = [ + { + trigger: ".o_livechat_button", + run: "click", + }, + { + trigger: ".o_thread_window", + }, +]; + +tour.register("im_livechat_request_chat", { test: true }, requestChatSteps); + +tour.register("im_livechat_request_chat_and_send_message", { test: true }, [ + ...requestChatSteps, + { + trigger: ".o_composer_text_field", + run: "text Hello, I need help please !", + }, + { + trigger: '.o_composer_text_field', + run() { + $(".o_composer_text_field").trigger($.Event("keydown", { which: 13 })); + }, + }, + { + trigger: ".o_thread_message:contains('Hello, I need help')", + }, +]); diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py index d554e5e2a42..8678526e6b9 100644 --- a/addons/website_livechat/controllers/main.py +++ b/addons/website_livechat/controllers/main.py @@ -60,12 +60,12 @@ class WebsiteLivechat(LivechatController): return request.render("website_livechat.channel_page", values) @http.route('/im_livechat/get_session', type="json", auth='public', cors="*") - def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, **kwargs): + def get_session(self, channel_id, anonymous_name, previous_operator_id=None, chatbot_script_id=None, persisted=True, **kwargs): """ Override to use visitor name instead of 'Visitor' whenever a visitor start a livechat session. """ visitor_sudo = request.env['website.visitor']._get_visitor_from_request() if visitor_sudo: anonymous_name = visitor_sudo.with_context(lang=visitor_sudo.lang_id.code).display_name - return super(WebsiteLivechat, self).get_session(channel_id, anonymous_name, previous_operator_id=previous_operator_id, chatbot_script_id=chatbot_script_id, **kwargs) + return super(WebsiteLivechat, self).get_session(channel_id, anonymous_name, previous_operator_id=previous_operator_id, chatbot_script_id=chatbot_script_id, persisted=persisted, **kwargs) def _livechat_templates_get(self): return super(WebsiteLivechat, self)._livechat_templates_get() + [ diff --git a/addons/website_livechat/tests/test_livechat_basic_flow.py b/addons/website_livechat/tests/test_livechat_basic_flow.py index 5b6182c56c7..35da46e2e36 100644 --- a/addons/website_livechat/tests/test_livechat_basic_flow.py +++ b/addons/website_livechat/tests/test_livechat_basic_flow.py @@ -9,6 +9,14 @@ from odoo.addons.website_livechat.tests.common import TestLivechatCommon @tests.tagged('post_install', '-at_install') class TestLivechatBasicFlowHttpCase(tests.HttpCase, TestLivechatCommon): + def test_channel_created_on_user_interaction(self): + self.start_tour('/', 'im_livechat_request_chat', login=None) + channel = self.env['mail.channel'].search([['livechat_active', '=', True], ['livechat_visitor_id', '=', self.visitor.id]]) + self.assertFalse(channel, 'Channel should not be created until user sends a message') + self.start_tour('/', 'im_livechat_request_chat_and_send_message', login=None) + channel = self.env['mail.channel'].search([['livechat_active', '=', True], ['livechat_visitor_id', '=', self.visitor.id]]) + self.assertTrue(channel, 'Channel should be created after sending the first message') + def test_visitor_banner_history(self): # create visitor history self.env['website.track'].create([{