From 34c88157a6ee98d1207f91e28369a02fb7c2eeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Wed, 17 Aug 2022 12:32:01 +0000 Subject: [PATCH] [IMP] im_livechat, *: Refactor JS Public Livechat (step 14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *: website_livechat This commit is a step towards refactoring the JS of public livechat, so that it reuses the same architecture as the code of Discuss. This implies code that uses JS models and OWL components. Task-2961782 closes odoo/odoo#98798 Signed-off-by: Alexandre Kühn (aku) --- .../legacy/models/public_livechat_message.js | 8 + .../static/src/legacy/public_livechat.scss | 107 +++++++ .../src/legacy/public_livechat_chatbot.js | 129 ++------- .../src/legacy/public_livechat_chatbot.xml | 2 +- .../src/legacy/widgets/livechat_button.js | 49 +--- .../public_livechat_view.xml | 56 ++-- .../public_livechat_window.js | 17 +- .../public_livechat_window.xml | 2 +- .../im_livechat/static/src/models/chatbot.js | 273 +++++++++++++++++- .../static/src/models/livechat_button_view.js | 202 +++++-------- .../models/public_livechat_feedback_view.js | 2 +- .../src/models/public_livechat_global.js | 88 +----- ...ic_livechat_global_notification_handler.js | 2 +- .../src/models/public_livechat_window.js | 4 +- .../website_livechat_chatbot_test_script.js | 4 +- .../static/src/models/chatbot.js | 40 +++ .../static/src/models/livechat_button_view.js | 4 +- .../tours/website_livechat_chatbot_flow.js | 18 +- 18 files changed, 590 insertions(+), 417 deletions(-) create mode 100644 addons/website_livechat/static/src/models/chatbot.js diff --git a/addons/im_livechat/static/src/legacy/models/public_livechat_message.js b/addons/im_livechat/static/src/legacy/models/public_livechat_message.js index 8b489881ac2..40403ee7f20 100644 --- a/addons/im_livechat/static/src/legacy/models/public_livechat_message.js +++ b/addons/im_livechat/static/src/legacy/models/public_livechat_message.js @@ -362,6 +362,14 @@ const PublicLivechatMessage = Class.extend({ return !this._isMyselfAuthor(); }, + isVisitorTheAuthor() { + return !this.hasAuthor() || this._isMyselfAuthor(); + }, + + isOperatorTheAuthor() { + return this.hasAuthor() && !this._isMyselfAuthor(); + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/im_livechat/static/src/legacy/public_livechat.scss b/addons/im_livechat/static/src/legacy/public_livechat.scss index d09343dcda5..af8f95c32e6 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat.scss +++ b/addons/im_livechat/static/src/legacy/public_livechat.scss @@ -616,3 +616,110 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default; .o_livechat_no_rating { opacity: 0.5; } + +.o_PublicLivechatMessage { + display: flex; + position: relative; + + &.o-isVisitorTheAuthor { + flex-direction: row-reverse !important; + } +} + +.o_PublicLivechatMessage_content { + word-break: break-word; + word-wrap: break-word; +} + +.o_PublicLivechatMessage_header { + display: flex; + align-items: center; + + &.o-isVisitorTheAuthor { + justify-content: flex-end !important; + } +} + +.o_PublicLivechatMessage_headerAuthor { + margin-right: map-get($spacers, 2); +} + +.o_PublicLivechatMessage_headerDate { + opacity: 50%; +} + +.o_PublicLivechatMessage_headerDatePrefix { + opacity: 50%; + margin-right: map-get($spacers, 1) / 2; +} + +.o_PublicLivechatMessage_bubbleWrap { + display: flex; + position: relative; + align-items: flex-start; + + &.o-isVisitorTheAuthor { + justify-content: flex-end; + margin-left: map-get($spacers, 4); + } + + &.o-isOperatorTheAuthor { + margin-right: map-get($spacers, 4); + } +} + +.o_PublicLivechatMessage_bubble { + position: relative; + + &.o-isContentNonEmpty { + padding: map-get($spacers, 4) / 2; + } + + &.o-isVisitorTheAuthor { + margin-left: map-get($spacers, 2); + } + + &.o-isOperatorTheAuthor { + margin-right: map-get($spacers, 2); + } + +} + +.o_PublicLivechatMessage_background { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + transition: opacity .5s ease-out; + margin-right: map-get($spacers, 5); + border-bottom-right-radius: 0.6rem !important; + border-bottom-left-radius: 0.6rem !important; + border: 1px solid #C9CCD2 !important; + + &.o-isVisitorTheAuthor { + border-bottom-left-radius: 0.6rem !important; + border-top-left-radius: 0.6rem !important; + opacity: 50%; + border-color: #198754 !important; + background-color: rgba(40, 167, 69, 0.5) !important; + color: #000; + opacity: 25%; + } + + &.o-isOperatorTheAuthor { + border-top-right-radius: 0.6rem !important; + border-bottom-right-radius: 0.6rem !important; + border-color: #17a2b8 !important; + background-color: rgba(23, 162, 184, 0.5) !important; + color: #000; + opacity: 15% + } +} + +.o_PublicLivechatWindow_composer { + padding: map-get($spacers, 2); + outline: none; + border: 0; + border-top: 1px solid lightgray; +} 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 89ca7b8d578..27c66b138ef 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js @@ -19,14 +19,6 @@ const _t = core._t; * message for a couple seconds and then trigger the next step of the script */ LivechatButton.include({ - init() { - this._super(...arguments); - - // debounced to let the user type several sentences, see '_chatbotAwaitUserInput' for details - this._debouncedChatbotAwaitUserInput = _.debounce( - this._chatbotAwaitUserInput.bind(this), - 10000); - }, //-------------------------------------------------------------------------- // Private - Chatbot specifics @@ -42,7 +34,7 @@ const _t = core._t; _chatbotAddMessage(message, options) { message.body = utils.Markup(message.body); this.messaging.publicLivechatGlobal.livechatButtonView.addMessage(message, options); - if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.isAtBottom()) { + if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { this.messaging.publicLivechatGlobal.publicLivechat.update({ unreadCounter: increment() }); } @@ -51,35 +43,6 @@ const _t = core._t; } }, - /** - * This method will be transformed into a 'debounced' version (see init). - * - * The purpose is to handle steps of type 'free_input_multi', that will let the user type in - * multiple lines of text before the bot goes to the next step. - * - * Every time a 'keydown' is detected into the input, or every time a message is sent, we call - * this debounced method, which will give the user about 10 seconds to type more text before - * the next step is triggered. - * - * First we check if the last message was sent by the user, to make sure we always let him type - * at least one message before moving on. - */ - _chatbotAwaitUserInput() { - if (this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { - if (this.messaging.publicLivechatGlobal.chatbot.shouldEndScript) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotEndScript(); - } else { - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSetIsTyping(); - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - chatbotNextStepTimeout: setTimeout( - this._chatbotTriggerNextStep.bind(this), - this.messaging.publicLivechatGlobal.chatbot.messageDelay, - ) - }); - } - } - }, - /** * When the user first interacts with the bot, we want to make sure to actually post the welcome * messages into the conversation. @@ -131,7 +94,7 @@ const _t = core._t; * @private */ _chatbotDisableInput(disableText) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field') + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_composer_text_field') .prop('disabled', true) .addClass('text-center fst-italic bg-200') .val(disableText); @@ -141,7 +104,7 @@ const _t = core._t; * @private */ _chatbotEnableInput() { - const $composerTextField = this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field'); + const $composerTextField = this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_composer_text_field'); $composerTextField .prop('disabled', false) .removeClass('text-center fst-italic bg-200') @@ -186,17 +149,17 @@ const _t = core._t; */ _chatbotProcessStep() { if (this.messaging.publicLivechatGlobal.chatbot.shouldEndScript) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotEndScript(); + this.messaging.publicLivechatGlobal.chatbot.endScript(); } else if (this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'forward_operator' && this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_operator_found) { this._chatbotEnableInput(); } else if (this.messaging.publicLivechatGlobal.chatbot.isExpectingUserInput) { if (this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { // user has already typed a message in -> trigger next step - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSetIsTyping(); - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - chatbotNextStepTimeout: setTimeout( - this._chatbotTriggerNextStep.bind(this), + this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); + this.messaging.publicLivechatGlobal.chatbot.update({ + nextStepTimeout: setTimeout( + this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, this.messaging.publicLivechatGlobal.chatbot.messageDelay, ), }); @@ -215,19 +178,19 @@ const _t = core._t; if (triggerNextStep) { let nextStepDelay = this.messaging.publicLivechatGlobal.chatbot.messageDelay; - if (this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_typing').length !== 0) { + if (this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_typing').length !== 0) { // special case where we already have a "is typing" message displayed // can happen when the previous step did not trigger any message posted from the bot // e.g: previous step was "forward_operator" and no-one is available // -> in that case, don't wait and trigger the next step immediately nextStepDelay = 0; } else { - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSetIsTyping(); + this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); } - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - chatbotNextStepTimeout: setTimeout( - this._chatbotTriggerNextStep.bind(this), + this.messaging.publicLivechatGlobal.chatbot.update({ + nextStepTimeout: setTimeout( + this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, nextStepDelay, ), }); @@ -235,7 +198,7 @@ const _t = core._t; } if (!this.messaging.publicLivechatGlobal.chatbot.hasRestartButton) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); } }, /** @@ -256,7 +219,7 @@ const _t = core._t; if (emailValidResult.success) { this.messaging.publicLivechatGlobal.chatbot.currentStep.data.is_email_valid = true; - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSaveSession(); + this.messaging.publicLivechatGlobal.chatbot.saveSession(); return true; } else { @@ -269,50 +232,6 @@ const _t = core._t; return false; } }, - /** - * Triggers the next step of the script by calling the associated route. - * This will receive the next step and call step processing. - * - * @private - */ - async _chatbotTriggerNextStep() { - let triggerNextStep = true; - if ( - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'question_email' - ) { - triggerNextStep = await this._chatbotValidateEmail(); - } - - if (!triggerNextStep) { - return; - } - - const nextStep = await session.rpc('/chatbot/step/trigger', { - channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, - chatbot_script_id: this.messaging.publicLivechatGlobal.chatbot.scriptId, - }); - - if (nextStep) { - if (nextStep.chatbot_posted_message) { - this._chatbotAddMessage(nextStep.chatbot_posted_message); - } - - this.messaging.publicLivechatGlobal.chatbot.update({ currentStep: { data: nextStep.chatbot_step } }); - - this._chatbotProcessStep(); - } else { - // did not find next step -> end the script - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_is_last = true; - this._renderMessages(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotEndScript(); - } - - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSaveSession(); - - return nextStep; - }, /** * Returns the 'this.messaging.publicLivechatGlobal.messages' filtered on our special 'welcome' ones. * See '_sendWelcomeChatbotMessage'. @@ -350,11 +269,11 @@ const _t = core._t; const self = this; - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_thread_message:last .o_livechat_chatbot_options li').each(function () { + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_thread_message:last .o_livechat_chatbot_options li').each(function () { $(this).on('click', self._onChatbotOptionClicked.bind(self)); }); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').on('click', + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').on('click', this.messaging.publicLivechatGlobal.livechatButtonView.onChatbotRestartScript ); @@ -374,7 +293,7 @@ const _t = core._t; if (this.messaging.publicLivechatGlobal.livechatButtonView.isChatbot) { this._sendWelcomeChatbotMessage( 0, - this.messaging.publicLivechatGlobal.chatbotState === 'welcome' ? 0 : this.messaging.publicLivechatGlobal.chatbot.messageDelay, + this.messaging.publicLivechatGlobal.chatbot.state === 'welcome' ? 0 : this.messaging.publicLivechatGlobal.chatbot.messageDelay, ); } else { this._super(...arguments); @@ -429,11 +348,11 @@ const _t = core._t; if (stepIndex + 1 < this.messaging.publicLivechatGlobal.chatbot.welcomeSteps.length) { if (welcomeMessageDelay !== 0) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSetIsTyping(true); + this.messaging.publicLivechatGlobal.chatbot.setIsTyping(true); } - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - chatbotWelcomeMessageTimeout: setTimeout(() => { + this.messaging.publicLivechatGlobal.chatbot.update({ + welcomeMessageTimeout: setTimeout(() => { this._sendWelcomeChatbotMessage(stepIndex + 1, welcomeMessageDelay); this._renderMessages(); }, welcomeMessageDelay), @@ -463,7 +382,7 @@ const _t = core._t; this.messaging.publicLivechatGlobal.chatbot.currentStep.data && this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'free_input_multi' ) { - this._debouncedChatbotAwaitUserInput(); + this.messaging.publicLivechatGlobal.chatbot.debouncedAwaitUserInput(); } }, @@ -486,7 +405,7 @@ const _t = core._t; const selectedAnswer = $target.data('chatbotStepAnswerId'); const redirectLink = $target.data('chatbotStepRedirectLink'); - this.messaging.publicLivechatGlobal.livechatButtonView.update({ isChatbotRedirecting: !!redirectLink }); + this.messaging.publicLivechatGlobal.chatbot.update({ isRedirecting: !!redirectLink }); await this.messaging.publicLivechatGlobal.livechatButtonView.sendMessage({ content: $target.text().trim(), @@ -508,7 +427,7 @@ const _t = core._t; stepMessage.legacyPublicLivechatMessage.setChatbotStepAnswerId(selectedAnswer); this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_selected_answer_id = selectedAnswer; this._renderMessages(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatbotSaveSession(); + this.messaging.publicLivechatGlobal.chatbot.saveSession(); const saveAnswerPromise = session.rpc('/chatbot/answer/save', { channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, diff --git a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml index b8b04411556..526f5275de6 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml @@ -75,7 +75,7 @@

-
+
is typing diff --git a/addons/im_livechat/static/src/legacy/widgets/livechat_button.js b/addons/im_livechat/static/src/legacy/widgets/livechat_button.js index 6b0d9add7c2..aff9e04d950 100644 --- a/addons/im_livechat/static/src/legacy/widgets/livechat_button.js +++ b/addons/im_livechat/static/src/legacy/widgets/livechat_button.js @@ -1,6 +1,5 @@ /** @odoo-module **/ -import config from 'web.config'; import time from 'web.time'; import utils from 'web.utils'; import Widget from 'web.Widget'; @@ -21,40 +20,8 @@ const LivechatButton = Widget.extend({ this._super(parent); this.messaging = messaging; }, - async willStart() { - this.messaging.publicLivechatGlobal.livechatButtonView.update({ widget: this }); - }, start() { - this.$el.text(this.messaging.publicLivechatGlobal.livechatButtonView.buttonText); - if (this.messaging.publicLivechatGlobal.history) { - for (const m of this.messaging.publicLivechatGlobal.history) { - this.messaging.publicLivechatGlobal.livechatButtonView.addMessage(m); - } - this.messaging.publicLivechatGlobal.livechatButtonView.openChat(); - } else if (!config.device.isMobile && this.messaging.publicLivechatGlobal.rule.action === 'auto_popup') { - const autoPopupCookie = utils.get_cookie('im_livechat_auto_popup'); - if (!autoPopupCookie || JSON.parse(autoPopupCookie)) { - this.messaging.publicLivechatGlobal.livechatButtonView.update({ - autoOpenChatTimeout: setTimeout( - this.messaging.publicLivechatGlobal.livechatButtonView.openChat, - this.messaging.publicLivechatGlobal.rule.auto_popup_timer * 1000, - ), - }); - } - } - if (this.messaging.publicLivechatGlobal.livechatButtonView.buttonBackgroundColor) { - this.$el.css('background-color', this.messaging.publicLivechatGlobal.livechatButtonView.buttonBackgroundColor); - } - if (this.messaging.publicLivechatGlobal.livechatButtonView.buttonTextColor) { - this.$el.css('color', this.messaging.publicLivechatGlobal.livechatButtonView.buttonTextColor); - } - - // If website_event_track installed, put the livechat banner above the PWA banner. - const pwaBannerHeight = $('.o_pwa_install_banner').outerHeight(true); - if (pwaBannerHeight) { - this.$el.css('bottom', pwaBannerHeight + 'px'); - } - + this.messaging.publicLivechatGlobal.livechatButtonView.start(); return this._super(); }, @@ -94,10 +61,10 @@ const LivechatButton = Widget.extend({ * @private */ _renderMessages() { - const shouldScroll = !this.messaging.publicLivechatGlobal.publicLivechat.isFolded && this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.isAtBottom(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.render(); + const shouldScroll = !this.messaging.publicLivechatGlobal.publicLivechat.isFolded && this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.render(); if (shouldScroll) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.scrollToBottom(); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); } }, /** @@ -135,12 +102,12 @@ const LivechatButton = Widget.extend({ */ _onCloseChatWindow(ev) { ev.stopPropagation(); - const isComposerDisabled = this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled'); + const isComposerDisabled = this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled'); const shouldAskFeedback = !isComposerDisabled && this.messaging.publicLivechatGlobal.messages.find(function (message) { return message.id !== '_welcome'; }); if (shouldAskFeedback) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.toggleFold(false); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.toggleFold(false); this.messaging.publicLivechatGlobal.livechatButtonView.askFeedback(); } else { this.messaging.publicLivechatGlobal.livechatButtonView.closeChat(); @@ -176,7 +143,7 @@ const LivechatButton = Widget.extend({ */ _onUpdatedTypingPartners(ev) { ev.stopPropagation(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.renderHeader(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.renderHeader(); }, /** * @private @@ -184,7 +151,7 @@ const LivechatButton = Widget.extend({ */ _onUpdatedUnreadCounter(ev) { ev.stopPropagation(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.renderHeader(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.renderHeader(); }, }); diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml b/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml index aa41ac5d4ee..a4575f00389 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_view/public_livechat_view.xml @@ -136,7 +136,7 @@ @param {integer} [options.selectedMessageID] --> -
+
@@ -163,26 +163,27 @@
-

+

Note by + + + + + + + + + + + + + - - - - - - - - - - - - - - + - on @@ -192,14 +193,19 @@ t-att-data-message-id="message.getID()" title="Mark as Read" role="img" aria-label="Mark as Read"/>

-
- - - -

-
- - +
+
+
+
+ + + +

+
+ + +
+
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 0fba3d653b4..86955091cc3 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 @@ -53,8 +53,8 @@ const PublicLivechatWindow = Widget.extend({ const margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; this.$el.css(margin_dir, $.position.scrollbarWidth()); } - const def = this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.replace(this.$('.o_thread_window_content')).then(() => { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.$el.on('scroll', this, this._debouncedOnScroll); + 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); }); await Promise.all([this._super(), def]); if (this.messaging.publicLivechatGlobal.livechatButtonView.headerBackgroundColor) { @@ -90,7 +90,7 @@ const PublicLivechatWindow = Widget.extend({ */ render() { this.renderHeader(); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.render({ displayLoadMore: false }); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.render({ displayLoadMore: false }); }, /** * Render the header of this thread window. @@ -108,7 +108,7 @@ const PublicLivechatWindow = Widget.extend({ * @param {$.Element} $element */ replaceContentWith($element) { - $element.replace(this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.$el); + $element.replace(this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.$el); }, /** * Toggle the fold state of this thread window. Also update the fold state @@ -135,7 +135,7 @@ const PublicLivechatWindow = Widget.extend({ */ updateVisualFoldState() { if (!this.messaging.publicLivechatGlobal.publicLivechat.isFolded) { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.scrollToBottom(); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); this._focusInput(); } const height = this.messaging.publicLivechatGlobal.publicLivechat.isFolded ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; @@ -183,7 +183,7 @@ const PublicLivechatWindow = Widget.extend({ this.trigger_up('post_message_chat_window', { messageData }); this.messaging.publicLivechatGlobal.publicLivechat.legacyPublicLivechat.postMessage(messageData) .then(() => { - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.scrollToBottom(); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); }); }, @@ -278,12 +278,11 @@ const PublicLivechatWindow = Widget.extend({ if ( !this.messaging.exists() || !this.messaging.publicLivechatGlobal || - !this.messaging.publicLivechatGlobal.livechatButtonView || - !this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow + !this.messaging.publicLivechatGlobal.chatWindow ) { return; } - if (this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.isAtBottom()) { + if (this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { this.messaging.publicLivechatGlobal.publicLivechat.legacyPublicLivechat.markAsRead(); } }, 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 7f60355582b..96fbb31d689 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 @@ -19,7 +19,7 @@
- +
diff --git a/addons/im_livechat/static/src/models/chatbot.js b/addons/im_livechat/static/src/models/chatbot.js index 6148863d999..7d47e6f712a 100644 --- a/addons/im_livechat/static/src/models/chatbot.js +++ b/addons/im_livechat/static/src/models/chatbot.js @@ -4,9 +4,208 @@ import { registerModel } from '@mail/model/model_core'; import { attr, one } from '@mail/model/model_field'; import { clear } from '@mail/model/model_field_command'; +import { qweb } from 'web.core'; + registerModel({ name: 'Chatbot', recordMethods: { + /** + * Once the script ends, adds a visual element at the end of the chat window allowing to restart + * the whole script. + */ + endScript() { + if ( + this.currentStep && + this.currentStep.data && + this.currentStep.data.conversation_closed + ) { + // don't touch anything if the user has closed the conversation, let the chat window + // handle the display + return; + } + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_composer_text_field').addClass('d-none'); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').show(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_restart').one('click', this.messaging.publicLivechatGlobal.livechatButtonView.onChatbotRestartScript); + }, + /** + * See 'Chatbot/saveSession'. + * + * We retrieve the livechat uuid from the session cookie since the livechat Widget is not yet + * initialized when we restore the chatbot state. + * + * We also clear any older keys that store a previously saved chatbot session. + * (In that case we clear the actual browser's local storage, we don't use the localStorage + * object as it does not allow browsing existing keys, see 'local_storage.js'.) + */ + restoreSession() { + const browserLocalStorage = window.localStorage; + if (browserLocalStorage && browserLocalStorage.length) { + for (let i = 0; i < browserLocalStorage.length; i++) { + const key = browserLocalStorage.key(i); + if (key.startsWith('im_livechat.chatbot.state.uuid_') && key !== this.sessionCookieKey) { + browserLocalStorage.removeItem(key); + } + } + } + const chatbotState = localStorage.getItem(this.sessionCookieKey); + if (chatbotState) { + this.update({ currentStep: { data: this.localStorageState._chatbotCurrentStep } }); + } + }, + /** + * Register current chatbot step state into localStorage to be able to resume if the visitor + * goes to another website page or if he refreshes his page. + * + * (Will not work if the visitor switches browser but his livechat session will not be restored + * anyway in that case, since it's stored into a cookie). + */ + saveSession() { + localStorage.setItem('im_livechat.chatbot.state.uuid_' + this.messaging.publicLivechatGlobal.publicLivechat.uuid, JSON.stringify({ + '_chatbot': this.data, + '_chatbotCurrentStep': this.currentStep.data, + })); + }, + /** + * Adds a small "is typing" animation into the chat window. + * + * @param {boolean} [isWelcomeMessage=false] + */ + setIsTyping(isWelcomeMessage = false) { + if (this.messaging.publicLivechatGlobal.livechatButtonView.isTypingTimeout) { + clearTimeout(this.messaging.publicLivechatGlobal.livechatButtonView.isTypingTimeout); + } + this.messaging.publicLivechatGlobal.livechatButtonView.widget._chatbotDisableInput(''); + this.messaging.publicLivechatGlobal.livechatButtonView.update({ + isTypingTimeout: setTimeout( + () => { + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_mail_thread_content').append( + $(qweb.render('im_livechat.legacy.chatbot.is_typing_message', { + 'chatbotImageSrc': `/im_livechat/operator/${ + this.messaging.publicLivechatGlobal.publicLivechat.operator.id + }/avatar`, + 'chatbotName': this.name, + 'isWelcomeMessage': isWelcomeMessage, + })) + ); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); + }, + this.messageDelay / 3, + ), + }); + }, + /** + * Triggers the next step of the script by calling the associated route. + * This will receive the next step and call step processing. + */ + async triggerNextStep() { + let triggerNextStep = true; + if ( + this.currentStep && + this.currentStep.data && + this.currentStep.data.chatbot_step_type === 'question_email' + ) { + triggerNextStep = await this.messaging.publicLivechatGlobal.livechatButtonView.widget._chatbotValidateEmail(); + } + + if (!triggerNextStep) { + return; + } + + const nextStep = await this.messaging.rpc({ + route: '/chatbot/step/trigger', + params: { + channel_uuid: this.messaging.publicLivechatGlobal.publicLivechat.uuid, + chatbot_script_id: this.scriptId, + }, + }); + + if (nextStep) { + if (nextStep.chatbot_posted_message) { + this.messaging.publicLivechatGlobal.livechatButtonView.widget._chatbotAddMessage(nextStep.chatbot_posted_message); + } + + this.update({ currentStep: { data: nextStep.chatbot_step } }); + + this.messaging.publicLivechatGlobal.livechatButtonView.widget._chatbotProcessStep(); + } else { + // did not find next step -> end the script + this.currentStep.data.chatbot_step_is_last = true; + this.messaging.publicLivechatGlobal.livechatButtonView.widget._renderMessages(); + this.endScript(); + } + + this.saveSession(); + + return nextStep; + }, + /** + * This method will be transformed into a 'debounced' version (see init). + * + * The purpose is to handle steps of type 'free_input_multi', that will let the user type in + * multiple lines of text before the bot goes to the next step. + * + * Every time a 'keydown' is detected into the input, or every time a message is sent, we call + * this debounced method, which will give the user about 10 seconds to type more text before + * the next step is triggered. + * + * First we check if the last message was sent by the user, to make sure we always let him type + * at least one message before moving on. + */ + awaitUserInput() { + if (this.messaging.publicLivechatGlobal.isLastMessageFromCustomer) { + if (this.shouldEndScript) { + this.endScript(); + } else { + this.setIsTyping(); + this.update({ + nextStepTimeout: setTimeout( + this.triggerNextStep, + this.messageDelay, + ) + }); + } + } + }, + /** + * @private + * @returns {integer} + */ + _computeAwaitUserInputDebounceTime() { + return 10000; + }, + /** + * @private + * @returns {Object|FieldCommand} + */ + _computeData() { + if (this.messaging.publicLivechatGlobal.isTestChatbot) { + return this.messaging.publicLivechatGlobal.testChatbotData.chatbot; + } + if (this.state === 'init') { + return this.messaging.publicLivechatGlobal.rule.chatbot; + } + if (this.state === 'welcome') { + return this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot; + } + if ( + this.state === 'restore_session' && + this.localStorageState + ) { + return this.localStorageState._chatbot; + } + return clear(); + }, + /** + * @private + * @returns {function} + */ + _computeDebouncedAwaitUserInput() { + // debounced to let the user type several sentences, see 'Chatbot/awaitUserInput' for details + return _.debounce( + this.awaitUserInput, + this.awaitUserInputDebounceTime, + ); + }, /** * @private * @returns {boolean} @@ -22,11 +221,28 @@ registerModel({ 'free_input_multi', ].includes(this.currentStep.data.chatbot_step_type); }, + /** + * @private + * @returns {FieldCommand|Object} + */ + _computeLocalStorageState() { + if (!this.messaging.publicLivechatGlobal.sessionCookie) { + return clear(); + } + const data = localStorage.getItem(this.sessionCookieKey); + if (!data) { + return clear(); + } + return JSON.parse(data); + }, /** * @private * @returns {string} */ _computeName() { + if (!this.data) { + return clear(); + } return this.data.name; }, /** @@ -62,9 +278,6 @@ registerModel({ * @returns {integer|FieldCommand} */ _computeMessageDelay() { - if (this.messaging.publicLivechatGlobal.isWebsiteLivechatChatbotFlow) { - return 100; - } return clear(); }, /** @@ -72,8 +285,21 @@ registerModel({ * @returns {integer} */ _computeScriptId() { + if (!this.data) { + return clear(); + } return this.data.chatbot_script_id; }, + /** + * @private + * @returns {string|FieldCommand} + */ + _computeSessionCookieKey() { + if (!this.messaging.publicLivechatGlobal.sessionCookie) { + return clear(); + } + return 'im_livechat.chatbot.state.uuid_' + JSON.parse(this.messaging.publicLivechatGlobal.sessionCookie).uuid; + }, /** * Helper method that checks if the script should be ended or not. * If the user has closed the conversation -> script has ended. @@ -123,20 +349,44 @@ registerModel({ } return false; }, + /** + * @private + * @returns {string|FieldCommand} + */ + _computeState() { + if (this.messaging.publicLivechatGlobal.rule && !!this.messaging.publicLivechatGlobal.rule.chatbot) { + return 'init'; + } + if (this.messaging.publicLivechatGlobal.livechatInit && this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot) { + return 'welcome'; + } + return clear(); + }, /** * @private * @returns {Array|FieldCommand} */ _computeWelcomeSteps() { + if (!this.data) { + return clear(); + } return this.data.chatbot_welcome_steps; }, }, fields: { - data: attr(), + awaitUserInputDebounceTime: attr({ + compute: '_computeAwaitUserInputDebounceTime', + }), + data: attr({ + compute: '_computeData', + }), currentStep: one('ChatbotStep', { inverse: 'chabotOwner', isCausal: true, }), + debouncedAwaitUserInput: attr({ + compute: '_computeDebouncedAwaitUserInput', + }), hasRestartButton: attr({ compute: '_computeHasRestartButton', default: false, @@ -145,12 +395,19 @@ registerModel({ compute: '_computeIsExpectingUserInput', default: false, }), + isRedirecting: attr({ + default: false, + }), lastWelcomeStep: attr({ compute: '_computeLastWelcomeStep', }), + localStorageState: attr({ + compute: '_computeLocalStorageState', + }), name: attr({ compute: '_computeName', }), + nextStepTimeout: attr(), messageDelay: attr({ compute: '_computeMessageDelay', default: 3500, // in milliseconds @@ -162,10 +419,18 @@ registerModel({ scriptId: attr({ compute: '_computeScriptId', }), + serverUrl: attr(), + sessionCookieKey: attr({ + compute: '_computeSessionCookieKey', + }), shouldEndScript: attr({ compute: '_computeShouldEndScript', default: false, }), + state: attr({ + compute: '_computeState', + }), + welcomeMessageTimeout: attr(), welcomeSteps: attr({ compute: '_computeWelcomeSteps', }), diff --git a/addons/im_livechat/static/src/models/livechat_button_view.js b/addons/im_livechat/static/src/models/livechat_button_view.js index dce7fc82158..caa4e3be019 100644 --- a/addons/im_livechat/static/src/models/livechat_button_view.js +++ b/addons/im_livechat/static/src/models/livechat_button_view.js @@ -6,7 +6,6 @@ import { registerModel } from '@mail/model/model_core'; import { attr, one } from '@mail/model/model_field'; import { clear } from '@mail/model/model_field_command'; -import { qweb } from 'web.core'; import { get_cookie, set_cookie, unaccent } from 'web.utils'; registerModel({ @@ -54,7 +53,7 @@ registerModel({ } }, askFeedback() { - this.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled', true); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled', true); this.messaging.publicLivechatGlobal.update({ feedbackView: {} }); /** * When we enter the "ask feedback" process of the chat, we hide some elements that become @@ -66,59 +65,28 @@ registerModel({ this.messaging.publicLivechatGlobal.chatbot.currentStep.data ) { this.messaging.publicLivechatGlobal.chatbot.currentStep.data.conversation_closed = true; - this.chatbotSaveSession(); + this.messaging.publicLivechatGlobal.chatbot.saveSession(); } - this.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); - this.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); - this.chatWindow.legacyChatWindow.$('.o_composer_text_field') + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_composer_text_field') .removeClass('d-none') .val(''); }, - /** - * Once the script ends, adds a visual element at the end of the chat window allowing to restart - * the whole script. - */ - chatbotEndScript() { - if ( - this.messaging.publicLivechatGlobal.chatbot.currentStep && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data && - this.messaging.publicLivechatGlobal.chatbot.currentStep.data.conversation_closed - ) { - // don't touch anything if the user has closed the conversation, let the chat window - // handle the display - return; - } - this.chatWindow.legacyChatWindow.$('.o_composer_text_field').addClass('d-none'); - this.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').show(); - this.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_restart').one('click', this.onChatbotRestartScript); - }, - /** - * Register current chatbot step state into localStorage to be able to resume if the visitor - * goes to another website page or if he refreshes his page. - * - * (Will not work if the visitor switches browser but his livechat session will not be restored - * anyway in that case, since it's stored into a cookie). - */ - chatbotSaveSession() { - localStorage.setItem('im_livechat.chatbot.state.uuid_' + this.messaging.publicLivechatGlobal.publicLivechat.uuid, JSON.stringify({ - '_chatbot': this.messaging.publicLivechatGlobal.chatbot.data, - '_chatbotCurrentStep': this.messaging.publicLivechatGlobal.chatbot.currentStep.data, - })); - }, /** * Restart the script and then trigger the "next step" (which will be the first of the script * in this case). */ async onChatbotRestartScript(ev) { - this.chatWindow.legacyChatWindow.$('.o_composer_text_field').removeClass('d-none'); - this.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_composer_text_field').removeClass('d-none'); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); - if (this.chatbotNextStepTimeout) { - clearTimeout(this.chatbotNextStepTimeout); + if (this.messaging.publicLivechatGlobal.chatbot.nextStepTimeout) { + clearTimeout(this.messaging.publicLivechatGlobal.chatbot.nextStepTimeout); } - if (this.chatbotWelcomeMessageTimeout) { - clearTimeout(this.chatbotWelcomeMessageTimeout); + if (this.messaging.publicLivechatGlobal.chatbot.welcomeMessageTimeout) { + clearTimeout(this.messaging.publicLivechatGlobal.chatbot.welcomeMessageTimeout); } const postedMessage = await this.messaging.rpc({ @@ -134,41 +102,16 @@ registerModel({ } this.messaging.publicLivechatGlobal.chatbot.update({ currentStep: clear() }); - this.chatbotSetIsTyping(); - this.update({ - chatbotNextStepTimeout: setTimeout( - this.widget._chatbotTriggerNextStep.bind(this.widget), + this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); + this.messaging.publicLivechatGlobal.chatbot.update({ + nextStepTimeout: setTimeout( + this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, this.messaging.publicLivechatGlobal.chatbot.messageDelay, ), }); }, - /** - * See 'chatbotSaveSession'. - * - * We retrieve the livechat uuid from the session cookie since the livechat Widget is not yet - * initialized when we restore the chatbot state. - * - * We also clear any older keys that store a previously saved chatbot session. - * (In that case we clear the actual browser's local storage, we don't use the localStorage - * object as it does not allow browsing existing keys, see 'local_storage.js'.) - */ - chatbotRestoreSession() { - const browserLocalStorage = window.localStorage; - if (browserLocalStorage && browserLocalStorage.length) { - for (let i = 0; i < browserLocalStorage.length; i++) { - const key = browserLocalStorage.key(i); - if (key.startsWith('im_livechat.chatbot.state.uuid_') && key !== this.messaging.publicLivechatGlobal.chatbotSessionCookieKey) { - browserLocalStorage.removeItem(key); - } - } - } - const chatbotState = localStorage.getItem(this.messaging.publicLivechatGlobal.chatbotSessionCookieKey); - if (chatbotState) { - this.messaging.publicLivechatGlobal.chatbot.update({ currentStep: { data: this.messaging.publicLivechatGlobal.localStorageChatbotState._chatbotCurrentStep } }); - } - }, closeChat() { - this.update({ chatWindow: clear() }); + this.messaging.publicLivechatGlobal.update({ chatWindow: clear() }); set_cookie('im_livechat_session', "", -1); // remove cookie }, /** @@ -191,42 +134,14 @@ registerModel({ } }, async openChatWindow() { - this.update({ chatWindow: {} }); - await this.chatWindow.legacyChatWindow.appendTo($('body')); + this.messaging.publicLivechatGlobal.update({ chatWindow: {} }); + await this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.appendTo($('body')); const cssProps = { bottom: 0 }; cssProps[this.messaging.locale.textDirection === 'rtl' ? 'left' : 'right'] = 0; - this.chatWindow.legacyChatWindow.$el.css(cssProps); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.$el.css(cssProps); this.widget.$el.hide(); this._openChatWindowChatbot(); }, - /** - * Adds a small "is typing" animation into the chat window. - * - * @param {boolean} [isWelcomeMessage=false] - */ - chatbotSetIsTyping(isWelcomeMessage = false) { - if (this.isTypingTimeout) { - clearTimeout(this.isTypingTimeout); - } - this.widget._chatbotDisableInput(''); - this.update({ - isTypingTimeout: setTimeout( - () => { - this.chatWindow.legacyChatWindow.$('.o_mail_thread_content').append( - $(qweb.render('im_livechat.legacy.chatbot.is_typing_message', { - 'chatbotImageSrc': `/im_livechat/operator/${ - this.messaging.publicLivechatGlobal.publicLivechat.operator.id - }/avatar`, - 'chatbotName': this.messaging.publicLivechatGlobal.chatbot.name, - 'isWelcomeMessage': isWelcomeMessage, - })) - ); - this.chatWindow.publicLivechatView.widget.scrollToBottom(); - }, - this.messaging.publicLivechatGlobal.chatbot.messageDelay / 3, - ), - }); - }, /** * @param {Object} message */ @@ -235,6 +150,37 @@ registerModel({ await this._sendMessage(message); this._sendMessageChatbotAfter(); }, + start() { + this.widget.$el.text(this.buttonText); + if (this.messaging.publicLivechatGlobal.history) { + for (const m of this.messaging.publicLivechatGlobal.history) { + this.addMessage(m); + } + this.openChat(); + } else if (!this.messaging.device.isSmall && this.messaging.publicLivechatGlobal.rule.action === 'auto_popup') { + const autoPopupCookie = get_cookie('im_livechat_auto_popup'); + if (!autoPopupCookie || JSON.parse(autoPopupCookie)) { + this.update({ + autoOpenChatTimeout: setTimeout( + this.openChat, + this.messaging.publicLivechatGlobal.rule.auto_popup_timer * 1000, + ), + }); + } + } + if (this.buttonBackgroundColor) { + this.widget.$el.css('background-color', this.buttonBackgroundColor); + } + if (this.buttonTextColor) { + this.widget.$el.css('color', this.buttonTextColor); + } + + // If website_event_track installed, put the livechat banner above the PWA banner. + const pwaBannerHeight = $('.o_pwa_install_banner').outerHeight(true); + if (pwaBannerHeight) { + this.widget.$el.css('bottom', pwaBannerHeight + 'px'); + } + }, /** * @private * @returns {string} @@ -257,16 +203,6 @@ registerModel({ _computeButtonTextColor() { return this.messaging.publicLivechatGlobal.options.button_text_color; }, - /** - * @private - * @returns {integer|FieldCommand} - */ - _computeChatbotMessageDelay() { - if (this.messaging.publicLivechatGlobal.isWebsiteLivechatChatbotFlow) { - return 100; - } - return clear(); - }, /** * @private * @returns {integer} @@ -333,10 +269,10 @@ registerModel({ if (this.messaging.publicLivechatGlobal.livechatInit && this.messaging.publicLivechatGlobal.livechatInit.rule.chatbot) { return true; } - if (this.messaging.publicLivechatGlobal.chatbotState === 'welcome') { + if (this.messaging.publicLivechatGlobal.chatbot.state === 'welcome') { return true; } - if (this.messaging.publicLivechatGlobal.localStorageChatbotState) { + if (this.messaging.publicLivechatGlobal.chatbot.localStorageState) { return true; } return clear(); @@ -361,7 +297,7 @@ registerModel({ */ _computeServerUrl() { if (this.isChatbot) { - return this.messaging.publicLivechatGlobal.chatbotServerUrl; + return this.messaging.publicLivechatGlobal.chatbot.serverUrl; } return this.messaging.publicLivechatGlobal.serverUrl; }, @@ -449,8 +385,8 @@ registerModel({ */ _openChatWindowChatbot() { window.addEventListener('resize', () => { - if (this.chatWindow) { - this.chatWindow.publicLivechatView.widget.scrollToBottom(); + if (this.messaging.publicLivechatGlobal.chatWindow) { + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); } }); @@ -493,13 +429,13 @@ registerModel({ } this.closeChat(); } - this.chatWindow.publicLivechatView.widget.scrollToBottom(); + this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.scrollToBottom(); }, /** * @private */ _sendMessageChatbotAfter() { - if (this.isChatbotRedirecting) { + if (this.messaging.publicLivechatGlobal.chatbot.isRedirecting) { return; } if ( @@ -513,19 +449,19 @@ registerModel({ ) { return; // operator has taken over the conversation, let them speak } else if (this.messaging.publicLivechatGlobal.chatbot.currentStep.data.chatbot_step_type === 'free_input_multi') { - this.widget._debouncedChatbotAwaitUserInput(); + this.messaging.publicLivechatGlobal.chatbot.debouncedAwaitUserInput(); } else if (!this.messaging.publicLivechatGlobal.chatbot.shouldEndScript) { - this.chatbotSetIsTyping(); - this.update({ - chatbotNextStepTimeout: setTimeout( - this.widget._chatbotTriggerNextStep.bind(this.widget), + this.messaging.publicLivechatGlobal.chatbot.setIsTyping(); + this.messaging.publicLivechatGlobal.chatbot.update({ + nextStepTimeout: setTimeout( + this.messaging.publicLivechatGlobal.chatbot.triggerNextStep, this.messaging.publicLivechatGlobal.chatbot.messageDelay, ), }); } else { - this.chatbotEndScript(); + this.messaging.publicLivechatGlobal.chatbot.endScript(); } - this.chatbotSaveSession(); + this.messaging.publicLivechatGlobal.chatbot.saveSession(); } }, /** @@ -533,7 +469,7 @@ registerModel({ * - If the conversation has been forwarded to an operator * Then there is nothing to do, we let them speak * - If we are currently on a 'free_input_multi' step - * Await more user input (see #_chatbotAwaitUserInput() for details) + * Await more user input (see #Chatbot/awaitUserInput for details) * - Otherwise we continue the script or end it if it's the last step * * We also save the current session state. @@ -569,10 +505,6 @@ registerModel({ }), chatbotNextStepTimeout: attr(), chatbotWelcomeMessageTimeout: attr(), - chatWindow: one('PublicLivechatWindow', { - inverse: 'livechatButtonViewOwner', - isCausal: true, - }), currentPartnerId: attr({ compute: '_computeCurrentPartnerId', }), @@ -593,9 +525,6 @@ registerModel({ compute: '_computeIsChatbot', default: false, }), - isChatbotRedirecting: attr({ - default: false, - }), isOpenChatDebounced: attr({ compute: '_computeIsOpenChatDebounced', default: true, @@ -614,7 +543,6 @@ registerModel({ serverUrl: attr({ compute: '_computeServerUrl', }), - testChatbotData: attr(), titleColor: attr({ compute: '_computeTitleColor', }), diff --git a/addons/im_livechat/static/src/models/public_livechat_feedback_view.js b/addons/im_livechat/static/src/models/public_livechat_feedback_view.js index 6ae55db488b..7110f9959a4 100644 --- a/addons/im_livechat/static/src/models/public_livechat_feedback_view.js +++ b/addons/im_livechat/static/src/models/public_livechat_feedback_view.js @@ -16,7 +16,7 @@ registerModel({ this.messaging.publicLivechatGlobal.publicLivechat.legacyPublicLivechat, ), }); - this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.legacyChatWindow.replaceContentWith(this.widget); + this.messaging.publicLivechatGlobal.chatWindow.legacyChatWindow.replaceContentWith(this.widget); this.widget.on('feedback_sent', null, this._onFeedbackSent); this.widget.on('send_message', null, this._onSendMessage); }, diff --git a/addons/im_livechat/static/src/models/public_livechat_global.js b/addons/im_livechat/static/src/models/public_livechat_global.js index 84e652f333c..41760c632a7 100644 --- a/addons/im_livechat/static/src/models/public_livechat_global.js +++ b/addons/im_livechat/static/src/models/public_livechat_global.js @@ -48,48 +48,6 @@ registerModel({ _computeChannelId() { return this.options.channel_id; }, - /** - * @private - * @returns {FieldCommand} - */ - _computeChatbot() { - if (this.isTestChatbot) { - return { data: this.testChatbotData.chatbot }; - } - if (this.chatbotState === 'init') { - return { data: this.rule.chatbot }; - } - if (this.chatbotState === 'welcome') { - return { data: this.livechatInit.rule.chatbot }; - } - if (this.chatbotState === 'restore_session' && this.localStorageChatbotState) { - return { data: this.localStorageChatbotState._chatbot }; - } - return clear(); - }, - /** - * @private - * @returns {string|FieldCommand} - */ - _computeChatbotSessionCookieKey() { - if (!this.sessionCookie) { - return clear(); - } - return 'im_livechat.chatbot.state.uuid_' + JSON.parse(this.sessionCookie).uuid; - }, - /** - * @private - * @returns {string|FieldCommand} - */ - _computeChatbotState() { - if (this.rule && !!this.rule.chatbot) { - return 'init'; - } - if (this.livechatInit && this.livechatInit.rule.chatbot) { - return 'welcome'; - } - return clear(); - }, /** * Compares the last message of the conversation to this livechat's operator id. * @@ -125,20 +83,6 @@ registerModel({ } return clear(); }, - /** - * @private - * @returns {FieldCommand} - */ - _computeLocalStorageChatbotState() { - if (!this.sessionCookie) { - return clear(); - } - const data = localStorage.getItem(this.chatbotSessionCookieKey); - if (!data) { - return clear(); - } - return JSON.parse(data); - }, async _willStart() { const cookie = get_cookie('im_livechat_session'); if (cookie) { @@ -197,13 +141,13 @@ registerModel({ const sessionCookie = get_cookie('im_livechat_session'); if (sessionCookie) { this.update({ sessionCookie }); - if (localStorage.getItem(this.chatbotSessionCookieKey)) { - this.update({ chatbotState: 'restore_session' }); + if (localStorage.getItem(this.chatbot.sessionCookieKey)) { + this.chatbot.update({ state: 'restore_session' }); } } } - if (this.chatbotState === 'init') { + if (this.chatbot.state === 'init') { // we landed on a website page where a channel rule is configured to run a chatbot.script // -> initialize necessary state if (this.rule.chatbot_welcome_steps && this.rule.chatbot_welcome_steps.length !== 0) { @@ -213,7 +157,7 @@ registerModel({ }, }); } - } else if (this.chatbotState === 'welcome') { + } else if (this.chatbot.state === 'welcome') { // we landed on a website page and a chatbot script was initialized on a previous one // however the end-user did not interact with the bot ( :( ) // -> remove cookie to force opening the popup again @@ -222,10 +166,10 @@ registerModel({ set_cookie('im_livechat_auto_popup', '', -1); this.update({ history: clear() }); this.update({ rule: this.livechatInit.rule }); - } else if (this.chatbotState === 'restore_session') { + } else if (this.chatbot.state === 'restore_session') { // we landed on a website page and a chatbot script is currently running - // -> restore the user's session (see 'chatbotRestoreSession') - this.livechatButtonView.chatbotRestoreSession(); + // -> restore the user's session (see 'Chatbot/restoreSession') + this.chatbot.restoreSession(); } }, }, @@ -247,16 +191,13 @@ registerModel({ compute: '_computeChannelId', }), chatbot: one('Chatbot', { - compute: '_computeChatbot', + default: {}, inverse: 'publicLivechatGlobalOwner', isCausal: true, }), - chatbotServerUrl: attr(), - chatbotSessionCookieKey: attr({ - compute: '_computeChatbotSessionCookieKey', - }), - chatbotState: attr({ - compute: '_computeChatbotState', + chatWindow: one('PublicLivechatWindow', { + inverse: 'publicLivechatGlobalOwner', + isCausal: true, }), feedbackView: one('PublicLivechatFeedbackView', { inverse: 'publicLivechatGlobalOwner', @@ -281,9 +222,6 @@ registerModel({ isTestChatbot: attr({ default: false, }), - isWebsiteLivechatChatbotFlow: attr({ - default: false, - }), lastMessage: one('PublicLivechatMessage', { compute: '_computeLastMessage', }), @@ -293,9 +231,6 @@ registerModel({ isCausal: true, }), livechatInit: attr(), - localStorageChatbotState: attr({ - compute: '_computeLocalStorageChatbotState', - }), messages: many('PublicLivechatMessage'), notificationHandler: one('PublicLivechatGlobalNotificationHandler', { inverse: 'publicLivechatGlobalOwner', @@ -313,5 +248,6 @@ registerModel({ default: '', }), sessionCookie: attr(), + testChatbotData: attr(), }, }); diff --git a/addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js b/addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js index abd6fad3314..53d1c3ba161 100644 --- a/addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js +++ b/addons/im_livechat/static/src/models/public_livechat_global_notification_handler.js @@ -64,7 +64,7 @@ registerModel({ } notificationData.body = utils.Markup(notificationData.body); this.messaging.publicLivechatGlobal.livechatButtonView.addMessage(notificationData); - if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.publicLivechatView.widget.isAtBottom()) { + if (this.messaging.publicLivechatGlobal.publicLivechat.isFolded || !this.messaging.publicLivechatGlobal.chatWindow.publicLivechatView.widget.isAtBottom()) { this.messaging.publicLivechatGlobal.publicLivechat.update({ unreadCounter: increment() }); } this.messaging.publicLivechatGlobal.livechatButtonView.widget._renderMessages(); diff --git a/addons/im_livechat/static/src/models/public_livechat_window.js b/addons/im_livechat/static/src/models/public_livechat_window.js index 67387d9dea0..be8fa4b7de4 100644 --- a/addons/im_livechat/static/src/models/public_livechat_window.js +++ b/addons/im_livechat/static/src/models/public_livechat_window.js @@ -11,7 +11,7 @@ registerModel({ _created() { this.update({ legacyChatWindow: new PublicLivechatWindow( - this.livechatButtonViewOwner.widget, + this.messaging.publicLivechatGlobal.livechatButtonView.widget, this.messaging, this.messaging.publicLivechatGlobal.publicLivechat.legacyPublicLivechat, ), @@ -40,7 +40,7 @@ registerModel({ legacyChatWindow: attr({ default: null, }), - livechatButtonViewOwner: one('LivechatButtonView', { + publicLivechatGlobalOwner: one('PublicLivechatGlobal', { identifying: true, inverse: 'chatWindow', }), diff --git a/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js b/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js index 296f13351d2..a84bb3a5ccb 100644 --- a/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js +++ b/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js @@ -26,7 +26,7 @@ const LivechatButtonTestChatbot = LivechatButton.extend({ }, }); this.messaging.publicLivechatGlobal.update({ isTestChatbot: true }); - this.messaging.publicLivechatGlobal.livechatButtonView.update({ testChatbotData: chatbotData.chatbot }); + this.messaging.publicLivechatGlobal.update({ testChatbotData: chatbotData.chatbot }); this.messaging.publicLivechatGlobal.chatbot.update({ currentStep: { data: this.messaging.publicLivechatGlobal.chatbot.lastWelcomeStep, @@ -52,7 +52,7 @@ publicWidget.registry.livechatChatbotTestScript = publicWidget.Widget.extend({ const messaging = await this.env.services.messaging.get(); return this._super(...arguments).then(() => { messaging.update({ - publicLivechatGlobal: { isAvailable: true, chatbotServerUrl: this.$el.data().serverUrl }, + publicLivechatGlobal: { isAvailable: true, chatbot: { serverUrl: this.$el.data().serverUrl } }, }); this.livechatButton = new LivechatButtonTestChatbot(this, messaging, this.$el.data()); this.livechatButton.appendTo(document.body); diff --git a/addons/website_livechat/static/src/models/chatbot.js b/addons/website_livechat/static/src/models/chatbot.js new file mode 100644 index 00000000000..8cb790cf738 --- /dev/null +++ b/addons/website_livechat/static/src/models/chatbot.js @@ -0,0 +1,40 @@ +/** @odoo-module **/ + +import { addFields, patchRecordMethods } from '@mail/model/model_core'; +import { attr } from '@mail/model/model_field'; +// ensure that the model definition is loaded before the patch +import '@im_livechat/models/chatbot'; + +addFields('Chatbot', { + isWebsiteLivechatTourFlow: attr({ + default: false, + }), +}); + +patchRecordMethods('Chatbot', { + /** + * @override + */ + _computeAwaitUserInputDebounceTime() { + if (this.isWebsiteLivechatTourFlow) { + /** + * Let us make it a bit faster than the default delay (3500ms). + * Let us also debounce waiting for more user inputs for only 500ms. + */ + return 500; + } + return this._super(); + }, + /** + * @private + * @returns {integer|FieldCommand} + */ + _computeMessageDelay() { + if (this.isWebsiteLivechatTourFlow) { + return 100; + } + return this._super(); + }, +}); + + diff --git a/addons/website_livechat/static/src/models/livechat_button_view.js b/addons/website_livechat/static/src/models/livechat_button_view.js index a4a69300009..9d2df13064b 100644 --- a/addons/website_livechat/static/src/models/livechat_button_view.js +++ b/addons/website_livechat/static/src/models/livechat_button_view.js @@ -16,7 +16,7 @@ patchRecordMethods('LivechatButtonView', { * @override */ async onChatbotRestartScript(ev) { - if (this.messaging.publicLivechatGlobal.isWebsiteLivechatChatbotFlow) { + if (this.messaging.publicLivechatGlobal.chatbot.isWebsiteLivechatTourFlow) { this.messaging.publicLivechatGlobal.update({ messages: clear() }); this.widget._renderMessages(); } @@ -45,7 +45,7 @@ patchRecordMethods('LivechatButtonView', { return this._super(); } this.messaging.publicLivechatGlobal.update({ - publicLivechat: { data: this.testChatbotData.channel }, + publicLivechat: { data: this.messaging.publicLivechatGlobal.testChatbotData.channel }, }); await this.openChatWindow(); this.widget._sendWelcomeMessage(); diff --git a/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js b/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js index 90766eeff78..a074b29584b 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_chatbot_flow.js @@ -1,21 +1,19 @@ /** @odoo-module */ import tour from "web_tour.tour"; -import LivechatButton from '@im_livechat/legacy/widgets/livechat_button'; -LivechatButton.include({ +import { patchRecordMethods } from '@mail/model/model_core'; +// ensure that the model definition is loaded before the patch +import '@im_livechat/models/livechat_button_view'; + +patchRecordMethods('LivechatButtonView', { /** * Let us make it a bit faster than the default delay (3500ms). * Let us also debounce waiting for more user inputs for only 500ms. */ - start: function () { - this.messaging.publicLivechatGlobal.update({ isWebsiteLivechatChatbotFlow: true }); - - this._debouncedChatbotAwaitUserInput = _.debounce( - this._chatbotAwaitUserInput.bind(this), - 500); - - return this._super(...arguments); + start() { + this.messaging.publicLivechatGlobal.chatbot.update({ isWebsiteLivechatTourFlow: true }); + this._super(); }, });