From 67e35f45a438767ef971510bc44e710b361bba56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Wed, 22 Jun 2022 09:31:03 +0000 Subject: [PATCH] [IMP] im_livechat, *: Refactor JS Public Livechat (step 6) *: 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-2896237 Part-of: odoo/odoo#94722 --- .../src/legacy/models/abstract_message.js | 462 ---------------- .../src/legacy/models/abstract_thread.js | 194 ------- .../src/legacy/models/website_livechat.js | 148 +++++- .../legacy/models/website_livechat_message.js | 417 ++++++++++++++- .../legacy/models/website_livechat_window.js | 458 +++++++++++++++- .../src/legacy/public_livechat_chatbot.js | 12 +- .../src/legacy/public_livechat_chatbot.xml | 4 +- .../website_livechat_message_chatbot.js | 8 +- .../abstract_thread_window.js | 498 ------------------ .../abstract_thread_window.xml | 8 +- .../src/legacy/widgets/feedback/feedback.js | 2 +- .../src/legacy/widgets/livechat_button.js | 12 +- .../src/legacy/widgets/thread/thread.js | 4 +- .../im_livechat/static/src/models/chatbot.js | 10 + .../static/src/models/livechat_button_view.js | 21 +- .../website_livechat_chatbot_test_script.js | 4 +- 16 files changed, 1043 insertions(+), 1219 deletions(-) delete mode 100644 addons/im_livechat/static/src/legacy/models/abstract_message.js delete mode 100644 addons/im_livechat/static/src/legacy/models/abstract_thread.js delete mode 100644 addons/im_livechat/static/src/legacy/widgets/abstract_thread_window/abstract_thread_window.js diff --git a/addons/im_livechat/static/src/legacy/models/abstract_message.js b/addons/im_livechat/static/src/legacy/models/abstract_message.js deleted file mode 100644 index 3daf069d5d3..00000000000 --- a/addons/im_livechat/static/src/legacy/models/abstract_message.js +++ /dev/null @@ -1,462 +0,0 @@ -/** @odoo-module **/ - -import * as mailUtils from '@mail/js/utils'; - -import Class from 'web.Class'; -import core from 'web.core'; -import session from 'web.session'; -import time from 'web.time'; - -const _t = core._t; - -/** - * This is an abstract class for modeling messages in JS. - * The purpose of this interface is to make im_livechat compatible with - * mail.widget.Thread, as this widget was designed to work with messages that - * are instances of mail.model.Messages. - * - * Ideally, im_livechat should also handle mail.model.Message, but this is not - * feasible for the moment, as mail.model.Message requires mail.Manager to work, - * and this module should not leak outside of the backend, hence the use of - * mail.model.AbstractMessage as a work-around. - */ -const AbstractMessage = Class.extend({ - - /** - * @param {Widget} parent - * @param {Object} data - * @param {Array} [data.attachment_ids=[]] - * @param {Array} [data.author_id] - * @param {string} [data.body = ""] - * @param {string} [data.date] the server-format date time of the message. - * If not provided, use current date time for this message. - * @param {integer} data.id - * @param {boolean} [data.is_discussion = false] - * @param {boolean} [data.is_notification = false] - * @param {string} [data.message_type = undefined] - */ - init(parent, data) { - // Attachments are not supported in (public) livechat. - // We ignore data from server, otherwise field commands - // will be wrongly considered as unamed attachments. - this._attachmentIDs = []; - this._body = data.body || ""; - // by default: current datetime - this._date = data.date ? moment(time.str_to_datetime(data.date)) : moment(); - this._id = data.id; - this._isDiscussion = data.is_discussion; - this._isNotification = data.is_notification; - this._serverAuthorID = data.author_id; - this._type = data.message_type || undefined; - - this._processAttachmentURL(); - this._attachmentIDs.forEach(function (attachment) { - attachment.filename = attachment.filename || attachment.name || _t("unnamed"); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the list of files attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getAttachments() { - return this._attachmentIDs; - }, - /** - * Get the server ID (number) of the author of this message - * If there are no author, return -1; - * - * @return {integer} - */ - getAuthorID() { - if (!this.hasAuthor()) { - return -1; - } - return this._serverAuthorID[0]; - }, - /** - * Threads do not have an im status by default - * - * @return {undefined} - */ - getAuthorImStatus() { - return undefined; - }, - /** - * Get the relative url of the avatar to display next to the message - * - * @abstract - * @return {string} - */ - getAvatarSource() { - if (this.hasAuthor()) { - return '/web/image/res.partner/' + this.getAuthorID() + '/avatar_128'; - } - }, - /** - * Get the body content of this message - * - * @return {string} - */ - getBody() { - return this._body; - }, - /** - * @return {moment} - */ - getDate() { - return this._date; - }, - /** - * Get the date day of this message - * - * @return {string} - */ - getDateDay() { - const date = this.getDate().format('YYYY-MM-DD'); - if (date === moment().format('YYYY-MM-DD')) { - return _t("Today"); - } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { - return _t("Yesterday"); - } - return this.getDate().format('LL'); - }, - /** - * Get the name of the author, if there is an author of this message - * If there are no author of this message, returns 'null' - * - * @return {string} - */ - getDisplayedAuthor() { - return this.hasAuthor() ? this._getAuthorName() : null; - }, - /** - * Get the server ID (number) of this message - * - * @override - * @return {integer} - */ - getID() { - return this._id; - }, - /** - * Get the list of images attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getImageAttachments() { - return _.filter(this.getAttachments(), function (attachment) { - return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; - }); - }, - /** - * Get the list of non-images attached to this message. - * Note that attachments are stored with server-format - * - * @return {Object[]} - */ - getNonImageAttachments() { - return _.difference(this.getAttachments(), this.getImageAttachments()); - }, - /** - * Gets the class to use as the notification icon. - * - * @returns {string} - */ - getNotificationIcon() { - if (!this.hasNotificationsError()) { - return 'fa fa-envelope-o'; - } - return 'fa fa-envelope'; - }, - /** - * Gets the list of notifications of this message, in no specific order. - * By default messages do not have notifications. - * - * @returns {Object[]} - */ - getNotifications() { - return []; - }, - /** - * Gets the text to display next to the notification icon. - * - * @returns {string} - */ - getNotificationText() { - return ''; - }, - /** - * Get the time elapsed between sent message and now - * - * @return {string} - */ - getTimeElapsed() { - return mailUtils.timeFromNow(this.getDate()); - }, - /** - * Get the type of message (e.g. 'comment', 'email', 'notification', ...) - * By default, messages are of type 'undefined' - * - * @override - * @return {string|undefined} - */ - getType() { - return this._type; - }, - /** - * State whether this message contains some attachments. - * - * @override - * @return {boolean} - */ - hasAttachments() { - return this.getAttachments().length > 0; - }, - /** - * State whether this message has an author - * - * @return {boolean} - */ - hasAuthor() { - return !!(this._serverAuthorID && this._serverAuthorID[0]); - }, - /** - * State whether this message has an email of its sender. - * By default, messages do not have any email of its sender. - * - * @return {string} - */ - hasEmailFrom() { - return false; - }, - /** - * State whether this image contains images attachments - * - * @return {boolean} - */ - hasImageAttachments() { - return _.some(this.getAttachments(), function (attachment) { - return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; - }); - }, - /** - * State whether this image contains non-images attachments - * - * @return {boolean} - */ - hasNonImageAttachments() { - return _.some(this.getAttachments(), function (attachment) { - return !(attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'); - }); - }, - /** - * States whether this message has some notifications. - * - * @returns {boolean} - */ - hasNotifications() { - return this.getNotifications().length > 0; - }, - /** - * States whether this message has notifications that are in error. - * - * @returns {boolean} - */ - hasNotificationsError() { - return this.getNotifications().some(notif => - notif.notification_status === 'exception' || - notif.notification_status === 'bounce' - ); - }, - /** - * State whether this message originates from a channel. - * By default, messages do not originate from a channel. - * - * @override - * @return {boolean} - */ - originatesFromChannel() { - return false; - }, - /** - * State whether this message has a subject - * By default, messages do not have any subject. - * - * @return {boolean} - */ - hasSubject() { - return false; - }, - /** - * State whether this message is empty - * - * @return {boolean} - */ - isEmpty() { - return !this.hasTrackingValues() && - !this.hasAttachments() && - !this.getBody(); - }, - /** - * By default, messages do not have any subtype description - * - * @return {boolean} - */ - hasSubtypeDescription() { - return false; - }, - /** - * State whether this message contains some tracking values - * By default, messages do not have any tracking values. - * - * @return {boolean} - */ - hasTrackingValues() { - return false; - }, - /** - * State whether this message is a discussion - * - * @return {boolean} - */ - isDiscussion() { - return this._isDiscussion; - }, - /** - * State whether this message is linked to a document thread - * By default, messages are not linked to a document thread. - * - * @return {boolean} - */ - isLinkedToDocumentThread() { - return false; - }, - /** - * State whether this message is needaction - * By default, messages are not needaction. - * - * @return {boolean} - */ - isNeedaction() { - return false; - }, - /** - * State whether this message is a note (i.e. a message from "Log note") - * - * @return {boolean} - */ - isNote() { - return this._isNote; - }, - /** - * State whether this message is a notification - * - * User notifications are defined as either - * - notes - * - pushed to user Inbox or email through classic notification process - * - not linked to any document, meaning model and res_id are void - * - * This is useful in order to display white background for user - * notifications in chatter - * - * @returns {boolean} - */ - isNotification() { - return this._isNotification; - }, - /** - * State whether this message is starred - * By default, messages are not starred. - * - * @return {boolean} - */ - isStarred() { - return false; - }, - /** - * State whether this message is a system notification - * By default, messages are not system notifications - * - * @override - * @return {boolean} - */ - isSystemNotification() { - return false; - }, - /** - * States whether the current message needs moderation in general. - * By default, messages do not require any moderation. - * - * @returns {boolean} - */ - needsModeration() { - return false; - }, - /** - * @params {integer[]} attachmentIDs - */ - removeAttachments(attachmentIDs) { - this._attachmentIDs = _.reject(this._attachmentIDs, function (attachment) { - return _.contains(attachmentIDs, attachment.id); - }); - }, - /** - * State whether this message should redirect to the author - * when clicking on the author of this message. - * - * Do not redirect on author clicked of self-posted messages. - * - * @return {boolean} - */ - shouldRedirectToAuthor() { - return !this._isMyselfAuthor(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Get the name of the author of this message. - * If there are no author of this messages, returns '' (empty string). - * - * @private - * @returns {string} - */ - _getAuthorName() { - if (!this.hasAuthor()) { - return ""; - } - return this._serverAuthorID[1]; - }, - /** - * State whether the current user is the author of this message - * - * @private - * @return {boolean} - */ - _isMyselfAuthor() { - return this.hasAuthor() && (this.getAuthorID() === session.partner_id); - }, - /** - * Compute url of attachments of this message - * - * @private - */ - _processAttachmentURL() { - for (let attachment of this.getAttachments()) { - attachment.url = '/web/content/' + attachment.id + '?download=true'; - } - }, - -}); - -export default AbstractMessage; diff --git a/addons/im_livechat/static/src/legacy/models/abstract_thread.js b/addons/im_livechat/static/src/legacy/models/abstract_thread.js deleted file mode 100644 index 70911015d17..00000000000 --- a/addons/im_livechat/static/src/legacy/models/abstract_thread.js +++ /dev/null @@ -1,194 +0,0 @@ -/** @odoo-module **/ - -import Class from 'web.Class'; -import Mixins from'web.mixins'; - -/** - * Abstract thread is the super class of all threads, either backend threads - * (which are compatible with mail service) or website livechats. - * - * Abstract threads contain abstract messages - */ -const AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { - /** - * @param {Object} params - * @param {Object} params.data - * @param {integer|string} params.data.id the ID of this thread - * @param {string} params.data.name the name of this thread - * @param {string} [params.data.status=''] the status of this thread - * @param {Object} params.parent Object with the event-dispatcher mixin - * (@see {web.mixins.EventDispatcherMixin}) - */ - init(params) { - Mixins.EventDispatcherMixin.init.call(this, arguments); - this.setParent(params.parent); - - this._folded = false; // threads are unfolded by default - this._id = params.data.id; - this._name = params.data.name; - this._status = params.data.status || ''; - this._unreadCounter = 0; // amount of messages not yet been read - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Add a message to this thread. - * - * @param {@im_livechat/legacy/models/abstract_message} message - */ - addMessage(message) { - this._addMessage(...arguments); - this.trigger('message_added', message); - }, - /** - * Updates the folded state of the thread - * - * @param {boolean} folded - */ - fold(folded) { - this._folded = folded; - }, - /** - * Get the ID of this thread - * - * @returns {integer|string} - */ - getID() { - return this._id; - }, - /** - * @abstract - * @returns {@im_livechat/legacy/models/abstract_message[]} - */ - getMessages() {}, - /** - * Get the name of this thread. If the name of the thread has been created - * by the user from an input, it may be escaped. - * - * @returns {string} - */ - getName() { - return this._name; - }, - /** - * Get the status of the thread (e.g. 'online', 'offline', etc.) - * - * @returns {string} - */ - getStatus() { - return this._status; - }, - /** - * Returns the title to display in thread window's headers. - * - * @returns {string} the name of the thread by default (see @getName) - */ - getTitle() { - return this.getName(); - }, - getType() {}, - /** - * @returns {integer} - */ - getUnreadCounter() { - return this._unreadCounter; - }, - /** - * @returns {boolean} - */ - hasMessages() { - return !_.isEmpty(this.getMessages()); - }, - /** - * States whether this thread is folded or not. - * - * @return {boolean} - */ - isFolded() { - return this._folded; - }, - /** - * Mark the thread as read, which resets the unread counter to 0. This is - * only performed if the unread counter is not 0. - * - * @returns {Promise} - */ - markAsRead() { - if (this._unreadCounter > 0) { - return this._markAsRead(); - } - return Promise.resolve(); - }, - /** - * Post a message on this thread - * - * @returns {Promise} resolved with the message object to be sent to the - * server - */ - postMessage() { - return this._postMessage(...arguments) - .then(this.trigger.bind(this, 'message_posted')); - }, - /** - * Resets the unread counter of this thread to 0. - */ - resetUnreadCounter() { - this._unreadCounter = 0; - this._warnUpdatedUnreadCounter(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Add a message to this thread. - * - * @abstract - * @private - * @param {@im_livechat/legacy/models/abstract_message} message - */ - _addMessage(message) {}, - /** - * Increments the unread counter of this thread by 1 unit. - * - * @private - */ - _incrementUnreadCounter() { - this._unreadCounter++; - }, - /** - * Mark the thread as read - * - * @private - * @returns {Promise} - */ - _markAsRead() { - this.resetUnreadCounter(); - return Promise.resolve(); - }, - /** - * Post a message on this thread - * - * @abstract - * @private - * @returns {Promise} resolved with the message object to be sent to the - * server - */ - _postMessage() { - return Promise.resolve(); - }, - /** - * Warn views (e.g. discuss app, thread window, etc.) to update visually - * the unread counter of this thread. - * - * @abstract - * @private - */ - _warnUpdatedUnreadCounter() {}, -}); - -export default AbstractThread; diff --git a/addons/im_livechat/static/src/legacy/models/website_livechat.js b/addons/im_livechat/static/src/legacy/models/website_livechat.js index 2fda5df2dfb..42e346b5411 100644 --- a/addons/im_livechat/static/src/legacy/models/website_livechat.js +++ b/addons/im_livechat/static/src/legacy/models/website_livechat.js @@ -1,15 +1,16 @@ /** @odoo-module **/ -import AbstractThread from '@im_livechat/legacy/models/abstract_thread'; import ThreadTypingMixin from '@im_livechat/legacy/models/thread_typing_mixin'; +import Class from 'web.Class'; import session from 'web.session'; +import Mixins from 'web.mixins'; /** * Thread model that represents a livechat on the website-side. This livechat * is not linked to the mail service. */ -const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { +const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMixin, { /** * @override @@ -26,11 +27,19 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { * @param {string} params.data.name the name of this livechat. * @param {string} [params.data.state] if 'folded', the livechat is folded. * This is ignored if `folded` is provided and is a boolean value. + * @param {string} [params.data.status=''] the status of this thread * @param {string} params.data.uuid the UUID of this livechat. * @param {@im_livechat/legacy/widgets/livechat_button} params.parent */ init(params) { - this._super(...arguments); + Mixins.EventDispatcherMixin.init.call(this, arguments); + this.setParent(params.parent); + + this._folded = false; // threads are unfolded by default + this._id = params.data.id; + this._name = params.data.name; + this._status = params.data.status || ''; + this._unreadCounter = 0; // amount of messages not yet been read ThreadTypingMixin.init.call(this, arguments); this._members = []; @@ -59,26 +68,87 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { // Public //-------------------------------------------------------------------------- + /** + * Add a message to this thread. + * + * @param {@im_livechat/legacy/models/public_livechat_message} message + */ + addMessage(message) { + this.trigger('message_added', message); + }, + /** + * Updates the folded state of the thread + * + * @param {boolean} folded + */ + fold(folded) { + this._folded = folded; + }, + /** + * Get the ID of this thread + * + * @returns {integer|string} + */ + getID() { + return this._id; + }, /** * @override - * @returns {@im_livechat/legacy/models/website_livechat_message[]} + * @returns {@im_livechat/legacy/models/public_livechat_message[]} */ getMessages() { // ignore removed messages return this._messages.filter(message => !message.isEmpty()); }, + /** + * Get the name of this thread. If the name of the thread has been created + * by the user from an input, it may be escaped. + * + * @returns {string} + */ + getName() { + return this._name; + }, /** * @returns {Array} */ getOperatorPID() { return this._operatorPID; }, + /** + * Get the status of the thread (e.g. 'online', 'offline', etc.) + * + * @returns {string} + */ + getStatus() { + return this._status; + }, + /** + * Returns the title to display in thread window's headers. + * + * @returns {string} the name of the thread by default (see @getName) + */ + getTitle() { + return this.getName(); + }, + /** + * @returns {integer} + */ + getUnreadCounter() { + return this._unreadCounter; + }, /** * @returns {string} */ getUUID() { return this._uuid; }, + /** + * @returns {boolean} + */ + hasMessages() { + return !_.isEmpty(this.getMessages()); + }, /** * Increments the unread counter of this livechat by 1 unit. * @@ -89,10 +159,47 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { incrementUnreadCounter() { this._incrementUnreadCounter(); }, + /** + * States whether this thread is folded or not. + * + * @return {boolean} + */ + isFolded() { + return this._folded; + }, + /** + * Mark the thread as read, which resets the unread counter to 0. This is + * only performed if the unread counter is not 0. + * + * @returns {Promise} + */ + markAsRead() { + if (this._unreadCounter > 0) { + return this._markAsRead(); + } + return Promise.resolve(); + }, + /** + * Post a message on this thread + * + * @returns {Promise} resolved with the message object to be sent to the + * server + */ + postMessage() { + return this._postMessage(...arguments) + .then(this.trigger.bind(this, 'message_posted')); + }, + /** + * Resets the unread counter of this thread to 0. + */ + resetUnreadCounter() { + this._unreadCounter = 0; + this._warnUpdatedUnreadCounter(); + }, /** * AKU: hack for the moment * - * @param {@im_livechat/legacy/models/website_livechat_message[]} messages + * @param {@im_livechat/legacy/models/public_livechat_message[]} messages */ setMessages(messages) { this._messages = messages; @@ -115,6 +222,14 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { // Private //-------------------------------------------------------------------------- + /** + * Increments the unread counter of this thread by 1 unit. + * + * @private + */ + _incrementUnreadCounter() { + this._unreadCounter++; + }, /** * @override {mail.model.ThreadTypingMixin} * @private @@ -125,6 +240,16 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { _isTypingMyselfInfo(params) { return params.isWebsiteUser; }, + /** + * Mark the thread as read + * + * @private + * @returns {Promise} + */ + _markAsRead() { + this.resetUnreadCounter(); + return Promise.resolve(); + }, /** * @override {mail.model.ThreadTypingMixin} * @private @@ -138,6 +263,16 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { is_typing: params.typing, }, { shadow: true }); }, + /** + * Post a message on this thread + * + * @private + * @returns {Promise} resolved with the message object to be sent to the + * server + */ + _postMessage() { + return Promise.resolve(); + }, /** * Warn views that the list of users that are currently typing on this * livechat has been updated. @@ -151,7 +286,6 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { /** * Warn that the unread counter has been updated on this livechat * - * @override * @private */ _warnUpdatedUnreadCounter() { @@ -181,4 +315,4 @@ const WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { }, }); -export default WebsiteLivechat; +export default PublicLivechat; diff --git a/addons/im_livechat/static/src/legacy/models/website_livechat_message.js b/addons/im_livechat/static/src/legacy/models/website_livechat_message.js index 051fa90a205..f35889065e2 100644 --- a/addons/im_livechat/static/src/legacy/models/website_livechat_message.js +++ b/addons/im_livechat/static/src/legacy/models/website_livechat_message.js @@ -1,26 +1,55 @@ /** @odoo-module **/ -import AbstractMessage from '@im_livechat/legacy/models/abstract_message'; +import * as mailUtils from '@mail/js/utils'; + +import Class from 'web.Class'; +import { _t } from 'web.core'; +import session from 'web.session'; +import time from 'web.time'; /** * This is a message that is handled by im_livechat, without making use of the * mail.Manager. The purpose of this is to make im_livechat compatible with * mail.widget.Thread. * - * @see @im_livechat/legacy/models/abstract_message for more information. + * @see @im_livechat/legacy/models/public_livechat_message for more information. */ -const WebsiteLivechatMessage = AbstractMessage.extend({ +const PublicLivechatMessage = Class.extend({ /** * @param {@im_livechat/legacy/widgets/livechat_button} parent * @param {Object} data + * @param {Array} [data.attachment_ids=[]] + * @param {Array} [data.author_id] + * @param {string} [data.body = ""] + * @param {string} [data.date] the server-format date time of the message. + * If not provided, use current date time for this message. + * @param {integer} data.id + * @param {boolean} [data.is_discussion = false] + * @param {boolean} [data.is_notification = false] + * @param {string} [data.message_type = undefined] * @param {Object} options * @param {string} options.default_username * @param {string} options.serverURL */ init(parent, data, options) { - this._super(...arguments); + // Attachments are not supported in (public) livechat. + // We ignore data from server, otherwise field commands + // will be wrongly considered as unamed attachments. + this._attachmentIDs = []; + this._body = data.body || ""; + // by default: current datetime + this._date = data.date ? moment(time.str_to_datetime(data.date)) : moment(); + this._id = data.id; + this._isDiscussion = data.is_discussion; + this._isNotification = data.is_notification; + this._serverAuthorID = data.author_id; + this._type = data.message_type || undefined; + this._processAttachmentURL(); + this._attachmentIDs.forEach(function (attachment) { + attachment.filename = attachment.filename || attachment.name || _t("unnamed"); + }); this._defaultUsername = options.default_username; this._serverURL = options.serverURL; }, @@ -29,13 +58,41 @@ const WebsiteLivechatMessage = AbstractMessage.extend({ // Public //-------------------------------------------------------------------------- + /** + * Get the list of files attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getAttachments() { + return this._attachmentIDs; + }, + /** + * Get the server ID (number) of the author of this message + * If there are no author, return -1; + * + * @return {integer} + */ + getAuthorID() { + if (!this.hasAuthor()) { + return -1; + } + return this._serverAuthorID[0]; + }, + /** + * Threads do not have an im status by default + * + * @return {undefined} + */ + getAuthorImStatus() { + return undefined; + }, /** * Get the relative url of the avatar to display next to the message * - * @override * @return {string} */ - getAvatarSource() { + getAvatarSource() { let source = this._serverURL; if (this.hasAuthor()) { source += `/im_livechat/operator/${this.getAuthorID()}/avatar`; @@ -44,6 +101,34 @@ const WebsiteLivechatMessage = AbstractMessage.extend({ } return source; }, + /** + * Get the body content of this message + * + * @return {string} + */ + getBody() { + return this._body; + }, + /** + * @return {moment} + */ + getDate() { + return this._date; + }, + /** + * Get the date day of this message + * + * @return {string} + */ + getDateDay() { + const date = this.getDate().format('YYYY-MM-DD'); + if (date === moment().format('YYYY-MM-DD')) { + return _t("Today"); + } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { + return _t("Yesterday"); + } + return this.getDate().format('LL'); + }, /** * Get the text to display for the author of the message * @@ -51,13 +136,327 @@ const WebsiteLivechatMessage = AbstractMessage.extend({ * * author name > default usernane * - * @override * @return {string} */ getDisplayedAuthor() { - return this._super(...arguments) || this._defaultUsername; + return (this.hasAuthor() ? this._getAuthorName() : null) || this._defaultUsername; + }, + /** + * Get the server ID (number) of this message + * + * @return {integer} + */ + getID() { + return this._id; + }, + /** + * Get the list of images attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getImageAttachments() { + return _.filter(this.getAttachments(), function (attachment) { + return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; + }); + }, + /** + * Get the list of non-images attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getNonImageAttachments() { + return _.difference(this.getAttachments(), this.getImageAttachments()); + }, + /** + * Gets the class to use as the notification icon. + * + * @returns {string} + */ + getNotificationIcon() { + if (!this.hasNotificationsError()) { + return 'fa fa-envelope-o'; + } + return 'fa fa-envelope'; + }, + /** + * Gets the list of notifications of this message, in no specific order. + * By default messages do not have notifications. + * + * @returns {Object[]} + */ + getNotifications() { + return []; + }, + /** + * Gets the text to display next to the notification icon. + * + * @returns {string} + */ + getNotificationText() { + return ''; + }, + /** + * Get the time elapsed between sent message and now + * + * @return {string} + */ + getTimeElapsed() { + return mailUtils.timeFromNow(this.getDate()); + }, + /** + * Get the type of message (e.g. 'comment', 'email', 'notification', ...) + * By default, messages are of type 'undefined' + * + * @return {string|undefined} + */ + getType() { + return this._type; + }, + /** + * State whether this message contains some attachments. + * + * @return {boolean} + */ + hasAttachments() { + return this.getAttachments().length > 0; + }, + /** + * State whether this message has an author + * + * @return {boolean} + */ + hasAuthor() { + return !!(this._serverAuthorID && this._serverAuthorID[0]); + }, + /** + * State whether this message has an email of its sender. + * By default, messages do not have any email of its sender. + * + * @return {string} + */ + hasEmailFrom() { + return false; + }, + /** + * State whether this image contains images attachments + * + * @return {boolean} + */ + hasImageAttachments() { + return _.some(this.getAttachments(), function (attachment) { + return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; + }); + }, + /** + * State whether this image contains non-images attachments + * + * @return {boolean} + */ + hasNonImageAttachments() { + return _.some(this.getAttachments(), function (attachment) { + return !(attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'); + }); + }, + /** + * States whether this message has some notifications. + * + * @returns {boolean} + */ + hasNotifications() { + return this.getNotifications().length > 0; + }, + /** + * States whether this message has notifications that are in error. + * + * @returns {boolean} + */ + hasNotificationsError() { + return this.getNotifications().some(notif => + notif.notification_status === 'exception' || + notif.notification_status === 'bounce' + ); + }, + /** + * State whether this message originates from a channel. + * By default, messages do not originate from a channel. + * + * @return {boolean} + */ + originatesFromChannel() { + return false; + }, + /** + * State whether this message has a subject + * By default, messages do not have any subject. + * + * @return {boolean} + */ + hasSubject() { + return false; + }, + /** + * State whether this message is empty + * + * @return {boolean} + */ + isEmpty() { + return !this.hasTrackingValues() && + !this.hasAttachments() && + !this.getBody(); + }, + /** + * By default, messages do not have any subtype description + * + * @return {boolean} + */ + hasSubtypeDescription() { + return false; + }, + /** + * State whether this message contains some tracking values + * By default, messages do not have any tracking values. + * + * @return {boolean} + */ + hasTrackingValues() { + return false; + }, + /** + * State whether this message is a discussion + * + * @return {boolean} + */ + isDiscussion() { + return this._isDiscussion; + }, + /** + * State whether this message is linked to a document thread + * By default, messages are not linked to a document thread. + * + * @return {boolean} + */ + isLinkedToDocumentThread() { + return false; + }, + /** + * State whether this message is needaction + * By default, messages are not needaction. + * + * @return {boolean} + */ + isNeedaction() { + return false; + }, + /** + * State whether this message is a note (i.e. a message from "Log note") + * + * @return {boolean} + */ + isNote() { + return this._isNote; + }, + /** + * State whether this message is a notification + * + * User notifications are defined as either + * - notes + * - pushed to user Inbox or email through classic notification process + * - not linked to any document, meaning model and res_id are void + * + * This is useful in order to display white background for user + * notifications in chatter + * + * @returns {boolean} + */ + isNotification() { + return this._isNotification; + }, + /** + * State whether this message is starred + * By default, messages are not starred. + * + * @return {boolean} + */ + isStarred() { + return false; + }, + /** + * State whether this message is a system notification + * By default, messages are not system notifications + * + * @return {boolean} + */ + isSystemNotification() { + return false; + }, + /** + * States whether the current message needs moderation in general. + * By default, messages do not require any moderation. + * + * @returns {boolean} + */ + needsModeration() { + return false; + }, + /** + * @params {integer[]} attachmentIDs + */ + removeAttachments(attachmentIDs) { + this._attachmentIDs = _.reject(this._attachmentIDs, function (attachment) { + return _.contains(attachmentIDs, attachment.id); + }); + }, + /** + * State whether this message should redirect to the author + * when clicking on the author of this message. + * + * Do not redirect on author clicked of self-posted messages. + * + * @return {boolean} + */ + shouldRedirectToAuthor() { + return !this._isMyselfAuthor(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Get the name of the author of this message. + * If there are no author of this messages, returns '' (empty string). + * + * @private + * @returns {string} + */ + _getAuthorName() { + if (!this.hasAuthor()) { + return ""; + } + return this._serverAuthorID[1]; + }, + /** + * State whether the current user is the author of this message + * + * @private + * @return {boolean} + */ + _isMyselfAuthor() { + return this.hasAuthor() && (this.getAuthorID() === session.partner_id); + }, + /** + * Compute url of attachments of this message + * + * @private + */ + _processAttachmentURL() { + for (let attachment of this.getAttachments()) { + attachment.url = '/web/content/' + attachment.id + '?download=true'; + } }, }); -export default WebsiteLivechatMessage; +export default PublicLivechatMessage; diff --git a/addons/im_livechat/static/src/legacy/models/website_livechat_window.js b/addons/im_livechat/static/src/legacy/models/website_livechat_window.js index f1c1fb06fa5..38766d75a17 100644 --- a/addons/im_livechat/static/src/legacy/models/website_livechat_window.js +++ b/addons/im_livechat/static/src/legacy/models/website_livechat_window.js @@ -1,26 +1,60 @@ /** @odoo-module **/ -import AbstractThreadWindow from '@im_livechat/legacy/widgets/abstract_thread_window/abstract_thread_window'; +import ThreadWidget from '@im_livechat/legacy/widgets/thread/thread'; + +import config from 'web.config'; +import { _t, qweb } from 'web.core'; +import Widget from 'web.Widget'; /** * This is the widget that represent windows of livechat in the frontend. * * @see @im_livechat/legacy/widgets/abstract_thread_window for more information */ -const LivechatWindow = AbstractThreadWindow.extend({ - events: Object.assign(AbstractThreadWindow.prototype.events, { +const PublicLivechatWindow = Widget.extend({ + FOLD_ANIMATION_DURATION: 200, // duration in ms for (un)fold transition + HEIGHT_OPEN: '400px', // height in px of thread window when open + HEIGHT_FOLDED: '34px', // height, in px, of thread window when folded + template: 'im_livechat.legacy.PublicLivechatWindow', + events: { + 'click .o_thread_window_close': '_onClickClose', + 'click .o_thread_window_title': '_onClickFold', + 'click .o_composer_text_field': '_onComposerClick', + 'click .o_mail_thread': '_onThreadWindowClicked', + 'keydown .o_composer_text_field': '_onKeydown', + 'keypress .o_composer_text_field': '_onKeypress', 'input .o_composer_text_field': '_onInput', - }), + }, + custom_events: { + document_viewer_closed: '_onDocumentViewerClose', + }, /** - * @override - * @param {@im_livechat/legacy/widgets/livechat_button} parent - * @param {@im_livechat/legacy/models/website_livechat} thread + * @param {Widget} parent + * @param {@im_livechat/legacy/models/public_livechat} thread * @param {Object} [options={}] * @param {string} [options.headerBackgroundColor] * @param {string} [options.titleColor] */ - init(parent, thread, options = {}) { - this._super(...arguments); + init(parent, thread, options) { + this._super(parent); + + this.options = _.defaults(options || {}, { + autofocus: true, + displayStars: true, + displayReplyIcons: false, + displayNotificationIcons: false, + placeholder: _t("Say something"), + }); + + this._hidden = false; + this._thread = thread || null; + + this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100); + + if (!this.hasThread()) { + // internal fold state of thread window without any thread + this._folded = false; + } this._thread = thread; }, /** @@ -28,7 +62,32 @@ const LivechatWindow = AbstractThreadWindow.extend({ * @return {Promise} */ async start() { - await this._super(...arguments); + this.$input = this.$('.o_composer_text_field'); + this.$header = this.$('.o_thread_window_header'); + const options = { + displayMarkAsRead: false, + displayStars: this.options.displayStars, + }; + if (this._thread && this._thread._type === 'document_thread') { + options.displayDocumentLinks = false; + } + this._threadWidget = new ThreadWidget(this, options); + + // animate the (un)folding of thread windows + this.$el.css({ transition: 'height ' + this.FOLD_ANIMATION_DURATION + 'ms linear' }); + if (this.isFolded()) { + this.$el.css('height', this.HEIGHT_FOLDED); + } else if (this.options.autofocus) { + this._focusInput(); + } + if (!config.device.isMobile) { + const margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; + this.$el.css(margin_dir, $.position.scrollbarWidth()); + } + const def = this._threadWidget.replace(this.$('.o_thread_window_content')).then(() => { + this._threadWidget.$el.on('scroll', this, this._debouncedOnScroll); + }); + await Promise.all([this._super(), def]); if (this.options.headerBackgroundColor) { this.$('.o_thread_window_header').css('background-color', this.options.headerBackgroundColor); } @@ -36,6 +95,27 @@ const LivechatWindow = AbstractThreadWindow.extend({ this.$('.o_thread_window_header').css('color', this.options.titleColor); } }, + /** + * @override + */ + do_hide() { + this._hidden = true; + this._super(...arguments); + }, + /** + * @override + */ + do_show() { + this._hidden = false; + this._super(...arguments); + }, + /** + * @override + */ + do_toggle(display) { + this._hidden = _.isBoolean(display) ? !display : !this._hidden; + this._super(...arguments); + }, //-------------------------------------------------------------------------- @@ -48,6 +128,142 @@ const LivechatWindow = AbstractThreadWindow.extend({ close() { this.trigger_up('close_chat_window'); }, + /** + * States whether this thread window is related to a thread or not. + * + * This is useful in order to provide specific behaviour for thread windows + * without any thread, e.g. let them open a thread from this "blank" thread + * window. + * + * @returns {boolean} + */ + hasThread() { + return !! this._thread; + }, + /** + * Get the ID of the thread window, which is equivalent to the ID of the + * thread related to this window + * + * @returns {integer|string} + */ + getID() { + return this._getThreadID(); + }, + /** + * @returns {mail.model.Thread|undefined} + */ + getThread() { + if (!this.hasThread()) { + return undefined; + } + return this._thread; + }, + /** + * Get the status of the thread, such as the im status of a DM chat + * ('online', 'offline', etc.). If this window has no thread, returns + * `undefined`. + * + * @returns {string|undefined} + */ + getThreadStatus() { + if (!this.hasThread()) { + return undefined; + } + return this._thread.getStatus(); + }, + /** + * Get the title of the thread window, which usually contains the name of + * the thread. + * + * @returns {string} + */ + getTitle() { + if (!this.hasThread()) { + return _t("Undefined"); + } + return this._thread.getTitle(); + }, + /** + * Get the unread counter of the related thread. If there are no thread + * linked to this window, returns 0. + * + * @returns {integer} + */ + getUnreadCounter() { + if (!this.hasThread()) { + return 0; + } + return this._thread.getUnreadCounter(); + }, + /** + * Tells whether the bottom of the thread in the thread window is visible + * or not. + * + * @returns {boolean} + */ + isAtBottom() { + return this._threadWidget.isAtBottom(); + }, + /** + * State whether the related thread is folded or not. If there are no + * thread related to this window, it means this is the "blank" thread + * window, therefore we use the internal folded state. + * + * @returns {boolean} + */ + isFolded() { + if (!this.hasThread()) { + return this._folded; + } + return this._thread.isFolded(); + }, + /** + * States whether the thread window is hidden or not. + * + * @returns {boolean} + */ + isHidden() { + return this._hidden; + }, + /** + * States whether the current environment is in mobile or not. This is + * useful in order to customize the template rendering for mobile view. + * + * @returns {boolean} + */ + isMobile() { + return config.device.isMobile; + }, + /** + * States whether the input of the thread window should be displayed or not. + * By default, any thread window with a thread needs a composer. + * + * @returns {boolean} + */ + needsComposer() { + return this.hasThread(); + }, + /** + * Render the thread window + */ + render() { + this.renderHeader(); + if (this.hasThread()) { + this._threadWidget.render(this._thread, { displayLoadMore: false }); + } + }, + /** + * Render the header of this thread window. + * This is useful when some information on the header have be updated such + * as the status or the title of the thread that have changed. + * + * @private + */ + renderHeader() { + const options = this._getHeaderRenderingOptions(); + this.$header.html( + qweb.render('im_livechat.legacy.PublicLivechatWindow.HeaderContent', options)); + }, /** * Replace the thread content with provided new content * @@ -57,35 +273,194 @@ const LivechatWindow = AbstractThreadWindow.extend({ $element.replace(this._threadWidget.$el); }, /** + * Scroll to the bottom of the thread in the thread window + */ + scrollToBottom() { + this._threadWidget.scrollToBottom(); + }, + /** + * Toggle the fold state of this thread window. Also update the fold state + * of the thread model. If the boolean parameter `folded` is provided, it + * folds/unfolds the window when it is set/unset. + * * Warn the parent widget (LivechatButton) * - * @override - * @param {boolean} folded + * @param {boolean} [folded] if not a boolean, toggle the fold state. + * Otherwise, fold/unfold the window if set/unset. */ - toggleFold() { - this._super(...arguments); + toggleFold(folded) { + if (!_.isBoolean(folded)) { + folded = !this.isFolded(); + } + this._updateThreadFoldState(folded); this.trigger_up('save_chat_window'); this.updateVisualFoldState(); }, + /** + * Update the visual state of the window so that it matched the internal + * fold state. This is useful in case the related thread has its fold state + * that has been changed. + */ + updateVisualFoldState() { + if (!this.isFolded()) { + this._threadWidget.scrollToBottom(); + if (this.options.autofocus) { + this._focusInput(); + } + } + const height = this.isFolded() ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; + this.$el.css({ height }); + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** - * @override + * Set the focus on the composer of the thread window. This operation is + * ignored in mobile context. + * + * @private + * Set the focus on the input of the window + */ + _focusInput() { + if ( + config.device.touch && + config.device.size_class <= config.device.SIZES.SM + ) { + return; + } + this.$input.focus(); + }, + /** + * Returns the options used by the rendering of the window's header + * + * @private + * @returns {Object} + */ + _getHeaderRenderingOptions() { + return { + status: this.getThreadStatus(), + thread: this.getThread(), + title: this.getTitle(), + unreadCounter: this.getUnreadCounter(), + widget: this, + }; + }, + /** + * Get the ID of the related thread. + * If this window is not related to a thread, it means this is the "blank" + * thread window, therefore it returns "_blank" as its ID. + * + * @private + * @returns {integer|string} the threadID, or '_blank' for the window that + * is not related to any thread. + */ + _getThreadID() { + if (!this.hasThread()) { + return '_blank'; + } + return this._thread.getID(); + }, + /** + * Tells whether there is focus on this thread. Note that a thread that has + * the focus means the input has focus. + * + * @private + * @returns {boolean} + */ + _hasFocus() { + return this.$input.is(':focus'); + }, + /** + * Post a message on this thread window, and auto-scroll to the bottom of + * the thread. + * * @private * @param {Object} messageData */ _postMessage(messageData) { this.trigger_up('post_message_chat_window', { messageData }); - this._super(...arguments); + if (!this.hasThread()) { + return; + } + this._thread.postMessage(messageData) + .then(() => { + this._threadWidget.scrollToBottom(); + }); + }, + /** + * Update the fold state of the thread. + * + * This function is called when toggling the fold state of this window. + * If there is no thread linked to this window, it means this is the + * "blank" thread window, therefore we use the internal state 'folded' + * + * @private + * @param {boolean} folded + */ + _updateThreadFoldState(folded) { + if (this.hasThread()) { + this._thread.fold(folded); + } else { + this._folded = folded; + this.updateVisualFoldState(); + } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- + /** + * Close the thread window. + * Mark the thread as read if the thread window was open. + * + * @private + * @param {MouseEvent} ev + */ + _onClickClose(ev) { + ev.stopPropagation(); + ev.preventDefault(); + if ( + this.hasThread() && + this._thread.getUnreadCounter() > 0 && + !this.isFolded() + ) { + this._thread.markAsRead(); + } + this.close(); + }, + /** + * Fold/unfold the thread window. + * Also mark the thread as read. + * + * @private + */ + _onClickFold() { + if (!config.device.isMobile) { + this.toggleFold(); + } + }, + /** + * Called when the composer is clicked -> forces focus on input even if + * jquery's blockUI is enabled. + * + * @private + * @param {Event} ev + */ + _onComposerClick(ev) { + if ($(ev.target).closest('a, button').length) { + return; + } + this._focusInput(); + }, + /** + * @private + */ + _onDocumentViewerClose() { + this._focusInput(); + }, /** * Called when the input in the composer changes * @@ -97,6 +472,55 @@ const LivechatWindow = AbstractThreadWindow.extend({ this._thread.setMyselfTyping({ typing: isTyping }); } }, + /** + * Called when typing something on the composer of this thread window. + * + * @private + * @param {KeyboardEvent} ev + */ + _onKeydown(ev) { + ev.stopPropagation(); // to prevent jquery's blockUI to cancel event + // ENTER key (avoid requiring jquery ui for external livechat) + if (ev.which === 13) { + const content = _.str.trim(this.$input.val()); + const messageData = { + content, + attachment_ids: [], + partner_ids: [], + }; + this.$input.val(''); + if (content) { + this._postMessage(messageData); + } + } + }, + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeypress(ev) { + ev.stopPropagation(); // to prevent jquery's blockUI to cancel event + }, + /** + * @private + */ + _onScroll() { + if (this.hasThread() && this.isAtBottom()) { + this._thread.markAsRead(); + } + }, + /** + * When a thread window is clicked on, we want to give the focus to the main + * input. An exception is made when the user is selecting something. + * + * @private + */ + _onThreadWindowClicked() { + const selectObj = window.getSelection(); + if (selectObj.anchorOffset === selectObj.focusOffset) { + this.$input.focus(); + } + }, }); -export default LivechatWindow; +export default PublicLivechatWindow; 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 914488e002b..a24785e8454 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js @@ -85,7 +85,6 @@ const QWeb = core.qweb; utils.set_cookie('im_livechat_auto_popup', '', -1); this.messaging.livechatButtonView.update({ history: clear() }); this.messaging.livechatButtonView.update({ rule: this.messaging.livechatButtonView.livechatInit.rule }); - this.messaging.livechatButtonView.update({ isChatbotBatchWelcomeMessages: true }); } else if (this.messaging.livechatButtonView.chatbotState === 'restore_session') { // we landed on a website page and a chatbot script is currently running // -> restore the user's session (see '_chatbotRestoreSession') @@ -166,7 +165,7 @@ const QWeb = core.qweb; const postedWelcomeMessages = await session.rpc('/chatbot/post_welcome_steps', { channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), - chatbot_script_id: this.messaging.livechatButtonView.chatbot.data.chatbot_script_id, + chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); const welcomeMessagesIds = welcomeMessages.map(welcomeMessage => welcomeMessage._id); @@ -367,7 +366,6 @@ const QWeb = core.qweb; let chatbotState = localStorage.getItem(this.messaging.livechatButtonView.sessionCookieKey); if (chatbotState) { - this.messaging.livechatButtonView.update({ localStorageChatbotState: JSON.parse(chatbotState) }); this.messaging.livechatButtonView.chatbot.update({ currentStep: insertAndReplace({ data: this.messaging.livechatButtonView.localStorageChatbotState._chatbotCurrentStep }) }); } }, @@ -516,7 +514,7 @@ const QWeb = core.qweb; const nextStep = await session.rpc('/chatbot/step/trigger', { channel_uuid:this.messaging.livechatButtonView.livechat.getUUID(), - chatbot_script_id: this.messaging.livechatButtonView.chatbot.data.chatbot_script_id, + chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); if (nextStep) { @@ -625,7 +623,7 @@ const QWeb = core.qweb; const parameters = this._super(...arguments); if (this.messaging.livechatButtonView.isChatbot) { - parameters.chatbot_script_id = this.messaging.livechatButtonView.chatbot.data.chatbot_script_id; + parameters.chatbot_script_id = this.messaging.livechatButtonView.chatbot.scriptId; } return parameters; @@ -720,7 +718,7 @@ const QWeb = core.qweb; if (this.messaging.livechatButtonView.isChatbot) { this._sendWelcomeChatbotMessage( 0, - this.messaging.livechatButtonView.isChatbotBatchWelcomeMessages ? 0 : this.messaging.livechatButtonView.chatbot.messageDelay, + this.messaging.livechatButtonView.chatbotState === 'welcome' ? 0 : this.messaging.livechatButtonView.chatbot.messageDelay, ); } else { this._super(...arguments); @@ -815,7 +813,7 @@ const QWeb = core.qweb; const postedMessage = await session.rpc('/chatbot/restart', { channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), - chatbot_script_id: this.messaging.livechatButtonView.chatbot.data.chatbot_script_id, + chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); if (postedMessage) { 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 150a2397b64..6dcc46c5e0e 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.xml @@ -8,7 +8,7 @@ --> - +