diff options
| author | Alexandre Kühn <aku@odoo.com> | 2018-06-12 15:46:46 +0200 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-07-02 15:06:54 +0200 |
| commit | cd34f6de727d5b3858420cff3c9d3c5995c4e75c (patch) | |
| tree | 8d25861f98a038b71728355375d634a5c178290e /addons/im_support/static/src/js | |
| parent | ee9c1f52c316c83775969a3c9038e7e9f560f7e7 (diff) | |
[REF] mail: JS mail refactoring
----------------------
Summary
----------------------
The purpose of this commit is to improve the JS code of the `mail` module.
It applies the new coding guidelines and makes some changes on the design
of some modules, such as the old ChatManager module.
Here is a short summary of the changes that have been made:
1. New coding guidelines
- snake_case to camelCase
- prefix private attributes and methods with '_'
- jsdoc on most methods
- one class per module
2. Rename/Merge some classes
- 'chat manager' becomes 'mail manager' (internal) and 'mail service' (external)
- 'chat window manager' is now included in mail manager
- 'thread' widget now named 'thread widget'
3. New model abstraction for mail objects:
- modules 'mail.model.*'
- modeling:
0..1 0..1 * *
ThreadWindow <------> Thread <-------> Message
/ \
/ \
Thread With Cache Document Thread
/ | \
/ | \
Mailbox Channel Support Channel
|
|
DM
- Thread: the superclass of threads.
- ThreadWindow: the window component of a thread.
- Message: mail objects representing messages.
- Thread With Cache: threads that can be used with search view (Discuss app compatible).
- Document Thread: represents the thread part of a chatter.
- Mailbox: represents what was previously called 'static' channel, e.g. 'Inbox'.
- Channel: mail objects representing channels, including livechat.
- DM: special kind of Channel for 1:1 communication in the backend.
- Support Channel: special channel for im_support module.
This new modeling approach let us easily add features on all threads, such as the
possibility to put any thread in a small window.
----------------------
Known issues
----------------------
[Already Present in Master]
1. When the Discuss app is in the background with 'Inbox' as the selected
Thread, when clicking on a document thread preview in the messaging menu
of the systray, the rainbow man appears.
2. When a document with the chatter is in the background, when receiving an
inbox notification from this document thread, the document thread is
automatically marked as read, which removes the notification right away.
3. Sometimes, opening a DM window from the "blank" thread window does not work.
4. Reply-to feature on Inbox is not working: no message is sent in the document
thread.
5. On the first login of admin user with demo data, the inbox counter is wrong
(it displays 6, instead of 3).
Explanation after investigation:
> On page load, it fetches the correct number of Inbox messages (3),
but the server notifies of 3 needaction messages right away,
so it wrongly assumes these are new needaction messages.
> Not possible for web client to detect that these messages should not
increment the Inbox counter while keeping same API.
6. Notifications for new document thread messages only work when the user sets
'handle with Odoo' for the Notification Management in the preferences.
> due to notifications on the longpoll bus for document thread
messages that come from needaction notifications.
> requires server-side changes to send notification on the longpoll
bus to mentionned user.
[New]
7. When receiving a message on a unjoined channel, thread window flickers
('open' > 'close' > 'open')
Explanation after investigation:
> JS logic:
a) On auto-join, ask server to join the channel and get channel infos.
b) The info tells the channel is not detached, but JS code makes decision
to detach it, and tells server the channel is now detached.
c) From (a), server notifies on longpoll bus the state of channel, which is
not detached. The web client thinks that the window state of the channel
has been changed somewhere else, and the channel is now closed.
d) From (b), server notifies on longpoll bus the state of channel, which is
detached. The web client opens the thread window of this channel.
> The flicker didn't occur before refactoring because the web client was only
updating the model of channel when it receives the longpoll notification.
> Server behaviour on 1st longpoll notification is necessary for cross-tab
synchronization for channel window state.
> New design implies that model and view should be synchronized, hence the
issue now.
> Solution: remove server-side thread window synchronization and replace with
client-side synchronization.
----------------------
Hacks
----------------------
The module `im_livechat` now uses mail objects that are compatible with Message
and Window objects:
Modeling for messages:
AbstractMessage
/ \
/ \
LivechatMessage Message
- AbstractMessage: message compatible with the thread widget.
- LivechatMessage: message used by im_livechat.
- Message: message used with the mail manager.
Modeling for thread windows:
AbstractThreadWindow
/ \
/ \
LivechatWindow ThreadWindow
- AbstractThreadWindow: behaviour share between all types of thread windows.
- LivechatWindow: window used by im_livechat.
- ThreadWindow: window used with mail_manager.
The reason for these hacks are twofold:
1. Use the thread widget in the frontend and livechat external lib bundles.
2. Do not have a dependency with the mail manager in the frontend and external
lib bundles.
Diffstat (limited to 'addons/im_support/static/src/js')
| -rw-r--r-- | addons/im_support/static/src/js/chat_manager.js | 455 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/discuss.js | 26 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/extended_chat_window.js | 47 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/mail_manager.js | 260 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/support_channel.js | 233 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/support_message.js | 67 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/support_session.js | 7 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/systray_messaging_menu.js (renamed from addons/im_support/static/src/js/systray.js) | 26 | ||||
| -rw-r--r-- | addons/im_support/static/src/js/thread_window.js | 67 |
9 files changed, 655 insertions, 533 deletions
diff --git a/addons/im_support/static/src/js/chat_manager.js b/addons/im_support/static/src/js/chat_manager.js deleted file mode 100644 index 4fabcd5deb9..00000000000 --- a/addons/im_support/static/src/js/chat_manager.js +++ /dev/null @@ -1,455 +0,0 @@ -odoo.define('im_support.ChatManager', function (require) { -"use strict"; - -/** - * This module includes the ChatManager to handle the case of the Support - * channel, allowing the users of the current database to communicate with - * livechat operators from another database (the Support database). - */ - -var ChatManager = require('mail.ChatManager'); - -var core = require('web.core'); -var session = require('web.session'); -var WebClient = require('web.WebClient'); - -var supportSession = require('im_support.SupportSession'); -var supportBus = require('im_support.SupportBus'); - -var _t = core._t; -var CHANNEL_STATES = { - CLOSED: 'closed', - FOLDED: 'folded', - OPEN: 'open', -}; -var ODOOBOT_ID = "ODOOBOT"; // FIXME: should be exported by the ChatManager -var POLL_TIMEOUT_DELAY = 1000 * 60 * 30; // 30 minutes -var POLL_TIMEOUT_KEY = 'im_support.poll_timeout'; -var SUPPORT_CHANNEL_LIMIT = 30; // limit of messages to fetch -var SUPPORT_CHANNEL_STATE_KEY = 'im_support.channel_state'; - -ChatManager.include({ - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - closeChatSession: function (channelID) { - if (channelID === this.supportChannelUUID) { - this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, CHANNEL_STATES.CLOSED); - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - detachChannel: function (channelID) { - if (channelID === this.supportChannelUUID) { - this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, CHANNEL_STATES.OPEN); - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - foldChannel: function (channelID, folded) { - if (channelID === this.supportChannelUUID) { - var value = folded ? CHANNEL_STATES.FOLDED : CHANNEL_STATES.OPEN; - this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, value); - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Overrides to filter out the Support channel from the previews. - * - * @override - */ - getChannelsPreview: function () { - var self = this; - return this._super.apply(this, arguments).then(function (channelsPreview) { - return _.reject(channelsPreview, {id: self.supportChannelUUID}); - }); - }, - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - getMentionPartnerSuggestions: function (channel) { - if (channel && channel.id === this.supportChannelUUID) { - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Initialises the Support: checks if there is a pending chat session - * between the user and Support, and if so, re-opens it. - * Note: we can't directly override init(), because it is already called - * when the include is applied, so we use this hook instead (called at - * webclient startup) - */ - initSupport: function () { - var self = this; - this.supportChannelDef = null; - this.supportChannelUUID = null; - this.pollTimeout = null; - this.pollingSupport = false; - - // listen to notifications coming from Support longpolling - supportBus.on('notification', this, this._onSupportNotification); - - // check if there is a pending chat session with the Support - var timeoutTimestamp = this.call('local_storage', 'getItem', POLL_TIMEOUT_KEY); - var pollingDelay = timeoutTimestamp && (JSON.parse(timeoutTimestamp) - Date.now()); - if (pollingDelay && pollingDelay > 0) { - var channelState = this.call('local_storage', 'getItem', SUPPORT_CHANNEL_STATE_KEY); - this.startSupportLivechat(channelState).then(function () { - var supportChannel = self.getChannel(self.supportChannelUUID); - if (supportChannel.available) { - self._startPollingSupport(pollingDelay); - } - }); - } - }, - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - markChannelAsSeen: function (channel) { - if (channel.id === this.supportChannelUUID) { - this._updateChannelUnreadCounter(channel, 0); - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Overrides to call a specific route for messages posted in the Support - * channel. - * - * @override - */ - postMessage: function (data, options) { - if (options.channelID && options.channelID === this.supportChannelUUID) { - // ensure that the poll is active before posting the message - if (!this.pollingSupport) { - this._startPollingSupport(); - } - return supportSession.rpc("/odoo_im_support/chat_post", { - uuid: this.supportChannelUUID, - message_content: data.content, - }); - } - return this._super.apply(this, arguments); - }, - /** - * Opens the support channel between a livechat operator from the Support - * database and the current user (if there is an available operator). - * Ensures to perform only once the request to create/retrieve the Support - * channel. - * - * @param {string} [channelState=CHANNEL_STATES.OPEN] state of the Support - * channel (see CHANNEL_STATES for accepted values) - * @returns {Deferred} - */ - startSupportLivechat: function (channelState) { - var self = this; - if (!this.supportChannelDef) { - // retrieve or create the channel - this.supportChannelDef = supportSession.rpc('/odoo_im_support/get_support_channel', { - channel_uuid: session.support_token, - db_uuid: session.db_uuid, - user_name: session.name, - }); - } - return this.supportChannelDef.then(function (channel) { - if (!channel) { - // there is no channel (because there is no online operator, and - // no support channel has been created yet), so create one to - // open in a chat window - channel = { - available: false, - support_channel: true, - type: 'livechat', - uuid: "support_unavailable", - }; - } - if (!channelState) { - channelState = self.discussOpen ? CHANNEL_STATES.CLOSED : CHANNEL_STATES.OPEN; - self.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, channelState); - } - if (!self.supportChannelUUID) { - // this part is only executed the first time the RPC is resolved - self.supportChannelUUID = channel.uuid; - - // add the channel to the ChatManager - self._addChannel(_.extend(channel, { - id: channel.uuid, - is_minimized: _.contains([CHANNEL_STATES.OPEN, CHANNEL_STATES.FOLDED], channelState), - state: channelState, - })); - - // display automatic messages in the channel - if (!channel.available) { - self._addSupportNotAvailableMessage(); - } else { - self._addSupportWelcomeMessage(); - } - } else { - // the channel has already been added to the ChatManager, so - // simply re-open it - channel = self.getChannel(self.supportChannelUUID); - if (self.discussOpen) { - self.openChannel(channel); - } else { - channel.is_folded = channelState === CHANNEL_STATES.FOLDED; - self.call('chat_window_manager', 'openChat', channel); - } - } - }).fail(function () { - self.do_warn(_t("The Support server can't be reached.")); - }); - }, - /** - * Overrides to prevent from calling the server for messages of the Support - * channel (which are records on the Support database). - * - * @override - */ - toggleStarStatus: function (msgID) { - var message = this.getMessage(msgID); - if (_.contains(message.channel_ids, this.supportChannelUUID)) { - return $.when(); - } - return this._super.apply(this, arguments); - }, - /** - * Overrides to prevent from calling the server with the Support channel - * (which is a record on the Support database). - * - * @override - */ - unsubscribe: function (channel) { - if (channel.id === this.supportChannelUUID) { - return $.when(); - } - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Adds a message in the Support channel indicating that this channel is - * not available for now. - * - * @private - */ - _addSupportNotAvailableMessage: function () { - var msg = { - author_id: ODOOBOT_ID, - body: _t("None of our operators are available. <a href='https://www.odoo.com/help' " + - "target='_blank'>Submit a ticket</a> to ask your question now."), - channel_ids: [this.supportChannelUUID], - id: Number.MAX_SAFE_INTEGER, // last message in the channel - }; - this._addMessage(msg, {silent: true}); - }, - /** - * Adds a welcome message as first message of the Support channel. - * - * @private - */ - _addSupportWelcomeMessage: function () { - var channel = this.getChannel(this.supportChannelUUID); - if (channel.welcome_message) { - var msg = { - author_id: channel.operator, - body: channel.welcome_message, - channel_ids: [this.supportChannelUUID], - id: -1, // first message of the channel - }; - this._addMessage(msg, {silent: true}); - } - }, - /** - * Overrides to handle the Support channel case: fetches the messages from - * the Support database in that case. - * - * @override - * @private - */ - _fetchFromChannel: function (channel, options) { - var self = this; - if (channel.id === this.supportChannelUUID) { - var domain = options && options.domain || []; - var cache = this._getChannelCache(channel, domain); - if (options && options.loadMore) { - var minMessageID = cache.messages[0].id; - domain = [['id', '<', minMessageID]].concat(domain); - } - - return supportSession.rpc('/odoo_im_support/fetch_messages', { - domain: domain, - channel_uuid: session.support_token, - limit: SUPPORT_CHANNEL_LIMIT, - }).then(function (msgs) { - if (!cache.all_history_loaded) { - cache.all_history_loaded = msgs.length < SUPPORT_CHANNEL_LIMIT; - } - cache.loaded = true; - - _.each(msgs, function (msg) { - _.extend(msg, {channel_ids: [self.supportChannelUUID]}); - self._addMessage(msg, { - channel_id: self.supportChannelUUID, - silent: true, - domain: domain, - }); - }); - var channelCache = self._getChannelCache(channel, domain); - return channelCache.messages; - }); - } - return this._super.apply(this, arguments); - }, - /** - * @override - * @private - */ - _makeChannel: function (data) { - var channel = this._super.apply(this, arguments); - if (channel.id === this.supportChannelUUID) { - channel.supportChannel = true; - channel.name = _t('Support'); - channel.available = data.available; - if (!channel.available) { - channel.name += _t(' (offline)'); - } - channel.welcome_message = data.welcome_message; - channel.operator = data.operator; - } - return channel; - }, - /** - * @override - * @private - */ - _makeMessage: function (data) { - if (_.contains(data.channel_ids, this.supportChannelUUID) && data.author_id !== ODOOBOT_ID) { - if (!data.author_id[0]) { // the author is the client - data.author_id = [session.partner_id, session.name]; - } else { // the author is the operator - data.author_id[0] = -1; // prevent from conflicting with partners of this instance - var msg = this._super.apply(this, arguments); - msg.author_redirect = false; - msg.avatar_src = "/mail/static/src/img/odoo_o.png"; - return msg; - } - } - return this._super.apply(this, arguments); - }, - /** - * Automatically stop polling for Support messages after a given delay of - * inactivity. - * - * @private - * @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling - * timeout delay to set - */ - _setPollTimeout: function (pollingDelay) { - pollingDelay = pollingDelay || POLL_TIMEOUT_DELAY; - clearTimeout(this.pollTimeout); - this.pollTimeout = setTimeout(this._stopPollingSupport.bind(this), pollingDelay); - // save the timeout expiration datetime into the LocalStorage so that - // we can re-open the Support channel if necessary on F5 - var timeoutTimestamp = Date.now() + pollingDelay; - this.call('local_storage', 'setItem', POLL_TIMEOUT_KEY, timeoutTimestamp); - }, - /** - * Initiates a longpoll with the server hosting the Support channel. - * - * @private - * @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling - * timeout delay to set - */ - _startPollingSupport: function (pollingDelay) { - this.pollingSupport = true; - supportBus.add_channel(this.supportChannelUUID); - supportBus.start_polling(); - this._setPollTimeout(pollingDelay); - }, - /** - * Stops the longpoll with the server hosting the Support channel. - * - * @private - */ - _stopPollingSupport: function () { - this.pollingSupport = false; - supportBus.stop_polling(); - this.call('local_storage', 'removeItem', POLL_TIMEOUT_KEY); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Handles poll notifications from the Support server. - * - * @private - * @param {Object[]} notifications - */ - _onSupportNotification: function (notifications) { - var self = this; - if (notifications && notifications.length) { - this._setPollTimeout(); - } - _.each(notifications, function (notification) { - if (notification[1]._type === 'history_command') { - // ignore history requests - return; - } - var msg = _.extend(notification[1], { - channel_ids: [self.supportChannelUUID], - }); - self._manageChannelNotification(msg); - }); - }, -}); - - -// Unfortunately, we can't override init() of ChatManager because it is called -// before the include is applied, so we override the WebClient instead to call -// an initialization hook for Livechat Support in the ChatManager service. -WebClient.include({ - /** - * Overrides to ask the ChatManager service to check whether there is a - * pending chat session with Support, and if so, to re-open it. - * - * @override - */ - show_application: function () { - this.call('chat_manager', 'initSupport'); - return this._super.apply(this, arguments); - }, -}); - -}); diff --git a/addons/im_support/static/src/js/discuss.js b/addons/im_support/static/src/js/discuss.js index 0f29370b049..8f26f272346 100644 --- a/addons/im_support/static/src/js/discuss.js +++ b/addons/im_support/static/src/js/discuss.js @@ -1,13 +1,13 @@ -odoo.define('im_support.chat_discuss', function (require) { +odoo.define('im_support.Discuss', function (require) { "use strict"; +var Discuss = require('mail.Discuss'); + /** * This module includes Discuss to handle the case of the Support channel. */ - -var Discuss = require('mail.chat_discuss'); - Discuss.include({ + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -21,8 +21,8 @@ Discuss.include({ */ _getThreadRenderingOptions: function () { var options = this._super.apply(this, arguments); - if (this.channel.supportChannel) { - options.display_stars = false; + if (this._thread.getType() === 'support_channel') { + options.displayStars = false; } return options; }, @@ -34,13 +34,13 @@ Discuss.include({ * @override * @private */ - _setChannel: function (channel) { + _setThread: function () { var self = this; return this._super.apply(this, arguments).then(function () { - var $buttonAddAttachment = self.basicComposer.$('.o_composer_button_add_attachment'); - if (channel.supportChannel) { - if (!channel.available) { - self.basicComposer.do_hide(); + var $buttonAddAttachment = self._basicComposer.$('.o_composer_button_add_attachment'); + if (self._thread.getType() === 'support_channel') { + if (!self._thread.isAvailable()) { + self._basicComposer.do_hide(); } $buttonAddAttachment.toggleClass('o_hidden', true); } else { @@ -54,9 +54,9 @@ Discuss.include({ * @override * @private */ - _updateControlPanelButtons: function (channel) { + _updateControlPanelButtons: function (thread) { this._super.apply(this, arguments); - if (channel.supportChannel) { + if (thread.getType() === 'support_channel') { this.$buttons.find('button').hide(); } }, diff --git a/addons/im_support/static/src/js/extended_chat_window.js b/addons/im_support/static/src/js/extended_chat_window.js deleted file mode 100644 index 6f598d16cba..00000000000 --- a/addons/im_support/static/src/js/extended_chat_window.js +++ /dev/null @@ -1,47 +0,0 @@ -odoo.define('im_support.ExtendedChatWindow', function (require) { -"use strict"; - -/** - * This module includes ExtendedChatWindow to handle the case of the Support - * channel. - */ - - -var ExtendedChatWindow = require('mail.ExtendedChatWindow'); - -ExtendedChatWindow.include({ - /** - * Overrides to tweak the options in the case of the Support channel (no - * star, and no input if no operator available). - * - * @override - */ - init: function () { - this._super.apply(this, arguments); - var channel = this.call('chat_manager', 'getChannel', this.channel_id); - if (channel && channel.supportChannel) { - this.options.display_stars = false; - if (!channel.available) { - this.options.input_less = true; - } - } - }, - /** - * Overrides to remove the attachment button in the Support channel as this - * feature does not work in that channel. - * - * @note: this feature could be enabled by storing the attachments on the - * Support server. - * @override - */ - start: function () { - var def = this._super.apply(this, arguments); - var channel = this.call('chat_manager', 'getChannel', this.channel_id); - if (channel && channel.supportChannel) { - this.$('.o_composer .o_composer_button_add_attachment').remove(); - } - return def; - }, -}); - -}); diff --git a/addons/im_support/static/src/js/mail_manager.js b/addons/im_support/static/src/js/mail_manager.js new file mode 100644 index 00000000000..b25b47cc1ba --- /dev/null +++ b/addons/im_support/static/src/js/mail_manager.js @@ -0,0 +1,260 @@ +odoo.define('im_support.MailManager', function (require) { +"use strict"; + +var MailManager = require('mail.Manager'); + +var core = require('web.core'); +var session = require('web.session'); +var WebClient = require('web.WebClient'); + +var SupportChannel = require('im_support.SupportChannel'); +var SupportMessage = require('im_support.SupportMessage'); +var supportSession = require('im_support.SupportSession'); +var supportBus = require('im_support.SupportBus'); + +var _t = core._t; + +var POLL_TIMEOUT_DELAY = 1000 * 60 * 30; // 30 minutes +var POLL_TIMEOUT_KEY = 'im_support.poll_timeout'; +var SUPPORT_CHANNEL_STATE_KEY = 'im_support.channel_state'; + +/** + * This module includes the MailService to handle the case of the Support + * channel, allowing the users of the current database to communicate with + * livechat operators from another database (the Support database). + */ +MailManager.include({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Initialises the Support: checks if there is a pending chat session + * between the user and Support, and if so, re-opens it. + * Note: we can't directly override init(), because it is already called + * when the include is applied, so we use this hook instead (called at + * webclient startup) + */ + initSupport: function () { + var self = this; + this.supportChannelDef = null; + this.supportChannelUUID = null; + this.pollTimeout = null; + this.pollingSupport = false; + + // listen to notifications coming from Support longpolling + supportBus.on('notification', this, this._onSupportNotification); + + // check if there is a pending chat session with the Support + var timeoutTimestamp = this.call('local_storage', 'getItem', POLL_TIMEOUT_KEY); + var pollingDelay = timeoutTimestamp && (JSON.parse(timeoutTimestamp) - Date.now()); + if (pollingDelay && pollingDelay > 0) { + var channelState = this.call('local_storage', 'getItem', SUPPORT_CHANNEL_STATE_KEY); + this.startSupportLivechat(channelState).then(function () { + var supportChannel = self.getChannel(self.supportChannelUUID); + if (supportChannel.isAvailable()) { + self.startPollingSupport(pollingDelay); + } + }); + } + }, + /** + * Overrides to filter out the Support channel from the previews. + * + * @override + * @returns {$.Promise<Object[]>} list of valid objects for mail.Preview + * template + */ + getChannelPreviews: function () { + var self = this; + return this._super.apply(this, arguments) + .then(function (channelsPreview) { + return _.reject(channelsPreview, { id: self.supportChannelUUID }); + }); + }, + /** + * Initiates a longpoll with the server hosting the Support channel. + * + * @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling + * timeout delay to set + */ + startPollingSupport: function (pollingDelay) { + if (!this.pollingSupport) { + this.pollingSupport = true; + supportBus.add_channel(this.supportChannelUUID); + supportBus.start_polling(); + this._setPollTimeout(pollingDelay); + } + }, + /** + * Opens the Support channel between a livechat operator from the Support + * database and the current user (if there is an available operator). + * Ensures to perform only once the request to create/retrieve the Support + * channel. + * + * @param {string} [channelState='open'] state of the Support + * channel (see CHANNEL_STATES for accepted values) + * @returns {Deferred} + */ + startSupportLivechat: function (channelState) { + var self = this; + if (!this.supportChannelDef) { + // retrieve or create the channel + this.supportChannelDef = supportSession.rpc('/odoo_im_support/get_support_channel', { + channel_uuid: session.support_token, + db_uuid: session.db_uuid, + user_name: session.name, + }); + } + return this.supportChannelDef.then(function (channel) { + if (!channel) { + // there is no channel (because there is no online operator, and + // no support channel has been created yet), so create one to + // open in a chat window + channel = { + available: false, + support_channel: true, + type: 'livechat', + uuid: "support_unavailable", + }; + } + if (!channelState) { + channelState = self._isDiscussOpen() ? 'closed' : 'open'; + self.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, channelState); + } + if (!self.supportChannelUUID) { + // this part is only executed the first time the RPC is resolved + self.supportChannelUUID = channel.uuid; + + // add the channel to the MailManager + self._addChannel(_.extend(channel, { + id: channel.uuid, + is_minimized: _.contains(['open', 'folded'], channelState), + state: channelState, + })); + + // display an automatic message in the channel + var supportChannel = self.getChannel(channel.uuid); + supportChannel.addDefaultMessage(); + } else { + // the channel has already been added to the MailManager, so + // simply re-open it + if (self._isDiscussOpen()) { + self._openThreadInDiscuss(self.supportChannelUUID); + } else { + channel = self.getChannel(self.supportChannelUUID); + channel.fold(channelState === 'folded'); + } + } + }).fail(function () { + self.do_warn(_t("The Support server can't be reached.")); + }); + }, + /** + * Updates the state of the Support channel (stored in the localStorage). + * + * @param {string} state ('closed', 'folded' or 'open') + */ + updateSupportChannelState: function (state) { + this.call('local_storage', 'setItem', SUPPORT_CHANNEL_STATE_KEY, state); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _makeChannel: function (data, options) { + if (data.id === this.supportChannelUUID) { + return new SupportChannel(this, data, options, this._commands); + } + return this._super.apply(this, arguments); + }, + /** + * Overrides to instantiate a SupportMessage when necessary. + * @override + */ + _makeMessage: function (data) { + if (_.contains(data.channel_ids, this.supportChannelUUID)) { + return new SupportMessage(this, data, this._emojis); + } + return this._super.apply(this, arguments); + }, + /** + * Automatically stop polling for Support messages after a given delay of + * inactivity. + * + * @private + * @param {integer} [pollingDelay=POLL_TIMEOUT_DELAY] the longpolling + * timeout delay to set + */ + _setPollTimeout: function (pollingDelay) { + pollingDelay = pollingDelay || POLL_TIMEOUT_DELAY; + clearTimeout(this.pollTimeout); + this.pollTimeout = setTimeout(this._stopPollingSupport.bind(this), pollingDelay); + // save the timeout expiration datetime into the LocalStorage so that + // we can re-open the Support channel if necessary on F5 + var timeoutTimestamp = Date.now() + pollingDelay; + this.call('local_storage', 'setItem', POLL_TIMEOUT_KEY, timeoutTimestamp); + }, + /** + * Stops the longpoll with the server hosting the Support channel. + * + * @private + */ + _stopPollingSupport: function () { + this.pollingSupport = false; + supportBus.stop_polling(); + this.call('local_storage', 'removeItem', POLL_TIMEOUT_KEY); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Handles poll notifications from the Support server. + * + * @private + * @param {Object[]} notifications + */ + _onSupportNotification: function (notifications) { + var self = this; + if (notifications && notifications.length) { + this._setPollTimeout(); + } + _.each(notifications, function (notification) { + if (notification[1]._type === 'history_command') { + // ignore history requests + return; + } + var message = _.extend(notification[1], { + channel_ids: [self.supportChannelUUID], + }); + self._handleChannelNotification(message); + }); + }, +}); + + +// Unfortunately, we can't override init() of MailService because it is called +// before the include is applied, so we override the WebClient instead to call +// an initialization hook for Livechat Support in the Mail service. +WebClient.include({ + /** + * Overrides to ask the Mail service to check whether there is a + * pending chat session with Support, and if so, to re-open it. + * + * @override + */ + show_application: function () { + this.call('mail_service', 'initSupport'); + return this._super.apply(this, arguments); + }, +}); + +}); + diff --git a/addons/im_support/static/src/js/support_channel.js b/addons/im_support/static/src/js/support_channel.js new file mode 100644 index 00000000000..adcf2a0c51e --- /dev/null +++ b/addons/im_support/static/src/js/support_channel.js @@ -0,0 +1,233 @@ +odoo.define('im_support.SupportChannel', function (require) { +"use strict"; + +var supportSession = require('im_support.SupportSession'); + +var ThreadWithCache = require('mail.model.ThreadWithCache'); + +var core = require('web.core'); +var session = require('web.session'); + +var _t = core._t; + +/** + * This mail model represents support channel, which are communication channels + * between two different databases for support-related reasons. It is like + * livechat, but both users are in different databases, and both users + * communicate from their respective 'backend' access. + * + * FIXME: it should inherit from mail.model.Channel, not from + * mail.model.ThreadWithCache + */ +var SupportChannel = ThreadWithCache.extend({ + init: function (parent, data, options) { + + data.type = 'support_channel'; + data.name = _t("Support"); + + this._available = data.available; + this._operator = data.operator; + this._supportChannelUUID = data.id; + this._welcomeMessage = data.welcome_message; + if (!this._available) { + data.name += _t(" (offline)"); + } + + this._super.apply(this, arguments); + + // force stuff that should probably be in Thread (or at least + // ThreadWithCache), but that are currently in Channel + this._detached = data.is_minimized; + this._folded = data.state === 'folded'; + this._uuid = data.uuid; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Adds the default message in the Support channel (depending on its + * availability). + */ + addDefaultMessage: function () { + if (!this._available) { + this._addSupportNotAvailableMessage(); + } else { + this._addSupportWelcomeMessage(); + } + }, + /** + * Overrides to store the state of the Support channel in the localStorage. + * + * @override + */ + close: function () { + this._super.apply(this, arguments); + this.call('mail_service', 'updateSupportChannelState', 'closed'); + }, + /** + * Overrides to store the state of the Support channel in the localStorage. + * + * @override + */ + detach: function () { + this._super.apply(this, arguments); + this.call('mail_service', 'updateSupportChannelState', 'open'); + }, + /** + * Overrides to store the state of the Support channel in the localStorage. + * + * @override + */ + fold: function (folded) { + this._super.apply(this, arguments); + var value = folded ? 'folded' : 'open'; + this.call('mail_service', 'updateSupportChannelState', value); + }, + /** + * FIXME: this method is necessary just because the support channel is + * considered as a channel, even though it does not inherit from + * mail.model.Channel. + * + * @returns {boolean} + */ + hasBeenPreviewed: function () { + return true; + }, + /** + * @returns {boolean} true iff the Support channel is available + */ + isAvailable: function () { + return this._available; + }, + /** + * FIXME: this override is necessary just because the support channel is + * considered as a channel, even though it does not inherit from + * mail.model.Channel. + * + * @override + * @returns {boolean} + */ + isChannel: function () { + return true; + }, + /** + * Support channels are not considered as chat, so they are not displayed + * in the 'chat' filter of the systray messaging menu. + * + * @override + * @returns {boolean} + */ + isChat: function () { + return false; + }, + /** + * FIXME: this override is necessary just because the support channel is + * considered as a channel, even though it does not inherit from + * mail.model.Channel. + * + * @returns {integer} + */ + getNeedactionCounter: function () { + return 0; + }, + /** + * @return {string} uuid of this channel + */ + getUUID: function () { + return this._uuid; + }, + /** + * Posts the message on the Support server. + * + * @override + * @return {$.Promise} + */ + postMessage: function (data) { + // ensure that the poll is active before posting the message + this.call('mail_service', 'startPollingSupport'); + return supportSession.rpc('/odoo_im_support/chat_post', { + uuid: this._supportChannelUUID, + message_content: data.content, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Adds a message in the Support channel indicating that this channel is + * not available for now. + * + * @private + */ + _addSupportNotAvailableMessage: function () { + var msg = { + author_id: this.call('mail_service', 'getOdoobotID'), + body: _t("None of our operators are available. <a href='https://www.odoo.com/help' " + + "target='_blank'>Submit a ticket</a> to ask your question now."), + channel_ids: [this.getID()], + id: Number.MAX_SAFE_INTEGER, // last message in the channel + }; + this.call('mail_service', 'addMessage', msg, { silent: true }); + }, + /** + * Adds the welcome message as first message of the Support channel. + * + * @private + */ + _addSupportWelcomeMessage: function () { + if (this._welcomeMessage) { + var msg = { + author_id: this._operator, + body: this._welcomeMessage, + channel_ids: [this.getID()], + id: -1, // first message of the channel + }; + this.call('mail_service', 'addMessage', msg, { silent: true }); + } + }, + /** + * Fetches the messages from the Support server. + * + * @override + * @private + */ + _fetchMessages: function (channel, options) { + var self = this; + var domain = options && options.domain || []; + var cache = this._getCache(channel, domain); + if (options && options.loadMore) { + var minMessageID = cache.messages[0].id; + domain = [['id', '<', minMessageID]].concat(domain); + } + + return supportSession.rpc('/odoo_im_support/fetch_messages', { + domain: domain, + channel_uuid: session.support_token, + limit: self._FETCH_LIMIT, + }).then(function (msgs) { + if (!cache.all_history_loaded) { + cache.all_history_loaded = msgs.length < self._FETCH_LIMIT; + } + cache.loaded = true; + + _.each(msgs, function (msg) { + _.extend(msg, {channel_ids: [self.getID()]}); + self.call('mail_service', 'addMessage', msg, { + channel_id: self.getID(), + silent: true, + domain: domain, + }); + }); + var channelCache = self._getCache(channel, domain); + return channelCache.messages; + }); + }, +}); + +return SupportChannel; + +}); diff --git a/addons/im_support/static/src/js/support_message.js b/addons/im_support/static/src/js/support_message.js new file mode 100644 index 00000000000..df03d0fa477 --- /dev/null +++ b/addons/im_support/static/src/js/support_message.js @@ -0,0 +1,67 @@ +odoo.define('im_support.SupportMessage', function (require) { +"use strict"; + +var Message = require('mail.model.Message'); + +var session = require('web.session'); + +/** + * This is a model for messages that are in the the support channel + */ +var SupportMessage = Message.extend({ + /** + * @override + * @param {Class} parent + * @param {Object} data + * @param {Array} [data.author_id=[]] + */ + init: function (parent, data) { + data.author_id = data.author_id || []; + if (data.author_id !== this.call('mail_service', 'getOdoobotID')) { + if (!data.author_id[0]) { + // the author is the client + data.author_id = [session.partner_id, session.name]; + } else { + // the author is the operator + // prevent from conflicting with partners of this instance + data.author_id[0] = -1; + } + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {string} + */ + getAvatarSource: function () { + return '/mail/static/src/img/odoo_o.png'; + }, + /** + * Overrides to prevent clicks on the Support operator from redirecting. + * + * @override + * @returns {boolean} + */ + shouldRedirectToAuthor: function () { + return false; + }, + /** + * Overrides to prevent from calling the server for messages of the Support + * channel (which are records on the Support database). + * + * @override + * @returns {$.Promise} + */ + toggleStarStatus: function () { + return $.when(); + }, +}); + +return SupportMessage; + +}); diff --git a/addons/im_support/static/src/js/support_session.js b/addons/im_support/static/src/js/support_session.js index d1228674737..1c6268c40db 100644 --- a/addons/im_support/static/src/js/support_session.js +++ b/addons/im_support/static/src/js/support_session.js @@ -1,15 +1,14 @@ odoo.define('im_support.SupportSession', function (require) { "use strict"; +var session = require('web.session'); +var Session = require('web.Session'); + /** * This module returns an instance of Session which is linked to the Support * server, allowing the current instance to communicate with the Support * server (CORS). */ - -var session = require('web.session'); -var Session = require('web.Session'); - return new Session(null, session.support_origin, { modules: odoo._modules, use_cors: true, diff --git a/addons/im_support/static/src/js/systray.js b/addons/im_support/static/src/js/systray_messaging_menu.js index 99c6f7f077b..c6779dfbb4e 100644 --- a/addons/im_support/static/src/js/systray.js +++ b/addons/im_support/static/src/js/systray_messaging_menu.js @@ -1,18 +1,13 @@ -odoo.define('im_support.systray', function (require) { +odoo.define('im_support.systray.MessagingMenu', function (require) { "use strict"; -/** - * This module adds a fake Support channel in the messaging menu of the systray. - */ - -var mailSystray = require('mail.systray'); +var MessagingMenu = require('mail.systray.MessagingMenu'); var config = require('web.config'); var core = require('web.core'); var session = require('web.session'); var _t = core._t; -var MessagingMenu = mailSystray.MessagingMenu; var SUPPORT_CHANNEL_ID = 'SupportChannel'; // Disable Support in mobile for design purposes (for now at least), and don't @@ -21,6 +16,9 @@ if (config.device.isMobile || !session.support_token || !session.support_origin) return; } +/** + * This module adds a fake Support channel in the messaging menu of the systray. + */ MessagingMenu.include({ /** * @override @@ -29,8 +27,8 @@ MessagingMenu.include({ this._super.apply(this, arguments); this.supportChannel = { id: SUPPORT_CHANNEL_ID, - name: _t('Support'), - image_src: '/mail/static/src/img/odoo_o.png', + name: _t("Support"), + imageSRC: '/mail/static/src/img/odoo_o.png', }; }, /** @@ -45,7 +43,7 @@ MessagingMenu.include({ start: function () { var self = this; return this._super.apply(this, arguments).then(function () { - self.$('.o_mail_navbar_dropdown_bottom').addClass('o_mail_navbar_dropdown_channels'); + self.$('.o_mail_systray_dropdown_bottom').addClass('o_mail_systray_dropdown_items'); }); }, @@ -59,10 +57,10 @@ MessagingMenu.include({ * @override * @private */ - _onClickChannel: function (ev) { - var channelID = $(ev.currentTarget).data('channel_id'); - if (channelID === SUPPORT_CHANNEL_ID) { - this.call('chat_manager', 'startSupportLivechat'); + _onClickPreview: function (ev) { + var id = $(ev.currentTarget).data('preview-id'); + if (id === SUPPORT_CHANNEL_ID) { + this.call('mail_service', 'startSupportLivechat'); } else { this._super.apply(this, arguments); } diff --git a/addons/im_support/static/src/js/thread_window.js b/addons/im_support/static/src/js/thread_window.js new file mode 100644 index 00000000000..9173ef21d41 --- /dev/null +++ b/addons/im_support/static/src/js/thread_window.js @@ -0,0 +1,67 @@ +odoo.define('im_support.ThreadWindow', function (require) { +"use strict"; + +/** + * This module includes ThreadWindow to handle the case of the Support channel. + */ +var ThreadWindow = require('mail.ThreadWindow'); + +ThreadWindow.include({ + /** + * Overrides to tweak the options in the case of the Support channel (no + * star). + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + var channel = this.call('mail_service', 'getChannel', this._getThreadID()); + if (channel && channel.getType() === 'support_channel') { + this.options.displayStars = false; + } + }, + /** + * Overrides to remove the attachment button in the Support channel as this + * feature does not work in that channel. + * + * @note: this feature could be enabled by storing the attachments on the + * Support server. + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + var channel = this.call('mail_service', 'getChannel', this._getThreadID()); + if (channel && channel.getType() === 'support_channel') { + this.$('.o_composer .o_composer_button_add_attachment').remove(); + if (!channel.isAvailable()) { + this.$('.o_thread_composer').remove(); + } + } + return def; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Overrides to prevent from displaying a composer if there is no operator + * available. + * + * @override + * @private + */ + _needsComposer: function () { + var channel = this.call('mail_service', 'getChannel', this._getThreadID()); + if ( + channel && + channel.getType() === 'support_channel' && + !channel.isAvailable() + ) { + return false; + } + return this._super.apply(this, arguments); + }, +}); + +}); |
