diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-08-11 21:02:55 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-08-13 14:17:40 +0200 |
| commit | 5f1d32f3fb89f40740b25491caafb9a5710d555a (patch) | |
| tree | d94aab4d0a6fb36d60079391216b3dd4c89b23c7 /addons/mail/static/src/js/models/threads | |
| parent | 614f9f84d7da281b0feacc39a612c19193f3a4d8 (diff) | |
[IMP] mail, im_livechat: 'is typing...' indicator on mail channels
With this commit, most threads show an indicator when a member is typing
something. e.g.
"Mitchell Stephens is typing..."
Supported threads:
- DM
- (public & private) channels
- Livechat (Website & Backend)
Unsupported threads:
- mailboxes (e.g. 'Inbox')
- document thread (= 'Chatter')
- support channel (with `im_support` module)
Some technical details:
- This is a mix between 'start'/'stop' and 'regular notify' strategies.
- On typing text in composer: notify 'start typing' to all members of
thread.
- On clearing text in composer: notify 'stop typing' to all members of
thread.
- A member can only notify once every 2.5 seconds to the server when he
starts or stops typing something (throttled, buffered notify call).
- On receiving a message from someone that is typing: determine that he is
no longer typing.
- On non-empty and unchanged composer text for 5.0 seconds: automatically
notifies all members of thread that he is no longer typing anything.
(This is like a 'soft / cooperative sender' timeout).
- On non-receiving a typing notification of someone after 60 seconds:
determine that he is not longer typing something (This is like a
'hard / receiver assumption' timeout).
- When some types for more than 50 seconds straight: notify that he is
still typing something (again and again every 50 seconds).
- When two or more people are typing something (Multi-User channels), it
displays at most 2 typing users (ordering rule: longer typers first):
"Mitchell Stephens, Marc Brown and more are typing..."
- Timing summary:
- Usually 2.5 seconds uncertainty.
- Up to 5.0 seconds uncertainty on typer inactivity.
- Up to 60 seconds uncertainty on typer page reload / browser tab
closed / etc.
Future Improvements:
- Detect page reload or browser tab close with loose of longpolling
connection
Task-ID 28188
Diffstat (limited to 'addons/mail/static/src/js/models/threads')
7 files changed, 765 insertions, 2 deletions
diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js index fb6695c6a6c..322541af016 100644 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ b/addons/mail/static/src/js/models/threads/abstract_thread.js @@ -103,6 +103,16 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { return !_.isEmpty(this.getMessages()); }, /** + * States whether this thread is compatible with the 'is typing...' feature. + * By default, threads do not have this feature active. + * @see {mail.model.ThreadTypingMixin} to enable this feature on a thread. + * + * @returns {boolean} + */ + hasTypingNotification: function () { + return false; + }, + /** * States whether this thread is folded or not. * * @return {boolean} diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js index 3f932ccb8ec..8f0f664b6b0 100644 --- a/addons/mail/static/src/js/models/threads/channel.js +++ b/addons/mail/static/src/js/models/threads/channel.js @@ -2,6 +2,7 @@ odoo.define('mail.model.Channel', function (require) { "use strict"; var SearchableThread = require('mail.model.SearchableThread'); +var ThreadTypingMixin = require('mail.model.ThreadTypingMixin'); var mailUtils = require('mail.utils'); var session = require('web.session'); @@ -15,7 +16,7 @@ var time = require('web.time'); * Any piece of code in JS that make use of channels must ideally interact with * such objects, instead of direct data from the server. */ -var Channel = SearchableThread.extend({ +var Channel = SearchableThread.extend(ThreadTypingMixin, { /** * @override * @param {Object} params @@ -38,6 +39,7 @@ var Channel = SearchableThread.extend({ init: function (params) { var self = this; this._super.apply(this, arguments); + ThreadTypingMixin.init.call(this, arguments); var data = params.data; var options = params.options; @@ -375,6 +377,15 @@ var Channel = SearchableThread.extend({ ['channel_ids', 'in', [this._id]]]; }, /** + * @override {mail.model.ThreadTypingMixin} + * @private + * @param {Object} params + * @param {integer} params.partnerID + */ + _isTypingMyselfInfo: function (params) { + return session.partner_id === params.partnerID; + }, + /** * Marks this channel as read. * The last seen message will be the last message. * Resolved with the last seen message, only for non-mailbox channels @@ -389,6 +400,21 @@ var Channel = SearchableThread.extend({ return $.when(superDef, seenDef); }, /** + * @override {mail.model.ThreadTypingMixin} + * @private + * @param {Object} params + * @param {boolean} params.typing + * @returns {$.Promise} + */ + _notifyMyselfTyping: function (params) { + return this._rpc({ + model: 'mail.channel', + method: 'notify_typing', + args: [this.getID()], + kwargs: { is_typing: params.typing }, + }, { shadow: true }); + }, + /** * Prepare and send a message to the server on this channel. * * @override @@ -415,6 +441,17 @@ var Channel = SearchableThread.extend({ }); }); }, + /** + * Warn views that the list of users that are currently typing on this + * thread has been updated. + * + * @override {mail.model.ThreadTypingMixin} + * @private + */ + _warnUpdatedTypingPartners: function () { + this.call('mail_service', 'getMailBus') + .trigger('update_typing_partners', this.getID()); + }, }); return Channel; diff --git a/addons/mail/static/src/js/models/threads/livechat.js b/addons/mail/static/src/js/models/threads/livechat.js index d2aa04f926a..b7235e7bc0e 100644 --- a/addons/mail/static/src/js/models/threads/livechat.js +++ b/addons/mail/static/src/js/models/threads/livechat.js @@ -32,7 +32,9 @@ var Livechat = TwoUserChannel.extend({ //-------------------------------------------------------------------------- /** - * For the livechat, + * Override so that the list of members has the website use. This is + * necessary in order for the 'is_typing' feature to compute the name to + * display of a user that is typing. * * @override * @returns {$.Promise<Object[]>} resolved with list of livechat members @@ -50,6 +52,44 @@ var Livechat = TwoUserChannel.extend({ return self._members; }); }, + /** + * Called when someone starts typing something on the livechat. + * + * Overrides it so that it determine the partner based on the received + * userID. The reason is that anonymous users have the partner ID of the + * admin, which is likely also an operator, so userID must be used in order + * to distinct them. + * + * @override {mail.model.ThreadTypingMixin} + * @param {Object} params + * @param {integer} params.partnerID ID of the partner that is currently + * typing something on the thread. + * @param {boolean} [params.isWebsiteUser=false] whether the typing partner + * is an anonymous user (reminder: they share partnerID with admin). + */ + registerTyping: function (params) { + params.partnerID = this._WEBSITE_USER_ID; + this._super.call(this, params); + }, + /** + * Called when someone stops typing something on the livechat. + * + * Overrides it so that it determine the partner based on the received + * userID. The reason is that anonymous users have the partner ID of the + * admin, which is likely also an operator, so userID must be used in order + * to distinct them. + * + * @override {mail.model.ThreadTypingMixin} + * @param {Object} params + * @param {integer} params.partnerID ID of the partner that stops typing + * something on the thread. + * @param {boolean} [params.isWebsiteUser=false] whether the typing partner + * is an anonymous user (reminder: they share partnerID with admin). + */ + unregisterTyping: function (params) { + params.partnerID = this._WEBSITE_USER_ID; + this._super.call(this, params); + }, }); return Livechat; diff --git a/addons/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js b/addons/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js new file mode 100644 index 00000000000..b4b87087e6d --- /dev/null +++ b/addons/mail/static/src/js/models/threads/thread_typing_mixin/cc_throttle_function.js @@ -0,0 +1,181 @@ +odoo.define('mail.model.CCThrottleFunctionObject', function (require) { +"use strict"; + +var Class = require('web.Class'); + +/** + * This object models the behaviour of the clearable and cancellable (CC) + * throttle version of a provided function. + */ +var CCThrottleFunctionObject = Class.extend({ + + /** + * @param {Object} params + * @param {integer} params.duration duration of the 'cooldown' phase, i.e. + * the minimum duration between the most recent function call that has + * been made and the following function call. + * @param {function} params.func provided function for making the CC + * throttled version. + */ + init: function (params) { + this._arguments = undefined; + this._cooldownTimeout = undefined; + this._duration = params.duration; + this._func = params.func; + this._shouldCallFunctionAfterCD = false; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Cancel any buffered function call, but keep the cooldown phase running. + */ + cancel: function () { + this._arguments = undefined; + this._shouldCallFunctionAfterCD = false; + }, + /** + * Clear the internal throttle timer, so that the following function call + * is immediate. For instance, if there is a cooldown stage, it is aborted. + */ + clear: function () { + if (this._cooldownTimeout) { + clearTimeout(this._cooldownTimeout); + this._onCooldownTimeout(); + } + }, + /** + * Called when there is a call to the function. This function is throttled, + * so the time it is called depends on whether the "cooldown stage" occurs + * or not: + * + * - no cooldown stage: function is called immediately, and it starts + * the cooldown stage when successful. + * - in cooldown stage: function is called when the cooldown stage has + * ended from timeout. + * + * Note that after the cooldown stage, only the last attempted function + * call will be considered. + */ + do: function () { + this._arguments = Array.prototype.slice.call(arguments); + if (this._cooldownTimeout === undefined) { + this._callFunction(); + } else { + this._shouldCallFunctionAfterCD = true; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Immediately calls the function with arguments of last buffered function + * call. It initiates the cooldown stage after this function call. + * + * @private + */ + _callFunction: function () { + this._func.apply(null, this._arguments); + this._cooldown(); + }, + /** + * Called when the function has been successfully called. The following + * calls to the function with this object should suffer a "cooldown stage", + * which prevents the function from being called until this stage has ended. + * + * @private + */ + _cooldown: function () { + this.cancel(); + this._cooldownTimeout = setTimeout( + this._onCooldownTimeout.bind(this), + this._duration + ); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the cooldown stage ended from timeout. Calls the function if + * a function call was buffered. + * + * @private + */ + _onCooldownTimeout: function () { + if (this._shouldCallFunctionAfterCD) { + this._callFunction(); + } else { + this._cooldownTimeout = undefined; + } + }, +}); + +return CCThrottleFunctionObject; + +}); + + +odoo.define('mail.model.CCThrottleFunction', function (require) { +"use strict"; + +var CCThrottleFunctionObject = require('mail.model.CCThrottleFunctionObject'); + +/** + * A function that creates a cancellable and clearable (CC) throttle version + * of a provided function. + * + * This throttle mechanism allows calling a function at most once during a + * certain period: + * + * - When a function call is made, it enters a 'cooldown' phase, in which any + * attempt to call the function is buffered until the cooldown phase ends. + * - At most 1 function call can be buffered during the cooldown phase, and the + * latest one in this phase will be considered at its end. + * - When a cooldown phase ends, any buffered function call will be performed + * and another cooldown phase will follow up. + * + * This throttle version has the following interesting properties: + * + * - cancellable: it allows removing a buffered function call during the + * cooldown phase, but it keeps the cooldown phase running. + * - clearable: it allows to clear the internal clock of the throttled function, + * so that any cooldown phase is immediately ending. + * + * @param {Object} params + * @param {integer} params.duration a duration for the throttled behaviour, + * in milli-seconds. + * @param {function} params.func the function to throttle + * @returns {function} the cancellable and clearable throttle version of the + * provided function in argument. + */ +var CCThrottleFunction = function (params) { + var duration = params.duration; + var func = params.func; + + var throttleFunctionObject = new CCThrottleFunctionObject({ + duration: duration, + func: func, + }); + + var callable = function () { + return throttleFunctionObject.do.apply(throttleFunctionObject, arguments); + }; + callable.cancel = function () { + throttleFunctionObject.cancel(); + }; + callable.clear = function () { + throttleFunctionObject.clear(); + }; + + return callable; +}; + +return CCThrottleFunction; + +}); diff --git a/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js new file mode 100644 index 00000000000..fb1a3a91cd1 --- /dev/null +++ b/addons/mail/static/src/js/models/threads/thread_typing_mixin/thread_typing_mixin.js @@ -0,0 +1,344 @@ +odoo.define('mail.model.ThreadTypingMixin', function (require) { +"use strict"; + +var CCThrottleFunction = require('mail.model.CCThrottleFunction'); +var Timer = require('mail.model.Timer'); +var Timers = require('mail.model.Timers'); + +var core = require('web.core'); + +var _t = core._t; + +/** + * Mixin for enabling the "is typing..." notification on a type of thread. + */ +var 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: function () { + // 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: function () { + var typingPartnerIDs = this._typingPartnerIDs; + var typingMembers = _.filter(this._members, function (member) { + return _.contains(typingPartnerIDs, member.id); + }); + var sortedTypingMembers = _.sortBy(typingMembers, function (member) { + return _.indexOf(typingPartnerIDs, member.id); + }); + var displayableTypingMembers = sortedTypingMembers.slice(0, 3); + + if (displayableTypingMembers.length === 0) { + return ''; + } else if (displayableTypingMembers.length === 1) { + return _.str.sprintf(_t("%s is typing..."), displayableTypingMembers[0].name); + } else if (displayableTypingMembers.length === 2) { + return _.str.sprintf(_t("%s and %s are typing..."), + displayableTypingMembers[0].name, + displayableTypingMembers[1].name); + } else { + return _.str.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: function () { + return true; + }, + /** + * 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: function (params) { + if (this._isTypingMyselfInfo(params)) { + return; + } + this._othersTypingTimers.registerTimer({ + timeoutCallbackArguments: [params.partnerID], + timerID: params.partnerID, + }); + this._typingPartnerIDs.push(params.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: function (params) { + var 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: function (params) { + var partnerID = params.partnerID; + this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); + 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: function (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: function (params) { + return $.when(); + }, + /** + * Warn views that the list of users that are currently typing on this + * thread has been updated. + * + * @abstract + * @private + */ + _warnUpdatedTypingPartners: function () {}, + + //-------------------------------------------------------------------------- + // 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: function () { + 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: function () { + 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: function (params) { + var 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: function (partnerID) { + this.unregisterTyping({ partnerID: 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: function (message) { + var partnerID = message.hasAuthor() ? + message.getAuthorID() : + this._DEFAULT_TYPING_PARTNER_ID; + this.unregisterTyping({ partnerID: 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: function () { + this._lastNotifiedMyselfTyping = false; + this._throttleNotifyMyselfTyping.clear(); + this._myselfLongTypingTimer.clear(); + this._myselfTypingInactivityTimer.clear(); + }, +}; + +return ThreadTypingMixin; + +}); diff --git a/addons/mail/static/src/js/models/threads/thread_typing_mixin/timer.js b/addons/mail/static/src/js/models/threads/thread_typing_mixin/timer.js new file mode 100644 index 00000000000..a3236fd1a16 --- /dev/null +++ b/addons/mail/static/src/js/models/threads/thread_typing_mixin/timer.js @@ -0,0 +1,69 @@ +odoo.define('mail.model.Timer', function (require) { +"use strict"; + +var Class = require('web.Class'); + +/** + * This class creates a timer which, when times out, calls a function. + */ +var Timer = Class.extend({ + + /** + * Instantiate a new timer. Note that the timer is not started on + * initialization (@see start method). + * + * @param {Object} params + * @param {integer} params.duration duration of timer before timeout in + * milli-seconds. + * @param {function} params.onTimeout function that is called when the + * timer times out. + */ + init: function (params) { + this._duration = params.duration; + this._timeout = undefined; + this._timeoutCallback = params.onTimeout; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Clears the countdown of the timer. + */ + clear: function () { + clearTimeout(this._timeout); + }, + /** + * Resets the timer, i.e. resets its duration. + */ + reset: function () { + this.clear(); + this.start(); + }, + /** + * Starts the timer, i.e. after a certain duration, it times out and calls + * a function back. + */ + start: function () { + this._timeout = setTimeout(this._onTimeout.bind(this), this._duration); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * Called when the timer times out, calls back a function on timeout. + * + * @private + */ + _onTimeout: function () { + this._timeoutCallback(); + }, + +}); + +return Timer; + +}); diff --git a/addons/mail/static/src/js/models/threads/thread_typing_mixin/timers.js b/addons/mail/static/src/js/models/threads/thread_typing_mixin/timers.js new file mode 100644 index 00000000000..757945223f7 --- /dev/null +++ b/addons/mail/static/src/js/models/threads/thread_typing_mixin/timers.js @@ -0,0 +1,82 @@ +odoo.define('mail.model.Timers', function (require) { +"use strict"; + +var Timer = require('mail.model.Timer'); + +var Class = require('web.Class'); + +/** + * This class lists several timers that use a same callback and duration. + */ +var Timers = Class.extend({ + + /** + * Instantiate a new list of timers + * + * @param {Object} params + * @param {integer} params.duration duration of the underlying timers from + * start to timeout, in milli-seconds. + * @param {function} params.onTimeout a function to call back for underlying + * timers on timeout. + */ + init: function (params) { + this._duration = params.duration; + this._timeoutCallback = params.onTimeout; + this._timers = {}; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Register a timer with ID `timerID` to start. + * + * - an already registered timer with this ID is reset. + * - (optional) can provide a list of arguments that is passed to the + * function callback when timer times out. + * + * @param {Object} params + * @param {Array} [params.timeoutCallbackArguments] + * @param {integer} params.timerID + */ + registerTimer: function (params) { + var timerID = params.timerID; + if (this._timers[timerID]) { + this._timers[timerID].clear(); + } + var timerParams = { + duration: this._duration, + onTimeout: this._timeoutCallback, + }; + if ('timeoutCallbackArguments' in params) { + timerParams.onTimeout = this._timeoutCallback.bind.apply( + this._timeoutCallback, + [null].concat(params.timeoutCallbackArguments) + ); + } else { + timerParams.onTimeout = this._timeoutCallback; + } + this._timers[timerID] = new Timer(timerParams); + this._timers[timerID].start(); + }, + /** + * Unregister a timer with ID `timerID`. The unregistered timer is aborted + * and will not time out. + * + * @param {Object} params + * @param {integer} params.timerID + */ + unregisterTimer: function (params) { + var timerID = params.timerID; + if (this._timers[timerID]) { + this._timers[timerID].clear(); + delete this._timers[timerID]; + } + }, + +}); + +return Timers; + +}); |
