diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 2f723e75b70..ab36951cc6d 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -52,11 +52,7 @@ Help your customers with this chat, and analyse their feedback. 'web.assets_frontend': [ 'im_livechat/static/src/public/main.js', 'im_livechat/static/src/services/*.js', - 'im_livechat/static/src/legacy/public_livechat_constants.js', 'im_livechat/static/src/legacy/public_livechat_history_tracking.js', - 'im_livechat/static/src/legacy/models/*', - 'im_livechat/static/src/legacy/widgets/*', - 'im_livechat/static/src/legacy/widgets/*/*', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', 'im_livechat/static/src/legacy/public_livechat_message_chatbot.js', 'im_livechat/static/src/legacy/public_livechat.scss', @@ -79,11 +75,7 @@ Help your customers with this chat, and analyse their feedback. ], 'web.qunit_suite_tests': [ 'im_livechat/static/tests/qunit_suite_tests/components/**/*.js', - 'im_livechat/static/src/legacy/public_livechat_constants.js', 'im_livechat/static/src/legacy/public_livechat_history_tracking.js', - 'im_livechat/static/src/legacy/models/*', - 'im_livechat/static/src/legacy/widgets/*', - 'im_livechat/static/src/legacy/widgets/*/*', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', 'im_livechat/static/src/legacy/public_livechat_message_chatbot.js', ], @@ -94,6 +86,10 @@ Help your customers with this chat, and analyse their feedback. 'im_livechat/static/src/components/*/*.xml', ], 'mail.assets_messaging': [ + 'im_livechat/static/src/legacy/models/*', + 'im_livechat/static/src/legacy/public_livechat_constants.js', + 'im_livechat/static/src/legacy/widgets/*', + 'im_livechat/static/src/legacy/widgets/*/*', 'im_livechat/static/src/models/*.js', ], # Bundle of External Librairies of the Livechat (Odoo + required modules) @@ -190,11 +186,7 @@ Help your customers with this chat, and analyse their feedback. 'bus/static/src/js/crosstab_bus.js', 'bus/static/src/js/services/bus_service.js', 'mail/static/src/js/utils.js', - 'im_livechat/static/src/legacy/public_livechat_constants.js', 'im_livechat/static/src/legacy/public_livechat_history_tracking.js', - 'im_livechat/static/src/legacy/models/*', - 'im_livechat/static/src/legacy/widgets/*', - 'im_livechat/static/src/legacy/widgets/*/*', 'im_livechat/static/src/legacy/public_livechat_chatbot.js', 'im_livechat/static/src/legacy/public_livechat_message_chatbot.js', diff --git a/addons/im_livechat/static/src/legacy/models/public_livechat.js b/addons/im_livechat/static/src/legacy/models/public_livechat.js index 42e346b5411..4f1b70872ab 100644 --- a/addons/im_livechat/static/src/legacy/models/public_livechat.js +++ b/addons/im_livechat/static/src/legacy/models/public_livechat.js @@ -1,17 +1,20 @@ /** @odoo-module **/ -import ThreadTypingMixin from '@im_livechat/legacy/models/thread_typing_mixin'; +import CCThrottleFunction from '@im_livechat/legacy/models/cc_throttle_function'; +import Timer from '@im_livechat/legacy/models/timer'; +import Timers from '@im_livechat/legacy/models/timers'; import Class from 'web.Class'; +import { _t } from 'web.core'; import session from 'web.session'; import Mixins from 'web.mixins'; +import { sprintf } from 'web.utils'; /** * Thread model that represents a livechat on the website-side. This livechat * is not linked to the mail service. */ -const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMixin, { - +const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, { /** * @override * @private @@ -40,10 +43,83 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix 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 = []; + /** + * Initialize the internal data for typing feature on threads. + * + * Also listens on some internal events of the thread: + * + * - 'message_added': when a message is added, remove the author in the + * typing partners. + * - 'message_posted': when a message is posted, let the user have the + * possibility to immediately notify if he types something right away, + * instead of waiting for a throttle behaviour. + */ + + // Store the last "myself typing" status that has been sent to the + // server. This is useful in order to not notify the same typing + // status multiple times. + this._lastNotifiedMyselfTyping = false; + + // Timer of current user that is typing a very long text. When the + // receivers do not receive any typing notification for a long time, + // they assume that the related partner is no longer typing + // something (e.g. they have closed the browser tab). + // This is a timer to let others know that we are still typing + // something, so that they do not assume we stopped typing + // something. + this._myselfLongTypingTimer = new Timer({ + duration: 50 * 1000, + onTimeout: this._onMyselfLongTypingTimeout.bind(this), + }); + + // Timer of current user that was currently typing something, but + // there is no change on the input for several time. This is used + // in order to automatically notify other users that we have stopped + // typing something, due to making no changes on the composer for + // some time. + this._myselfTypingInactivityTimer = new Timer({ + duration: 5 * 1000, + onTimeout: this._onMyselfTypingInactivityTimeout.bind(this), + }); + + // Timers of users currently typing in the thread. This is useful + // in order to automatically unregister typing users when we do not + // receive any typing notification after a long time. Timers are + // internally indexed by partnerID. The current user is ignored in + // this list of timers. + this._othersTypingTimers = new Timers({ + duration: 60 * 1000, + onTimeout: this._onOthersTypingTimeout.bind(this), + }); + + // Clearable and cancellable throttled version of the + // `doNotifyMyselfTyping` method. (basically `notifyMyselfTyping` + // with slight pre- and post-processing) + // @see {mail.model.ResetableThrottleFunction} + // This is useful when the user posts a message and types something + // else: he must notify immediately that he is typing something, + // instead of waiting for the throttle internal timer. + this._throttleNotifyMyselfTyping = CCThrottleFunction({ + duration: 2.5 * 1000, + func: this._onNotifyMyselfTyping.bind(this), + }); + + // This is used to track the order of registered partners typing + // something, in order to display the oldest typing partners. + this._typingPartnerIDs = []; + + this.on('message_added', this, this._onTypingMessageAdded); + this.on('message_posted', this, this._onTypingMessagePosted); + + this._operatorPID = params.data.operator_pid; + // Necessary for thread typing mixin to display is typing notification + // bar text (at least, for the operator in the members). + this._members = [{ + id: this._operatorPID[0], + name: this._operatorPID[1] + }]; this._uuid = params.data.uuid; if (params.data.message_unread_counter !== undefined) { @@ -55,13 +131,6 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix } else { this._folded = params.data.state === 'folded'; } - - // Necessary for thread typing mixin to display is typing notification - // bar text (at least, for the operator in the members). - this._members.push({ - id: this._operatorPID[0], - name: this._operatorPID[1] - }); }, //-------------------------------------------------------------------------- @@ -84,14 +153,6 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix fold(folded) { this._folded = folded; }, - /** - * Get the ID of this thread - * - * @returns {integer|string} - */ - getID() { - return this._id; - }, /** * @override * @returns {@im_livechat/legacy/models/public_livechat_message[]} @@ -101,47 +162,53 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix 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. + * Get the text to display when some partners are typing something on the + * thread: * - * @returns {string} - */ - getName() { - return this._name; - }, - /** - * @returns {Array} - */ - getOperatorPID() { - return this._operatorPID; - }, - /** - * Get the status of the thread (e.g. 'online', 'offline', etc.) + * - single typing partner: * - * @returns {string} - */ - getStatus() { - return this._status; - }, - /** - * Returns the title to display in thread window's headers. + * A is typing... * - * @returns {string} the name of the thread by default (see @getName) + * - two typing partners: + * + * A and B are typing... + * + * - three or more typing partners: + * + * A, B and more are typing... + * + * The choice of the members name for display is not random: it displays + * the user that have been typing for the longest time. Also, this function + * is hard-coded to display at most 2 partners. This limitation comes from + * how translation works in Odoo, for which unevaluated string cannot be + * translated. + * + * @returns {string} list of members that are typing something on the thread + * (excluding the current user). */ - getTitle() { - return this.getName(); - }, - /** - * @returns {integer} - */ - getUnreadCounter() { - return this._unreadCounter; - }, - /** - * @returns {string} - */ - getUUID() { - return this._uuid; + getTypingMembersToText() { + const typingPartnerIDs = this._typingPartnerIDs; + const typingMembers = _.filter(this._members, function (member) { + return _.contains(typingPartnerIDs, member.id); + }); + const sortedTypingMembers = _.sortBy(typingMembers, function (member) { + return _.indexOf(typingPartnerIDs, member.id); + }); + const displayableTypingMembers = sortedTypingMembers.slice(0, 3); + + if (displayableTypingMembers.length === 0) { + return ''; + } else if (displayableTypingMembers.length === 1) { + return sprintf(_t("%s is typing..."), displayableTypingMembers[0].name); + } else if (displayableTypingMembers.length === 2) { + return sprintf(_t("%s and %s are typing..."), + displayableTypingMembers[0].name, + displayableTypingMembers[1].name); + } else { + return sprintf(_t("%s, %s and more are typing..."), + displayableTypingMembers[0].name, + displayableTypingMembers[1].name); + } }, /** * @returns {boolean} @@ -150,22 +217,13 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix return !_.isEmpty(this.getMessages()); }, /** - * Increments the unread counter of this livechat by 1 unit. + * Tells if someone other than current user is typing something on this + * thread. * - * Note: this public method makes sense because the management of messages - * for website livechat is external. This method should be dropped when - * this class handles messages by itself. + * @returns {boolean} */ - incrementUnreadCounter() { - this._incrementUnreadCounter(); - }, - /** - * States whether this thread is folded or not. - * - * @return {boolean} - */ - isFolded() { - return this._folded; + isSomeoneTyping() { + return !(_.isEmpty(this._typingPartnerIDs)); }, /** * Mark the thread as read, which resets the unread counter to 0. This is @@ -175,7 +233,9 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix */ markAsRead() { if (this._unreadCounter > 0) { - return this._markAsRead(); + this._unreadCounter = 0; + this.trigger_up('updated_unread_counter'); + return Promise.resolve(); } return Promise.resolve(); }, @@ -185,134 +245,216 @@ const PublicLivechat = Class.extend(Mixins.EventDispatcherMixin, ThreadTypingMix * @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')); + async postMessage() { + this.trigger('message_posted'); }, /** - * Resets the unread counter of this thread to 0. - */ - resetUnreadCounter() { - this._unreadCounter = 0; - this._warnUpdatedUnreadCounter(); - }, - /** - * AKU: hack for the moment + * Register someone that is currently typing something in this thread. + * If this is the current user that is typing something, don't do anything + * (we do not have to display anything) * - * @param {@im_livechat/legacy/models/public_livechat_message[]} messages + * This method is ignored if we try to register the current user. + * + * @param {Object} params + * @param {integer} params.partnerID ID of the partner linked to the user + * currently typing something on the thread. */ - setMessages(messages) { - this._messages = messages; + registerTyping(params) { + if (params.isWebsiteUser) { + return; + } + const partnerID = params.partnerID; + this._othersTypingTimers.registerTimer({ + timeoutCallbackArguments: [partnerID], + timerID: partnerID, + }); + if (_.contains(this._typingPartnerIDs, partnerID)) { + return; + } + this._typingPartnerIDs.push(partnerID); + this._warnUpdatedTypingPartners(); + }, + /** + * This method must be called when the user starts or stops typing something + * in the composer of the thread. + * + * @param {Object} params + * @param {boolean} params.typing tell whether the current is typing or not. + */ + setMyselfTyping(params) { + const typing = params.typing; + if (this._lastNotifiedMyselfTyping === typing) { + this._throttleNotifyMyselfTyping.cancel(); + } else { + this._throttleNotifyMyselfTyping(params); + } + + if (typing) { + this._myselfTypingInactivityTimer.reset(); + } else { + this._myselfTypingInactivityTimer.clear(); + } }, /** * @returns {Object} */ toData() { return { - folded: this.isFolded(), - id: this.getID(), - message_unread_counter: this.getUnreadCounter(), - operator_pid: this.getOperatorPID(), - name: this.getName(), - uuid: this.getUUID(), + folded: this._folded, + id: this._id, + message_unread_counter: this._unreadCounter, + operator_pid: this._operatorPID, + name: this._name, + uuid: this._uuid, }; }, + /** + * Unregister someone from currently typing something in this thread. + * + * @param {Object} params + * @param {integer} params.partnerID ID of the partner related to the user + * that is currently typing something + */ + unregisterTyping(params) { + const partnerID = params.partnerID; + this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); + if (!_.contains(this._typingPartnerIDs, partnerID)) { + return; + } + this._typingPartnerIDs = _.reject(this._typingPartnerIDs, function (id) { + return id === partnerID; + }); + this._warnUpdatedTypingPartners(); + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** - * Increments the unread counter of this thread by 1 unit. + * Notify to the server that the current user either starts or stops typing + * something. * * @private - */ - _incrementUnreadCounter() { - this._unreadCounter++; - }, - /** - * @override {mail.model.ThreadTypingMixin} - * @private * @param {Object} params - * @param {boolean} params.isWebsiteUser - * @returns {boolean} - */ - _isTypingMyselfInfo(params) { - return params.isWebsiteUser; - }, - /** - * Mark the thread as read - * - * @private - * @returns {Promise} - */ - _markAsRead() { - this.resetUnreadCounter(); - return Promise.resolve(); - }, - /** - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {Object} params - * @param {boolean} params.typing - * @returns {Promise} + * @param {boolean} params.typing whether we are typing something or not + * @returns {Promise} resolved if the server is notified, rejected + * otherwise */ _notifyMyselfTyping(params) { return session.rpc('/im_livechat/notify_typing', { - uuid: this.getUUID(), + uuid: this._uuid, 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. * - * @override {mail.model.ThreadTypingMixin} * @private */ _warnUpdatedTypingPartners() { this.trigger_up('updated_typing_partners'); }, - /** - * Warn that the unread counter has been updated on this livechat - * - * @private - */ - _warnUpdatedUnreadCounter() { - this.trigger_up('updated_unread_counter'); - }, //-------------------------------------------------------------------------- // Handler //-------------------------------------------------------------------------- /** - * Override so that it only unregister typing operators. + * Called when current user is typing something for a long time. In order + * to not let other users assume that we are no longer typing something, we + * must notify again that we are typing something. + * + * @private + */ + _onMyselfLongTypingTimeout() { + this._throttleNotifyMyselfTyping.clear(); + this._throttleNotifyMyselfTyping({ typing: true }); + }, + /** + * Called when current user has something typed in the composer, but is + * inactive for some time. In this case, he automatically notifies that he + * is no longer typing something + * + * @private + */ + _onMyselfTypingInactivityTimeout() { + this._throttleNotifyMyselfTyping.clear(); + this._throttleNotifyMyselfTyping({ typing: false }); + }, + /** + * Called by throttled version of notify myself typing + * + * Notify to the server that the current user either starts or stops typing + * something. Remember last notified stuff from the server, and update + * related typing timers. + * + * @private + * @param {Object} params + * @param {boolean} params.typing whether we are typing something or not. + */ + _onNotifyMyselfTyping(params) { + const typing = params.typing; + this._lastNotifiedMyselfTyping = typing; + this._notifyMyselfTyping(params); + if (typing) { + this._myselfLongTypingTimer.reset(); + } else { + this._myselfLongTypingTimer.clear(); + } + }, + /** + * Called when current user do not receive a typing notification of someone + * else typing for a long time. In this case, we assume that this person is + * no longer typing something. + * + * @private + * @param {integer} partnerID partnerID of the person we assume he is no + * longer typing something. + */ + _onOthersTypingTimeout(partnerID) { + this.unregisterTyping({ partnerID }); + }, + /** + * Called when a new message is added to the thread + * On receiving a message from a typing partner, unregister this partner + * from typing partners (otherwise, it will still display it until timeout). + * + * Note that it only unregister typing operators. * * Note that in the frontend, there is no way to identify a message that is * from the current user, because there is no partner ID in the session and * a message with an author sets the partner ID of the author. * - * @override {mail.model.ThreadTypingMixin} * @private * @param {mail.model.AbstractMessage} message */ - _onTypingMessageAdded(message) { - const operatorID = this.getOperatorPID()[0]; + _onTypingMessageAdded(message) { + const operatorID = this._operatorPID[0]; if (message.hasAuthor() && message.getAuthorID() === operatorID) { this.unregisterTyping({ partnerID: operatorID }); } }, + /** + * Called when current user has posted a message on this thread. + * + * The current user receives the possibility to immediately notify the + * other users if he is typing something else. + * + * Refresh the context for the current user to notify that he starts or + * stops typing something. In other words, when this function is called and + * then the current user types something, it immediately notifies the + * server as if it is the first time he is typing something. + * + * @private + */ + _onTypingMessagePosted() { + this._lastNotifiedMyselfTyping = false; + this._throttleNotifyMyselfTyping.clear(); + this._myselfLongTypingTimer.clear(); + this._myselfTypingInactivityTimer.clear(); + }, }); export default PublicLivechat; diff --git a/addons/im_livechat/static/src/legacy/models/thread_typing_mixin.js b/addons/im_livechat/static/src/legacy/models/thread_typing_mixin.js deleted file mode 100644 index 4ca338e44a3..00000000000 --- a/addons/im_livechat/static/src/legacy/models/thread_typing_mixin.js +++ /dev/null @@ -1,358 +0,0 @@ -/** @odoo-module **/ - -import CCThrottleFunction from '@im_livechat/legacy/models/cc_throttle_function'; -import Timer from '@im_livechat/legacy/models/timer'; -import Timers from '@im_livechat/legacy/models/timers'; - -import core from 'web.core'; -import { sprintf } from 'web.utils'; - -const _t = core._t; - -/** - * Mixin for enabling the "is typing..." notification on a type of thread. - */ -const ThreadTypingMixin = { - // Default partner infos - _DEFAULT_TYPING_PARTNER_ID: '_default', - _DEFAULT_TYPING_PARTNER_NAME: 'Someone', - - /** - * Initialize the internal data for typing feature on threads. - * - * Also listens on some internal events of the thread: - * - * - 'message_added': when a message is added, remove the author in the - * typing partners. - * - 'message_posted': when a message is posted, let the user have the - * possibility to immediately notify if he types something right away, - * instead of waiting for a throttle behaviour. - */ - init() { - // Store the last "myself typing" status that has been sent to the - // server. This is useful in order to not notify the same typing - // status multiple times. - this._lastNotifiedMyselfTyping = false; - - // Timer of current user that is typing a very long text. When the - // receivers do not receive any typing notification for a long time, - // they assume that the related partner is no longer typing - // something (e.g. they have closed the browser tab). - // This is a timer to let others know that we are still typing - // something, so that they do not assume we stopped typing - // something. - this._myselfLongTypingTimer = new Timer({ - duration: 50 * 1000, - onTimeout: this._onMyselfLongTypingTimeout.bind(this), - }); - - // Timer of current user that was currently typing something, but - // there is no change on the input for several time. This is used - // in order to automatically notify other users that we have stopped - // typing something, due to making no changes on the composer for - // some time. - this._myselfTypingInactivityTimer = new Timer({ - duration: 5 * 1000, - onTimeout: this._onMyselfTypingInactivityTimeout.bind(this), - }); - - // Timers of users currently typing in the thread. This is useful - // in order to automatically unregister typing users when we do not - // receive any typing notification after a long time. Timers are - // internally indexed by partnerID. The current user is ignored in - // this list of timers. - this._othersTypingTimers = new Timers({ - duration: 60 * 1000, - onTimeout: this._onOthersTypingTimeout.bind(this), - }); - - // Clearable and cancellable throttled version of the - // `doNotifyMyselfTyping` method. (basically `notifyMyselfTyping` - // with slight pre- and post-processing) - // @see {mail.model.ResetableThrottleFunction} - // This is useful when the user posts a message and types something - // else: he must notify immediately that he is typing something, - // instead of waiting for the throttle internal timer. - this._throttleNotifyMyselfTyping = CCThrottleFunction({ - duration: 2.5 * 1000, - func: this._onNotifyMyselfTyping.bind(this), - }); - - // This is used to track the order of registered partners typing - // something, in order to display the oldest typing partners. - this._typingPartnerIDs = []; - - this.on('message_added', this, this._onTypingMessageAdded); - this.on('message_posted', this, this._onTypingMessagePosted); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the text to display when some partners are typing something on the - * thread: - * - * - single typing partner: - * - * A is typing... - * - * - two typing partners: - * - * A and B are typing... - * - * - three or more typing partners: - * - * A, B and more are typing... - * - * The choice of the members name for display is not random: it displays - * the user that have been typing for the longest time. Also, this function - * is hard-coded to display at most 2 partners. This limitation comes from - * how translation works in Odoo, for which unevaluated string cannot be - * translated. - * - * @returns {string} list of members that are typing something on the thread - * (excluding the current user). - */ - getTypingMembersToText() { - const typingPartnerIDs = this._typingPartnerIDs; - const typingMembers = _.filter(this._members, function (member) { - return _.contains(typingPartnerIDs, member.id); - }); - const sortedTypingMembers = _.sortBy(typingMembers, function (member) { - return _.indexOf(typingPartnerIDs, member.id); - }); - const displayableTypingMembers = sortedTypingMembers.slice(0, 3); - - if (displayableTypingMembers.length === 0) { - return ''; - } else if (displayableTypingMembers.length === 1) { - return sprintf(_t("%s is typing..."), displayableTypingMembers[0].name); - } else if (displayableTypingMembers.length === 2) { - return sprintf(_t("%s and %s are typing..."), - displayableTypingMembers[0].name, - displayableTypingMembers[1].name); - } else { - return sprintf(_t("%s, %s and more are typing..."), - displayableTypingMembers[0].name, - displayableTypingMembers[1].name); - } - }, - /** - * Threads with this mixin have the typing notification feature - * - * @returns {boolean} - */ - hasTypingNotification() { - return true; - }, - /** - * Tells if someone other than current user is typing something on this - * thread. - * - * @returns {boolean} - */ - isSomeoneTyping() { - return !(_.isEmpty(this._typingPartnerIDs)); - }, - /** - * Register someone that is currently typing something in this thread. - * If this is the current user that is typing something, don't do anything - * (we do not have to display anything) - * - * This method is ignored if we try to register the current user. - * - * @param {Object} params - * @param {integer} params.partnerID ID of the partner linked to the user - * currently typing something on the thread. - */ - registerTyping(params) { - if (this._isTypingMyselfInfo(params)) { - return; - } - const partnerID = params.partnerID; - this._othersTypingTimers.registerTimer({ - timeoutCallbackArguments: [partnerID], - timerID: partnerID, - }); - if (_.contains(this._typingPartnerIDs, partnerID)) { - return; - } - this._typingPartnerIDs.push(partnerID); - this._warnUpdatedTypingPartners(); - }, - /** - * This method must be called when the user starts or stops typing something - * in the composer of the thread. - * - * @param {Object} params - * @param {boolean} params.typing tell whether the current is typing or not. - */ - setMyselfTyping(params) { - const typing = params.typing; - if (this._lastNotifiedMyselfTyping === typing) { - this._throttleNotifyMyselfTyping.cancel(); - } else { - this._throttleNotifyMyselfTyping(params); - } - - if (typing) { - this._myselfTypingInactivityTimer.reset(); - } else { - this._myselfTypingInactivityTimer.clear(); - } - }, - /** - * Unregister someone from currently typing something in this thread. - * - * @param {Object} params - * @param {integer} params.partnerID ID of the partner related to the user - * that is currently typing something - */ - unregisterTyping(params) { - const partnerID = params.partnerID; - this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); - if (!_.contains(this._typingPartnerIDs, partnerID)) { - return; - } - this._typingPartnerIDs = _.reject(this._typingPartnerIDs, function (id) { - return id === partnerID; - }); - this._warnUpdatedTypingPartners(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Tells whether the provided information on a partner is related to the - * current user or not. - * - * @abstract - * @private - * @param {Object} params - * @param {integer} params.partner ID of partner to check - */ - _isTypingMyselfInfo(params) { - return false; - }, - /** - * Notify to the server that the current user either starts or stops typing - * something. - * - * @abstract - * @private - * @param {Object} params - * @param {boolean} params.typing whether we are typing something or not - * @returns {Promise} resolved if the server is notified, rejected - * otherwise - */ - _notifyMyselfTyping(params) { - return Promise.resolve(); - }, - /** - * Warn views that the list of users that are currently typing on this - * thread has been updated. - * - * @abstract - * @private - */ - _warnUpdatedTypingPartners() {}, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when current user is typing something for a long time. In order - * to not let other users assume that we are no longer typing something, we - * must notify again that we are typing something. - * - * @private - */ - _onMyselfLongTypingTimeout() { - this._throttleNotifyMyselfTyping.clear(); - this._throttleNotifyMyselfTyping({ typing: true }); - }, - /** - * Called when current user has something typed in the composer, but is - * inactive for some time. In this case, he automatically notifies that he - * is no longer typing something - * - * @private - */ - _onMyselfTypingInactivityTimeout() { - this._throttleNotifyMyselfTyping.clear(); - this._throttleNotifyMyselfTyping({ typing: false }); - }, - /** - * Called by throttled version of notify myself typing - * - * Notify to the server that the current user either starts or stops typing - * something. Remember last notified stuff from the server, and update - * related typing timers. - * - * @private - * @param {Object} params - * @param {boolean} params.typing whether we are typing something or not. - */ - _onNotifyMyselfTyping(params) { - const typing = params.typing; - this._lastNotifiedMyselfTyping = typing; - this._notifyMyselfTyping(params); - if (typing) { - this._myselfLongTypingTimer.reset(); - } else { - this._myselfLongTypingTimer.clear(); - } - }, - /** - * Called when current user do not receive a typing notification of someone - * else typing for a long time. In this case, we assume that this person is - * no longer typing something. - * - * @private - * @param {integer} partnerID partnerID of the person we assume he is no - * longer typing something. - */ - _onOthersTypingTimeout(partnerID) { - this.unregisterTyping({ partnerID }); - }, - /** - * Called when a new message is added to the thread - * On receiving a message from a typing partner, unregister this partner - * from typing partners (otherwise, it will still display it until timeout). - * - * @private - * @param {mail.model.AbstractMessage} message - */ - _onTypingMessageAdded(message) { - const partnerID = message.hasAuthor() ? - message.getAuthorID() : - this._DEFAULT_TYPING_PARTNER_ID; - this.unregisterTyping({ partnerID }); - }, - /** - * Called when current user has posted a message on this thread. - * - * The current user receives the possibility to immediately notify the - * other users if he is typing something else. - * - * Refresh the context for the current user to notify that he starts or - * stops typing something. In other words, when this function is called and - * then the current user types something, it immediately notifies the - * server as if it is the first time he is typing something. - * - * @private - */ - _onTypingMessagePosted() { - this._lastNotifiedMyselfTyping = false; - this._throttleNotifyMyselfTyping.clear(); - this._myselfLongTypingTimer.clear(); - this._myselfTypingInactivityTimer.clear(); - }, -}; - -export default ThreadTypingMixin; 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 a24785e8454..5e9dbf56f55 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js +++ b/addons/im_livechat/static/src/legacy/public_livechat_chatbot.js @@ -108,8 +108,8 @@ const QWeb = core.qweb; _chatbotAddMessage(message, options) { message.body = utils.Markup(message.body); this._addMessage(message, options); - if (this.messaging.livechatButtonView.chatWindow.isFolded() || !this.messaging.livechatButtonView.chatWindow.isAtBottom()) { - this.messaging.livechatButtonView.livechat.incrementUnreadCounter(); + if (this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isFolded() || !this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isAtBottom()) { + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._unreadCounter++; } if (!options || !options.skipRenderMessages) { @@ -164,7 +164,7 @@ const QWeb = core.qweb; } const postedWelcomeMessages = await session.rpc('/chatbot/post_welcome_steps', { - channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); @@ -193,7 +193,7 @@ const QWeb = core.qweb; * @private */ _chatbotDisableInput(disableText) { - this.messaging.livechatButtonView.chatWindow.$('.o_composer_text_field') + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field') .prop('disabled', true) .addClass('text-center font-italic bg-200') .val(disableText); @@ -203,7 +203,7 @@ const QWeb = core.qweb; * @private */ _chatbotEnableInput() { - const $composerTextField = this.messaging.livechatButtonView.chatWindow.$('.o_composer_text_field'); + const $composerTextField = this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field'); $composerTextField .prop('disabled', false) .removeClass('text-center font-italic bg-200') @@ -235,9 +235,9 @@ const QWeb = core.qweb; // handle the display return; } - this.messaging.livechatButtonView.chatWindow.$('.o_composer_text_field').addClass('d-none'); - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_end').show(); - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_restart').one('click', + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field').addClass('d-none'); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').show(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_restart').one('click', this._onChatbotRestartScript.bind(this)); }, @@ -321,7 +321,7 @@ const QWeb = core.qweb; if (triggerNextStep) { let nextStepDelay = this.messaging.livechatButtonView.chatbot.messageDelay; - if (this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_typing').length !== 0) { + if (this.messaging.livechatButtonView.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 @@ -337,7 +337,7 @@ const QWeb = core.qweb; } if (!this._chatbotDisplayRestartButton()) { - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); } }, /** @@ -379,8 +379,7 @@ const QWeb = core.qweb; * @private */ _chatbotSaveSession() { - const chatUuid = this.messaging.livechatButtonView.livechat.toData().uuid; - localStorage.setItem('im_livechat.chatbot.state.uuid_' + chatUuid, JSON.stringify({ + localStorage.setItem('im_livechat.chatbot.state.uuid_' + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, JSON.stringify({ '_chatbot': this.messaging.livechatButtonView.chatbot.data, '_chatbotCurrentStep': this.messaging.livechatButtonView.chatbot.currentStep.data, })); @@ -399,17 +398,17 @@ const QWeb = core.qweb; this.messaging.livechatButtonView.update({ isTypingTimeout: setTimeout(() => { - this.messaging.livechatButtonView.chatWindow.$('.o_mail_thread_content').append( + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_mail_thread_content').append( $(QWeb.render('im_livechat.legacy.chatbot.is_typing_message', { 'chatbotImageSrc': `/im_livechat/operator/${ - this.messaging.livechatButtonView.livechat.getOperatorPID()[0] + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID[0] }/avatar`, 'chatbotName': this.messaging.livechatButtonView.chatbot.name, 'isWelcomeMessage': isWelcomeMessage, })) ); - this.messaging.livechatButtonView.chatWindow.scrollToBottom(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.scrollToBottom(); }, this.messaging.livechatButtonView.chatbot.messageDelay / 3), }); }, @@ -446,7 +445,7 @@ const QWeb = core.qweb; this.messaging.livechatButtonView.messages.length !== 0 ) { const lastMessage = this.messaging.livechatButtonView.messages[this.messaging.livechatButtonView.messages.length - 1]; - if (lastMessage.getAuthorID() !== this.messaging.livechatButtonView.livechat.getOperatorPID()[0]) { + if (lastMessage.getAuthorID() !== this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID[0]) { // we are on the last step of the script, expect a user input and the user has // already answered // -> end the script @@ -474,7 +473,7 @@ const QWeb = core.qweb; */ async _chatbotValidateEmail() { let emailValidResult = await session.rpc('/chatbot/step/validate_email', { - channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, }); if (emailValidResult.success) { @@ -513,7 +512,7 @@ const QWeb = core.qweb; } const nextStep = await session.rpc('/chatbot/step/trigger', { - channel_uuid:this.messaging.livechatButtonView.livechat.getUUID(), + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); @@ -554,7 +553,7 @@ const QWeb = core.qweb; */ _isLastMessageFromCustomer() { const lastMessage = this.messaging.livechatButtonView.messages.length !== 0 ? this.messaging.livechatButtonView.messages[this.messaging.livechatButtonView.messages.length - 1] : null; - return lastMessage && lastMessage.getAuthorID() !== this.messaging.livechatButtonView.livechat.getOperatorPID()[0]; + return lastMessage && lastMessage.getAuthorID() !== this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID[0]; }, //-------------------------------------------------------------------------- @@ -579,9 +578,9 @@ const QWeb = core.qweb; this.messaging.livechatButtonView.chatbot.currentStep.data.conversation_closed = true; this._chatbotSaveSession(); } - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_end').hide(); - this.messaging.livechatButtonView.chatWindow.$('.o_composer_text_field') + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').addClass('d-none'); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field') .removeClass('d-none') .val(''); }, @@ -600,7 +599,7 @@ const QWeb = core.qweb; return this._super(...arguments).then(() => { window.addEventListener('resize', () => { if (this.messaging.livechatButtonView.chatWindow) { - this.messaging.livechatButtonView.chatWindow.scrollToBottom(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.scrollToBottom(); } }); @@ -636,11 +635,11 @@ const QWeb = core.qweb; const self = this; - this.messaging.livechatButtonView.chatWindow.$('.o_thread_message:last .o_livechat_chatbot_options li').each(function () { + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_thread_message:last .o_livechat_chatbot_options li').each(function () { $(this).on('click', self._onChatbotOptionClicked.bind(self)); }); - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_main_restart').on('click', + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_main_restart').on('click', this._onChatbotRestartScript.bind(this)); if (this.messaging.livechatButtonView.messages.length !== 0) { @@ -754,14 +753,14 @@ const QWeb = core.qweb; id: '_welcome_' + stepIndex, is_discussion: true, // important for css style -> we only want white background for chatbot attachment_ids: [], - author_id: this.messaging.livechatButtonView.livechat.getOperatorPID(), + author_id: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID, body: utils.Markup(chatbotStep.chatbot_step_message), chatbot_script_step_id: chatbotStep.chatbot_script_step_id, chatbot_step_answers: chatbotStep.chatbot_step_answers, date: time.datetime_to_str(new Date()), model: "mail.channel", message_type: "comment", - res_id: this.messaging.livechatButtonView.livechat.getID(), + res_id: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id, }); } @@ -800,8 +799,8 @@ const QWeb = core.qweb; * @private */ async _onChatbotRestartScript(ev) { - this.messaging.livechatButtonView.chatWindow.$('.o_composer_text_field').removeClass('d-none'); - this.messaging.livechatButtonView.chatWindow.$('.o_livechat_chatbot_end').hide(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_composer_text_field').removeClass('d-none'); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_livechat_chatbot_end').hide(); if (this.nextStepTimeout) { clearTimeout(this.nextStepTimeout); @@ -812,7 +811,7 @@ const QWeb = core.qweb; } const postedMessage = await session.rpc('/chatbot/restart', { - channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, chatbot_script_id: this.messaging.livechatButtonView.chatbot.scriptId, }); @@ -880,7 +879,7 @@ const QWeb = core.qweb; this._chatbotSaveSession(); const saveAnswerPromise = session.rpc('/chatbot/answer/save', { - channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, message_id: messageId, selected_answer_id: selectedAnswer, }); diff --git a/addons/im_livechat/static/src/legacy/widgets/feedback/feedback.js b/addons/im_livechat/static/src/legacy/widgets/feedback/feedback.js index 8b4eebf6b5f..0284f9008a9 100644 --- a/addons/im_livechat/static/src/legacy/widgets/feedback/feedback.js +++ b/addons/im_livechat/static/src/legacy/widgets/feedback/feedback.js @@ -48,7 +48,7 @@ const Feedback = Widget.extend({ */ _sendFeedback(reason) { const args = { - uuid: this._livechat.getUUID(), + uuid: this._livechat._uuid, rate: this.rating, reason, }; @@ -125,7 +125,7 @@ const Feedback = Widget.extend({ this._rpc({ route: '/im_livechat/email_livechat_transcript', params: { - uuid: this._livechat.getUUID(), + uuid: this._livechat._uuid, email: $email.val(), } }).then(() => { diff --git a/addons/im_livechat/static/src/legacy/widgets/livechat_button.js b/addons/im_livechat/static/src/legacy/widgets/livechat_button.js index 78bdc4db5a9..b905cd8dde3 100644 --- a/addons/im_livechat/static/src/legacy/widgets/livechat_button.js +++ b/addons/im_livechat/static/src/legacy/widgets/livechat_button.js @@ -9,11 +9,9 @@ import Widget from 'web.Widget'; import { LIVECHAT_COOKIE_HISTORY } from 'im_livechat.legacy.im_livechat.Constants'; import Feedback from '@im_livechat/legacy/widgets/feedback/feedback'; -import PublicLivechat from '@im_livechat/legacy/models/public_livechat'; import PublicLivechatMessage from '@im_livechat/legacy/models/public_livechat_message'; -import PublicLivechatWindow from '@im_livechat/legacy/widgets/public_livechat_window/public_livechat_window'; -import { clear } from '@mail/model/model_field_command'; +import { clear, insertAndReplace } from '@mail/model/model_field_command'; const _t = core._t; const QWeb = core.qweb; @@ -110,8 +108,8 @@ const LivechatButton = Widget.extend({ return; } - if (this.messaging.livechatButtonView.livechat) { - this.messaging.livechatButtonView.livechat.addMessage(message); + if (this.messaging.livechatButtonView.publicLivechat && this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat) { + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.addMessage(message); } if (options && options.prepend) { @@ -128,10 +126,10 @@ const LivechatButton = Widget.extend({ * @private */ _askFeedback() { - this.messaging.livechatButtonView.chatWindow.$('.o_thread_composer input').prop('disabled', true); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled', true); - const feedback = new Feedback(this, this.messaging.livechatButtonView.livechat); - this.messaging.livechatButtonView.chatWindow.replaceContentWith(feedback); + const feedback = new Feedback(this, this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.replaceContentWith(feedback); feedback.on('send_message', this, this._sendMessage); feedback.on('feedback_sent', this, this._closeChat); @@ -140,7 +138,7 @@ const LivechatButton = Widget.extend({ * @private */ _closeChat() { - this.messaging.livechatButtonView.chatWindow.destroy(); + this.messaging.livechatButtonView.update({ chatWindow: clear() }); utils.set_cookie('im_livechat_session', "", -1); // remove cookie }, /** @@ -152,20 +150,20 @@ const LivechatButton = Widget.extend({ _handleNotification({ payload, type }) { switch (type) { case 'im_livechat.history_command': { - if (payload.id !== this.messaging.livechatButtonView.livechat._id) { + if (payload.id !== this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id) { return; } const cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY); const history = cookie ? JSON.parse(cookie) : []; session.rpc('/im_livechat/history', { - pid: this.messaging.livechatButtonView.livechat.getOperatorPID()[0], - channel_uuid: this.messaging.livechatButtonView.livechat.getUUID(), + pid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID[0], + channel_uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, page_history: history, }); return; } case 'mail.channel.partner/typing_status': { - if (payload.channel_id !== this.messaging.livechatButtonView.livechat._id) { + if (payload.channel_id !== this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id) { return; } const partnerID = payload.partner_id; @@ -174,14 +172,14 @@ const LivechatButton = Widget.extend({ return; } if (payload.is_typing) { - this.messaging.livechatButtonView.livechat.registerTyping({ partnerID }); + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.registerTyping({ partnerID }); } else { - this.messaging.livechatButtonView.livechat.unregisterTyping({ partnerID }); + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.unregisterTyping({ partnerID }); } return; } case 'mail.channel/new_message': { - if (payload.id !== this.messaging.livechatButtonView.livechat._id) { + if (payload.id !== this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id) { return; } const notificationData = payload.message; @@ -191,8 +189,8 @@ const LivechatButton = Widget.extend({ } notificationData.body = utils.Markup(notificationData.body); this._addMessage(notificationData); - if (this.messaging.livechatButtonView.chatWindow.isFolded() || !this.messaging.livechatButtonView.chatWindow.isAtBottom()) { - this.messaging.livechatButtonView.livechat.incrementUnreadCounter(); + if (this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isFolded() || !this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isAtBottom()) { + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._unreadCounter++; } this._renderMessages(); return; @@ -259,20 +257,17 @@ const LivechatButton = Widget.extend({ } } else { this.messaging.livechatButtonView.update({ - livechat: new PublicLivechat({ - parent: this, - data: livechatData - }), + publicLivechat: insertAndReplace({ data: livechatData }), }); return this._openChatWindow().then(() => { if (!this.messaging.livechatButtonView.history) { this._sendWelcomeMessage(); } this._renderMessages(); - this.call('bus_service', 'addChannel', this.messaging.livechatButtonView.livechat.getUUID()); + this.call('bus_service', 'addChannel', this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid); this.call('bus_service', 'startPolling'); - utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.livechat.toData()), true), 60 * 60); + utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.toData()), true), 60 * 60); utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60 * 60); if (livechatData.operator_pid[0]) { // livechatData.operator_pid contains a tuple (id, name) @@ -312,19 +307,11 @@ const LivechatButton = Widget.extend({ * @return {Promise} */ _openChatWindow() { - const options = { - displayStars: false, - headerBackgroundColor: this.messaging.livechatButtonView.headerBackgroundColor, - placeholder: this.messaging.livechatButtonView.inputPlaceholder, - titleColor: this.messaging.livechatButtonView.titleColor, - }; - this.messaging.livechatButtonView.update({ - chatWindow: new PublicLivechatWindow(this, this.messaging.livechatButtonView.livechat, options), - }); - return this.messaging.livechatButtonView.chatWindow.appendTo($('body')).then(() => { + this.messaging.livechatButtonView.update({ chatWindow: insertAndReplace() }); + return this.messaging.livechatButtonView.chatWindow.legacyChatWindow.appendTo($('body')).then(() => { const cssProps = { bottom: 0 }; cssProps[_t.database.parameters.direction === 'rtl' ? 'left' : 'right'] = 0; - this.messaging.livechatButtonView.chatWindow.$el.css(cssProps); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$el.css(cssProps); this.$el.hide(); }); }, @@ -342,11 +329,11 @@ const LivechatButton = Widget.extend({ * @private */ _renderMessages() { - const shouldScroll = !this.messaging.livechatButtonView.chatWindow.isFolded() && this.messaging.livechatButtonView.chatWindow.isAtBottom(); - this.messaging.livechatButtonView.livechat.setMessages(this.messaging.livechatButtonView.messages); - this.messaging.livechatButtonView.chatWindow.render(); + const shouldScroll = !this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isFolded() && this.messaging.livechatButtonView.chatWindow.legacyChatWindow.isAtBottom(); + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._messages = this.messaging.livechatButtonView.messages; + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.render(); if (shouldScroll) { - this.messaging.livechatButtonView.chatWindow.scrollToBottom(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.scrollToBottom(); } }, /** @@ -355,9 +342,9 @@ const LivechatButton = Widget.extend({ * @return {Promise} */ _sendMessage(message) { - this.messaging.livechatButtonView.livechat._notifyMyselfTyping({ typing: false }); + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._notifyMyselfTyping({ typing: false }); return session - .rpc('/mail/chat_post', { uuid: this.messaging.livechatButtonView.livechat.getUUID(), message_content: message.content }) + .rpc('/mail/chat_post', { uuid: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid, message_content: message.content }) .then((messageId) => { if (!messageId) { try { @@ -378,7 +365,7 @@ const LivechatButton = Widget.extend({ } this._closeChat(); } - this.messaging.livechatButtonView.chatWindow.scrollToBottom(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.scrollToBottom(); }); }, /** @@ -389,11 +376,11 @@ const LivechatButton = Widget.extend({ this._addMessage({ id: '_welcome', attachment_ids: [], - author_id: this.messaging.livechatButtonView.livechat.getOperatorPID(), + author_id: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._operatorPID, body: this.messaging.livechatButtonView.defaultMessage, date: time.datetime_to_str(new Date()), model: "mail.channel", - res_id: this.messaging.livechatButtonView.livechat.getID(), + res_id: this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id, }, { prepend: true }); } }, @@ -408,12 +395,12 @@ const LivechatButton = Widget.extend({ */ _onCloseChatWindow(ev) { ev.stopPropagation(); - const isComposerDisabled = this.messaging.livechatButtonView.chatWindow.$('.o_thread_composer input').prop('disabled'); + const isComposerDisabled = this.messaging.livechatButtonView.chatWindow.legacyChatWindow.$('.o_thread_composer input').prop('disabled'); const shouldAskFeedback = !isComposerDisabled && this.messaging.livechatButtonView.messages.find(function (message) { return message.getID() !== '_welcome'; }); if (shouldAskFeedback) { - this.messaging.livechatButtonView.chatWindow.toggleFold(false); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.toggleFold(false); this._askFeedback(); } else { this._closeChat(); @@ -448,7 +435,7 @@ const LivechatButton = Widget.extend({ */ _onSaveChatWindow(ev) { ev.stopPropagation(); - utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.livechat.toData()), true), 60 * 60); + utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.toData()), true), 60 * 60); }, /** * @private @@ -456,7 +443,7 @@ const LivechatButton = Widget.extend({ */ _onUpdatedTypingPartners(ev) { ev.stopPropagation(); - this.messaging.livechatButtonView.chatWindow.renderHeader(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.renderHeader(); }, /** * @private @@ -464,7 +451,7 @@ const LivechatButton = Widget.extend({ */ _onUpdatedUnreadCounter(ev) { ev.stopPropagation(); - this.messaging.livechatButtonView.chatWindow.renderHeader(); + this.messaging.livechatButtonView.chatWindow.legacyChatWindow.renderHeader(); }, /** * @private diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js index 6780ec3dfa7..ac16e0e2a1b 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.js @@ -47,15 +47,9 @@ const PublicLivechatWindow = Widget.extend({ }); this._hidden = false; - this._thread = thread || null; + this._thread = thread; 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; }, /** * @override @@ -128,49 +122,6 @@ const PublicLivechatWindow = Widget.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. @@ -178,10 +129,7 @@ const PublicLivechatWindow = Widget.extend({ * @returns {string} */ getTitle() { - if (!this.hasThread()) { - return _t("Undefined"); - } - return this._thread.getTitle(); + return this._thread._name; }, /** * Get the unread counter of the related thread. If there are no thread @@ -190,10 +138,7 @@ const PublicLivechatWindow = Widget.extend({ * @returns {integer} */ getUnreadCounter() { - if (!this.hasThread()) { - return 0; - } - return this._thread.getUnreadCounter(); + return this._thread._unreadCounter; }, /** * Tells whether the bottom of the thread in the thread window is visible @@ -212,10 +157,7 @@ const PublicLivechatWindow = Widget.extend({ * @returns {boolean} */ isFolded() { - if (!this.hasThread()) { - return this._folded; - } - return this._thread.isFolded(); + return this._thread._folded; }, /** * States whether the thread window is hidden or not. @@ -241,16 +183,14 @@ const PublicLivechatWindow = Widget.extend({ * @returns {boolean} */ needsComposer() { - return this.hasThread(); + return true; }, /** * Render the thread window */ render() { this.renderHeader(); - if (this.hasThread()) { - this._threadWidget.render(this._thread, { displayLoadMore: false }); - } + this._threadWidget.render(this._thread, { displayLoadMore: false }); }, /** * Render the header of this thread window. @@ -340,28 +280,13 @@ const PublicLivechatWindow = Widget.extend({ */ _getHeaderRenderingOptions() { return { - status: this.getThreadStatus(), - thread: this.getThread(), + status: this._thread._status, + thread: this._thread, 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. @@ -381,9 +306,6 @@ const PublicLivechatWindow = Widget.extend({ */ _postMessage(messageData) { this.trigger_up('post_message_chat_window', { messageData }); - if (!this.hasThread()) { - return; - } this._thread.postMessage(messageData) .then(() => { this._threadWidget.scrollToBottom(); @@ -400,12 +322,7 @@ const PublicLivechatWindow = Widget.extend({ * @param {boolean} folded */ _updateThreadFoldState(folded) { - if (this.hasThread()) { - this._thread.fold(folded); - } else { - this._folded = folded; - this.updateVisualFoldState(); - } + this._thread.fold(folded); }, //-------------------------------------------------------------------------- @@ -423,8 +340,7 @@ const PublicLivechatWindow = Widget.extend({ ev.stopPropagation(); ev.preventDefault(); if ( - this.hasThread() && - this._thread.getUnreadCounter() > 0 && + this._thread._unreadCounter > 0 && !this.isFolded() ) { this._thread.markAsRead(); @@ -467,10 +383,8 @@ const PublicLivechatWindow = Widget.extend({ * @private */ _onInput() { - if (this.hasThread() && this._thread.hasTypingNotification()) { - const isTyping = this.$input.val().length > 0; - this._thread.setMyselfTyping({ typing: isTyping }); - } + const isTyping = this.$input.val().length > 0; + this._thread.setMyselfTyping({ typing: isTyping }); }, /** * Called when typing something on the composer of this thread window. @@ -505,7 +419,7 @@ const PublicLivechatWindow = Widget.extend({ * @private */ _onScroll() { - if (this.hasThread() && this.isAtBottom()) { + if (this.isAtBottom()) { this._thread.markAsRead(); } }, diff --git a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml index 47c3fccdbc7..d72884a1832 100644 --- a/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml +++ b/addons/im_livechat/static/src/legacy/widgets/public_livechat_window/public_livechat_window.xml @@ -4,13 +4,13 @@ @param {im_livechat.legacy.PublicLivechatWindow} widget --> -
+
- + - +
@@ -39,7 +39,7 @@ () - + diff --git a/addons/im_livechat/static/src/legacy/widgets/thread/thread.js b/addons/im_livechat/static/src/legacy/widgets/thread/thread.js index efa6bfda3cc..2cd7b1345d2 100644 --- a/addons/im_livechat/static/src/legacy/widgets/thread/thread.js +++ b/addons/im_livechat/static/src/legacy/widgets/thread/thread.js @@ -115,10 +115,10 @@ const ThreadWidget = Widget.extend({ */ render(thread, options) { let shouldScrollToBottomAfterRendering = false; - if (this._currentThreadID === thread.getID() && this.isAtBottom()) { + if (this._currentThreadID === thread._id && this.isAtBottom()) { shouldScrollToBottomAfterRendering = true; } - this._currentThreadID = thread.getID(); + this._currentThreadID = thread._id; // copy so that reverse do not alter order in the thread object const messages = _.clone(thread.getMessages()); @@ -245,12 +245,12 @@ const ThreadWidget = Widget.extend({ * @param {Object} [options] options for the thread rendering */ removeMessageAndRender(messageID, thread, options) { - this._currentThreadID = thread.getID(); + this._currentThreadID = thread._id; return new Promise((resolve, reject) => { this.$('.o_thread_message[data-message-id="' + messageID + '"]') .fadeOut({ done: () => { - if (this._currentThreadID === thread.getID()) { + if (this._currentThreadID === thread._id) { this.render(thread, options); } resolve(); diff --git a/addons/im_livechat/static/src/legacy/widgets/thread/thread.xml b/addons/im_livechat/static/src/legacy/widgets/thread/thread.xml index 30b869c4ccd..2fb18cce943 100644 --- a/addons/im_livechat/static/src/legacy/widgets/thread/thread.xml +++ b/addons/im_livechat/static/src/legacy/widgets/thread/thread.xml @@ -10,7 +10,7 @@ @param {Array} [options.domain=[]] the domain to restrict messages on the thread. --> - + @@ -193,7 +193,7 @@ on - + (from #) diff --git a/addons/im_livechat/static/src/models/livechat_button_view.js b/addons/im_livechat/static/src/models/livechat_button_view.js index 5401c6653ba..57f34071b0f 100644 --- a/addons/im_livechat/static/src/models/livechat_button_view.js +++ b/addons/im_livechat/static/src/models/livechat_button_view.js @@ -216,9 +216,9 @@ registerModel({ chatbotState: attr({ compute: '_computeChatbotState', }), - // livechat window - chatWindow: attr({ - default: null, + chatWindow: one('PublicLivechatWindow', { + inverse: 'livechatButtonViewOwner', + isCausal: true, }), currentPartnerId: attr({ compute: '_computeCurrentPartnerId', @@ -256,10 +256,6 @@ registerModel({ isWebsiteLivechatChatbotFlow: attr({ default: false, }), - // livechat model - livechat: attr({ - default: null, - }), livechatInit: attr(), localStorageChatbotState: attr({ compute: '_computeLocalStorageChatbotState', @@ -267,6 +263,10 @@ registerModel({ messages: attr({ default: [], }), + publicLivechat: one('PublicLivechat', { + inverse: 'livechatButtonOwner', + isCausal: true, + }), rule: attr(), serverUrl: attr({ compute: '_computeServerUrl', diff --git a/addons/im_livechat/static/src/models/public_livechat.js b/addons/im_livechat/static/src/models/public_livechat.js new file mode 100644 index 00000000000..8af2b0c9683 --- /dev/null +++ b/addons/im_livechat/static/src/models/public_livechat.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import PublicLivechat from '@im_livechat/legacy/models/public_livechat'; + +import { registerModel } from '@mail/model/model_core'; +import { attr, one } from '@mail/model/model_field'; +import { clear } from '@mail/model/model_field_command'; + +registerModel({ + name: 'PublicLivechat', + identifyingFields: ['livechatButtonOwner'], + recordMethods: { + /** + * @private + * @returns {FieldCommand|im_livechat/legacy/models/public_livechat} + */ + _computeLegacyPublicLivechat() { + if (!this.data) { + return clear(); + } + return new PublicLivechat({ + parent: this.livechatButtonOwner.widget, + data: this.data, + }); + }, + }, + fields: { + data: attr(), + legacyPublicLivechat: attr({ + compute: '_computeLegacyPublicLivechat', + }), + livechatButtonOwner: one('LivechatButtonView', { + inverse: 'publicLivechat', + readonly: true, + required: true, + }), + }, +}); diff --git a/addons/im_livechat/static/src/models/public_livechat_window.js b/addons/im_livechat/static/src/models/public_livechat_window.js new file mode 100644 index 00000000000..a6ad3972f11 --- /dev/null +++ b/addons/im_livechat/static/src/models/public_livechat_window.js @@ -0,0 +1,52 @@ +/** @odoo-module **/ + +import PublicLivechatWindow from '@im_livechat/legacy/widgets/public_livechat_window/public_livechat_window'; + +import { registerModel } from '@mail/model/model_core'; +import { attr, one } from '@mail/model/model_field'; +import { clear } from '@mail/model/model_field_command'; + +registerModel({ + name: 'PublicLivechatWindow', + identifyingFields: ['livechatButtonViewOwner'], + lifecycleHooks: { + _willDelete() { + if (this.legacyChatWindow) { + this.legacyChatWindow.destroy(); + } + }, + }, + recordMethods: { + /** + * @private + * @returns {im_livechat/legacy/widgets/public_chat_window|FieldCommand} + */ + _computeLegacyChatWindow() { + if (!this.livechatButtonViewOwner.widget && this.legacyChatWindow) { + this.legacyChatWindow.destroy(); + return clear(); + } + return new PublicLivechatWindow( + this.livechatButtonViewOwner.widget, + this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat, + { + displayStars: false, + headerBackgroundColor: this.livechatButtonViewOwner.headerBackgroundColor, + placeholder: this.livechatButtonViewOwner.inputPlaceholder, + titleColor: this.livechatButtonViewOwner.titleColor, + }, + ); + }, + }, + fields: { + legacyChatWindow: attr({ + compute: '_computeLegacyChatWindow', + default: null, + }), + livechatButtonViewOwner: one('LivechatButtonView', { + inverse: 'chatWindow', + readonly: true, + required: true, + }), + }, +}); diff --git a/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js b/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js index 562c5df7a74..1894a48f9c6 100644 --- a/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js +++ b/addons/website_livechat/static/src/legacy/website_livechat_chatbot_test_script.js @@ -4,7 +4,6 @@ import publicWidget from 'web.public.widget'; import utils from 'web.utils'; import LivechatButton from '@im_livechat/legacy/widgets/livechat_button'; -import PublicLivechat from '@im_livechat/legacy/models/public_livechat'; import { insertAndReplace } from '@mail/model/model_field_command'; @@ -60,18 +59,15 @@ const LivechatButtonTestChatbot = LivechatButton.extend({ */ _openChat: function () { this.messaging.livechatButtonView.update({ - livechat: new PublicLivechat({ - parent: this, - data: this._channelData, - }), + publicLivechat: insertAndReplace({ data: this._channelData }), }); return this._openChatWindow().then(() => { this._sendWelcomeMessage(); this._renderMessages(); - this.call('bus_service', 'addChannel', this.messaging.livechatButtonView.livechat.getUUID()); + this.call('bus_service', 'addChannel', this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._uuid); this.call('bus_service', 'startPolling'); - utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.livechat.toData()), true), 60 * 60); + utils.set_cookie('im_livechat_session', utils.unaccent(JSON.stringify(this.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat.toData()), true), 60 * 60); this.messaging.livechatButtonView.update({ isOpeningChat: false }); }); }, diff --git a/addons/website_livechat/static/tests/tours/website_livechat_common.js b/addons/website_livechat/static/tests/tours/website_livechat_common.js index c92f50928dd..2876a25a6cb 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_common.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_common.js @@ -30,7 +30,7 @@ LivechatButton.include({ self._handleNotification({ type: 'mail.channel/new_message', payload: { - id: self.messaging.livechatButtonView.livechat._id, + id: self.messaging.livechatButtonView.publicLivechat.legacyPublicLivechat._id, message: { id: self.messaging.livechatButtonView.messages.length + 1, author_id: [0, 'Website Visitor Test'],