[IMP] im_livechat, *: Refactor JS Public Livechat (step 14)

*: 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) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-08-25 04:21:01 +02:00
parent 5d4aa927f1
commit 34c88157a6
18 changed files with 590 additions and 417 deletions
@@ -362,6 +362,14 @@ const PublicLivechatMessage = Class.extend({
return !this._isMyselfAuthor();
},
isVisitorTheAuthor() {
return !this.hasAuthor() || this._isMyselfAuthor();
},
isOperatorTheAuthor() {
return this.hasAuthor() && !this._isMyselfAuthor();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -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;
}
@@ -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,
@@ -75,7 +75,7 @@
<p class="o_mail_info text-muted">
<strong class="o_thread_author" t-out="chatbotName"/>
</p>
<div class="o_thread_message_content">
<div class="o_thread_message_content o_PublicLivechatMessage_content">
<img class="o_livechat_chatbot_typing"
src="/im_livechat/static/src/img/chatbot_is_typing.gif"
width="30" alt="is typing"/>
@@ -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();
},
});
@@ -136,7 +136,7 @@
@param {integer} [options.selectedMessageID]
-->
<t t-name="im_livechat.legacy.mail.widget.Thread.Message">
<div t-if="!message.isEmpty()" t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
<div t-if="!message.isEmpty()" t-att-class="'o_thread_message o_PublicLivechatMessage ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ') + (message.isVisitorTheAuthor() ? 'o-isVisitorTheAuthor' : '')">
<div t-if="options.displayAvatars" class="o_thread_message_sidebar">
<t t-if="message.hasAuthor()">
<div t-if="displayAuthorMessages[message.getID()]" class="o_thread_message_sidebar_image">
@@ -163,26 +163,27 @@
</span>
</div>
<div class="o_thread_message_core">
<p t-if="displayAuthorMessages[message.getID()]" class="o_mail_info text-muted">
<p t-if="displayAuthorMessages[message.getID()]" t-attf-class="o_mail_info text-muted o_PublicLivechatMessage_header {{ message.isVisitorTheAuthor() ? 'o-isVisitorTheAuthor' : '' }}">
<t t-if="message.isNote()">
Note by
</t>
<t t-if="!message.isVisitorTheAuthor()">
<strong t-if="message.hasAuthor()"
data-oe-model="res.partner" t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
t-attf-class="o_thread_author o_PublicLivechatMessage_headerAuthor #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}">
<t t-esc="message.getDisplayedAuthor()"/>
</strong>
<strong t-elif="message.hasEmailFrom()">
<a class="text-muted" t-attf-href="mailto:#{message.getEmailFrom()}?subject=Re: #{message.hasSubject() ? message.getSubject() : ''}">
<t t-esc="message.getEmailFrom()"/>
</a>
</strong>
<strong t-else="" class="o_thread_author o_PublicLivechatMessage_headerAuthor">
<t t-esc="message.getDisplayedAuthor()"/>
</strong>
</t>
<strong t-if="message.hasAuthor()"
data-oe-model="res.partner" t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
t-attf-class="o_thread_author #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}">
<t t-esc="message.getDisplayedAuthor()"/>
</strong>
<strong t-elif="message.hasEmailFrom()">
<a class="text-muted" t-attf-href="mailto:#{message.getEmailFrom()}?subject=Re: #{message.hasSubject() ? message.getSubject() : ''}">
<t t-esc="message.getEmailFrom()"/>
</a>
</strong>
<strong t-else="" class="o_thread_author">
<t t-esc="message.getDisplayedAuthor()"/>
</strong>
- <small class="o_mail_timestamp" t-att-title="message.getDate().format(dateFormat)"><t t-esc="message.getTimeElapsed()"/></small>
<span t-if="!message.isVisitorTheAuthor()" class="o_PublicLivechatMessage_headerDatePrefix">-</span> <small class="o_mail_timestamp o_PublicLivechatMessage_headerDate" t-att-title="message.getDate().format(dateFormat)"><t t-esc="message.getTimeElapsed()"/></small>
<t t-if="message.isLinkedToDocumentThread() and options.displayDocumentLinks">
<small>on</small> <a t-att-href="message.getURL()" t-att-data-oe-model="message.getDocumentModel()" t-att-data-oe-id="message.getDocumentID()" class="o_document_link"><t t-esc="message.getDocumentName()"/></a>
</t>
@@ -192,14 +193,19 @@
t-att-data-message-id="message.getID()" title="Mark as Read" role="img" aria-label="Mark as Read"/>
</span>
</p>
<div class="o_thread_message_content">
<t t-out="message.getBody()"/>
<t t-if="message.hasTrackingValues()">
<t t-if="message.hasSubtypeDescription()">
<p><t t-esc="message.getSubtypeDescription()"/></p>
</t>
<t t-call="im_livechat.legacy.mail.widget.Thread.MessageTracking"/>
</t>
<div t-att-data-message-id="message.getID()" t-attf-class="o_PublicLivechatMessage_bubbleWrap {{ message.isVisitorTheAuthor() ? 'o-isVisitorTheAuthor' : '' }} {{ message.isOperatorTheAuthor() ? 'o-isOperatorTheAuthor' : '' }}">
<div t-attf-class="o_PublicLivechatMessage_bubble {{ message.isVisitorTheAuthor() ? 'o-isVisitorTheAuthor' : '' }} {{ message.isOperatorTheAuthor() ? 'o-isOperatorTheAuthor' : '' }} {{ !message.isEmpty() ? 'o-isContentNonEmpty' : '' }}">
<div t-attf-class="o_PublicLivechatMessage_background {{ message.isVisitorTheAuthor() ? 'o-isVisitorTheAuthor' : '' }} {{ message.isOperatorTheAuthor() ? 'o-isOperatorTheAuthor' : '' }}"/>
<div class="o_thread_message_content o_PublicLivechatMessage_content">
<t t-out="message.getBody()"/>
<t t-if="message.hasTrackingValues()">
<t t-if="message.hasSubtypeDescription()">
<p><t t-esc="message.getSubtypeDescription()"/></p>
</t>
<t t-call="im_livechat.legacy.mail.widget.Thread.MessageTracking"/>
</t>
</div>
</div>
</div>
</div>
</div>
@@ -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();
}
},
@@ -19,7 +19,7 @@
<div class="o_thread_window_content">
</div>
<div class="o_thread_composer o_chat_mini_composer">
<input class="o_composer_text_field" t-att-placeholder="widget.messaging.publicLivechatGlobal.livechatButtonView.chatWindow.inputPlaceholder"/>
<input class="o_composer_text_field o_PublicLivechatWindow_composer" t-att-placeholder="widget.messaging.publicLivechatGlobal.chatWindow.inputPlaceholder"/>
</div>
</div>
</t>
+269 -4
View File
@@ -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',
}),
@@ -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',
}),
@@ -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);
},
@@ -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(),
},
});
@@ -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();
@@ -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',
}),
@@ -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);
@@ -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();
},
});
@@ -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();
@@ -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();
},
});