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

*: 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-2898692

closes odoo/odoo#94914

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-06-30 12:11:00 +02:00
parent 56455a69aa
commit 21471fc5e7
15 changed files with 484 additions and 722 deletions
+4 -12
View File
@@ -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',
@@ -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;
@@ -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;
@@ -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,
});
@@ -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(() => {
@@ -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
@@ -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();
}
},
@@ -4,13 +4,13 @@
@param {im_livechat.legacy.PublicLivechatWindow} widget
-->
<t t-name="im_livechat.legacy.PublicLivechatWindow">
<div class="o_thread_window o_in_home_menu" t-att-data-thread-id="widget.getID()">
<div class="o_thread_window o_in_home_menu" t-att-data-thread-id="widget._thread._id">
<div class="o_thread_window_header">
<t t-call="im_livechat.legacy.PublicLivechatWindow.HeaderContent">
<t t-set="status" t-value="widget.getThreadStatus()"/>
<t t-set="status" t-value="widget._thread._status"/>
<t t-set="title" t-value="widget.getTitle()"/>
<t t-set="unreadCounter" t-value="widget.getUnreadCounter()"/>
<t t-set="thread" t-value="widget.getThread()"/>
<t t-set="thread" t-value="widget._thread"/>
</t>
</div>
<div class="o_thread_window_content">
@@ -39,7 +39,7 @@
<span class="o_thread_window_title">
<t t-esc="title"/>
<span t-if="unreadCounter"> (<t t-esc="unreadCounter"/>)</span>
<t t-if="thread and thread.hasTypingNotification() and thread.isSomeoneTyping()" t-call="im_livechat.legacy.mail.ThreadTypingIcon"/>
<t t-if="thread.isSomeoneTyping()" t-call="im_livechat.legacy.mail.ThreadTypingIcon"/>
</span>
<span t-if="!widget.isMobile()" class="o_thread_window_buttons">
<a href="#" class="o_thread_window_close fa fa-close"/>
@@ -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();
@@ -10,7 +10,7 @@
@param {Array} [options.domain=[]] the domain to restrict messages on the thread.
-->
<t t-name="im_livechat.legacy.mail.widget.Thread">
<t t-if="thread.hasMessages({ 'domain': options.domain || [] })">
<t t-if="thread.hasMessages()">
<t t-call="im_livechat.legacy.mail.widget.Thread.Content"/>
</t>
</t>
@@ -193,7 +193,7 @@
<t t-if="message.isLinkedToDocumentThread() and options.displayDocumentLinks">
<small>on</small> <a t-att-href="message.getURL()" t-att-data-oe-model="message.getDocumentModel()" t-att-data-oe-id="message.getDocumentID()" class="o_document_link"><t t-esc="message.getDocumentName()"/></a>
</t>
<t t-if="message.originatesFromChannel() and (message.getOriginChannelID() !== thread.getID())">
<t t-if="message.originatesFromChannel() and (message.getOriginChannelID() !== thread._id)">
(<small>from</small> <a t-att-data-oe-id="message.getOriginChannelID()" href="#">#<t t-esc="message.getOriginChannelName()"/></a>)
</t>
<span t-if="options.displayNotificationIcons and message.hasNotifications()" class="o_thread_tooltip_container">
@@ -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',
@@ -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,
}),
},
});
@@ -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,
}),
},
});
@@ -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 });
});
},
@@ -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'],