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(); }, });