From 07a261db7196d3f7aa7d50d98575c5b6efca9242 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Mon, 7 Jan 2019 11:19:00 +0000 Subject: [PATCH 1/8] [IMP] bus, mail, mail_bot: add im_status service This commit is a refactoring of im_status management. This commit also add im_status in two places, near the author in a mail thread and on each suggestion when using mentions. A service to manage im_status will have multiple benefits here: -centralise information, avoid to call the server multiple time for the same im_status -update all im_status at once and keep consistency in display. With this commit, the im_status updates are now done by rpc call. Updates where previously made with the bus but this has some drawback, since the bus will only give the information 50 seconds after the beginning of the request, in the worst case, we van wait 2*50 seconds to get an update. More than that, the im_status where only updates for pinned dm_chat. Dm chat are synchronized cross tabs, making the use of the bus possible for this purpose. Since we will need to display im_status not linked to dm_chat, the list to update will be different from tab to tab making the use of bus difficult for this purpose. Technical notes: -im_search has been moved from bus to mail addons since it concerns mail.channel -Update of an im_status should be reflected everywhere in the page. Since im_status is a rendered template used in multiple widget, we should add the correct logic to all concerened widget. The current solution is simple: use a jquery selector to find every place where im_status is rendered. -we add a new im_status: im_partner. This will indicate that that the partner has no user linked to him, making it possible to avoid to ask for status updates for this partner. -The update will only be done when the tab is focused. (and will be done asap once the tab get focus back) --- addons/bus/models/bus.py | 13 +- addons/bus/models/res_partner.py | 46 +--- addons/bus/static/src/js/longpolling_bus.js | 6 - addons/mail/models/res_partner.py | 37 +++ .../static/src/js/composers/basic_composer.js | 10 +- addons/mail/static/src/js/discuss.js | 9 +- .../js/models/messages/abstract_message.js | 8 + .../static/src/js/models/messages/message.js | 10 + .../static/src/js/models/threads/dm_chat.js | 15 +- .../static/src/js/services/mail_manager.js | 8 - .../js/services/mail_notification_manager.js | 18 -- .../static/src/js/services/mail_service.js | 1 + .../src/js/services/mail_status_manager.js | 216 ++++++++++++++++++ .../src/js/services/mail_window_manager.js | 41 ++-- addons/mail/static/src/scss/composer.scss | 3 + addons/mail/static/src/xml/composer.xml | 4 + addons/mail/static/src/xml/discuss.xml | 15 +- addons/mail/static/src/xml/thread.xml | 18 +- addons/mail/static/tests/chatter_tests.js | 16 +- addons/mail/static/tests/discuss_tests.js | 12 + .../static/tests/mail_status_manager_tests.js | 213 +++++++++++++++++ addons/mail/views/mail_templates.xml | 5 +- addons/mail_bot/models/res_partner.py | 1 + addons/mail_bot/views/discuss.xml | 5 - 24 files changed, 603 insertions(+), 127 deletions(-) create mode 100644 addons/mail/static/src/js/services/mail_status_manager.js create mode 100644 addons/mail/static/tests/mail_status_manager_tests.js diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index 7a7f8b497db..8187e2d43e6 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -69,7 +69,7 @@ class ImBus(models.Model): self.sendmany([[channel, message]]) @api.model - def poll(self, channels, last=0, options=None, force_status=False): + def poll(self, channels, last=0, options=None): if options is None: options = {} # first poll return the notification in the 'buffer' @@ -89,15 +89,6 @@ class ImBus(models.Model): 'channel': json.loads(notif['channel']), 'message': json.loads(notif['message']), }) - - if result or force_status: - partner_ids = options.get('bus_presence_partner_ids') - if partner_ids: - partners = self.env['res.partner'].browse(partner_ids) - result += [{ - 'id': -1, - 'channel': (self._cr.dbname, 'bus.presence'), - 'message': {'id': r.id, 'im_status': r.im_status}} for r in partners] return result @@ -145,7 +136,7 @@ class ImDispatch(object): event.wait(timeout=timeout) with registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) - notifications = env['bus.bus'].poll(channels, last, options, force_status=True) + notifications = env['bus.bus'].poll(channels, last, options) except Exception: # timeout pass diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py index a476e0c08dc..3f67d2d1c9f 100644 --- a/addons/bus/models/res_partner.py +++ b/addons/bus/models/res_partner.py @@ -15,50 +15,16 @@ class ResPartner(models.Model): self.env.cr.execute(""" SELECT U.partner_id as id, - CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' - ELSE 'online' + CASE WHEN max(B.last_poll) IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' + ELSE 'online' END as status FROM bus_presence B - JOIN res_users U ON B.user_id = U.id + RIGHT JOIN res_users U ON B.user_id = U.id WHERE U.partner_id IN %s AND U.active = 't' GROUP BY U.partner_id """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) for partner in self: - partner.im_status = res.get(partner.id, 'offline') - - @api.model - def im_search(self, name, limit=20): - """ Search partner with a name and return its id, name and im_status. - Note : the user must be logged - :param name : the partner name to search - :param limit : the limit of result to return - """ - # This method is supposed to be used only in the context of channel creation or - # extension via an invite. As both of these actions require the 'create' access - # right, we check this specific ACL. - if self.env['mail.channel'].check_access_rights('create', raise_exception=False): - name = '%' + name + '%' - excluded_partner_ids = [self.env.user.partner_id.id] - self.env.cr.execute(""" - SELECT - U.id as user_id, - P.id as id, - P.name as name, - CASE WHEN B.last_poll IS NULL THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' - ELSE 'online' - END as im_status - FROM res_users U - JOIN res_partner P ON P.id = U.partner_id - LEFT JOIN bus_presence B ON B.user_id = U.id - WHERE P.name ILIKE %s - AND P.id NOT IN %s - AND U.active = 't' - LIMIT %s - """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) - return self.env.cr.dictfetchall() - else: - return {} + partner.im_status = res.get(partner.id, 'im_partner') # if not found, it is a partner, useful to avoid to refresh status in js diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index 4764e2e52b0..a62f4897391 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -37,7 +37,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, { // bus presence this._lastPresenceTime = new Date().getTime(); - this._lastPartnersPresenceCheck = this._lastPresenceTime; $(window).on("focus." + this._id, this._onFocusChange.bind(this, {focus: true})); $(window).on("blur." + this._id, this._onFocusChange.bind(this, {focus: false})); $(window).on("unload." + this._id, this._onFocusChange.bind(this, {focus: false})); @@ -177,11 +176,6 @@ var LongpollingBus = Bus.extend(ServicesMixin, { var options = _.extend({}, this._options, { bus_inactivity: now - this._getLastPresence(), }); - if (this._lastPartnersPresenceCheck + this.PARTNERS_PRESENCE_CHECK_PERIOD > now) { - options = _.omit(options, 'bus_presence_partner_ids'); - } else { - this._lastPartnersPresenceCheck = now; - } var data = {channels: this._channels, last: this._lastNotificationID, options: options}; // The backend has a maximum cycle time of 50 seconds so give +10 seconds this._pollRpc = this._rpc({route: this.POLL_ROUTE, params: data}, {shadow : true, timeout: 60000}); diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 04be083d8da..b6f65d79eec 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -7,6 +7,8 @@ import threading from odoo.tools.misc import split_every from odoo import _, api, fields, models, registry, SUPERUSER_ID +from odoo.addons.bus.models.bus_presence import AWAY_TIMER +from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER from odoo.osv import expression _logger = logging.getLogger(__name__) @@ -252,3 +254,38 @@ class Partner(models.Model): partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])] return [users, partners] + + @api.model + def im_search(self, name, limit=20): + """ Search partner with a name and return its id, name and im_status. + Note : the user must be logged + :param name : the partner name to search + :param limit : the limit of result to return + """ + # This method is supposed to be used only in the context of channel creation or + # extension via an invite. As both of these actions require the 'create' access + # right, we check this specific ACL. + if self.env['mail.channel'].check_access_rights('create', raise_exception=False): + name = '%' + name + '%' + excluded_partner_ids = [self.env.user.partner_id.id] + self.env.cr.execute(""" + SELECT + U.id as user_id, + P.id as id, + P.name as name, + CASE WHEN B.last_poll IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' + ELSE 'online' + END as im_status + FROM res_users U + JOIN res_partner P ON P.id = U.partner_id + LEFT JOIN bus_presence B ON B.user_id = U.id + WHERE P.name ILIKE %s + AND P.id NOT IN %s + AND U.active = 't' + LIMIT %s + """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) + return self.env.cr.dictfetchall() + else: + return {} diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 8a045ac2d82..58e334808bf 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -371,7 +371,15 @@ var BasicComposer = Widget.extend({ { limit: limit, search: search } ); } - return suggestions; + return $.when(suggestions).then(function (suggestions) { + //add im_status on suggestions + _.each(suggestions, function (suggestionsSet) { + _.each(suggestionsSet, function (suggestion) { + suggestion.im_status = self.call('mail_service', 'getImStatus', { partnerID: suggestion.id }); + }); + }); + return suggestions; + }); }); }, /** diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 808cd8338ad..073befc5fcd 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -56,7 +56,10 @@ var PartnerInviteDialog = Dialog.extend({ allowClear: true, multiple: true, formatResult: function (item) { - var status = QWeb.render('mail.UserStatus', { status: item.im_status }); + var status = QWeb.render('mail.UserStatus', { + status: self.call('mail_service', 'getImStatus', { partnerID: item.id }), + partnerID: item.id, + }); return $('').text(item.text).prepend(status); }, query: function (query) { @@ -992,10 +995,10 @@ var Discuss = AbstractAction.extend({ .on('new_channel', this, this._onNewChannel) .on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible) .on('unsubscribe_from_channel', this, this._onChannelLeft) + .on('updated_im_status', this, this._onUpdatedImStatus) .on('update_needaction', this, this._onUpdateNeedaction) .on('update_starred', this, this._onUpdateStarred) .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter) - .on('update_dm_presence', this, this._onUpdateDmPresence) .on('activity_updated', this, this._onActivityUpdated) .on('update_moderation_counter', this, this._onUpdateModerationCounter) .on('update_typing_partners', this, this._onTypingPartnersUpdated) @@ -1572,7 +1575,7 @@ var Discuss = AbstractAction.extend({ /** * @private */ - _onUpdateDmPresence: function () { + _onUpdatedImStatus: function () { this._throttledUpdateThreads(); }, /** diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js index c1ae22b9708..bb2a19cc6ac 100644 --- a/addons/mail/static/src/js/models/messages/abstract_message.js +++ b/addons/mail/static/src/js/models/messages/abstract_message.js @@ -75,6 +75,14 @@ var AbstractMessage = Class.extend({ } return this._serverAuthorID[0]; }, + /** + * Threads do not have an im status by default + * + * @return {undefined} + */ + getAuthorImStatus: function () { + return undefined; + }, /** * Get the relative url of the avatar to display next to the message * diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index dc98bb7195b..a414a11874b 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -65,6 +65,16 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin addCustomerEmailData: function (data) { this._customerEmailData.push(data); }, + /** + * @override + * @return {string|undefined} + */ + getAuthorImStatus: function () { + if (!this.hasAuthor()) { + return undefined; + } + return this.call('mail_service', 'getImStatus', { partnerID: this.getAuthorID() }); + }, /** * Get the name of the author of this message * If there are no author, return "". diff --git a/addons/mail/static/src/js/models/threads/dm_chat.js b/addons/mail/static/src/js/models/threads/dm_chat.js index 5e69dbeff8c..4967a1e9963 100644 --- a/addons/mail/static/src/js/models/threads/dm_chat.js +++ b/addons/mail/static/src/js/models/threads/dm_chat.js @@ -26,8 +26,14 @@ var DMChat = TwoUserChannel.extend({ this._directPartnerID = data.direct_partner[0].id; this._name = data.custom_channel_name || data.direct_partner[0].name; - this._status = data.direct_partner[0].im_status; + this._outOfOfficeMessage = data.direct_partner[0].out_of_office_message; + this._outOfOfficeDateEnd = data.direct_partner[0].out_of_office_date_end; this._type = 'dm_chat'; + + this.call('mail_service', 'updateImStatus', [{ + id: this._directPartnerID, + im_status: data.direct_partner[0].im_status + }]); }, //-------------------------------------------------------------------------- @@ -53,10 +59,11 @@ var DMChat = TwoUserChannel.extend({ return result; }, /** - * @param {string} newStatus + * @override + * @return {string} */ - setStatus: function (newStatus) { - this._status = newStatus; + getStatus: function () { + return this.call('mail_service', 'getImStatus', { partnerID: this._directPartnerID }); }, }); diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 777df1df4a6..204a88bb8e9 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -443,10 +443,6 @@ var MailManager = AbstractService.extend({ channel = this._makeChannel(data, options); if (channel.getType() === 'dm_chat') { this._pinnedDmPartners.push(channel.getDirectPartnerID()); - this.call('bus_service', 'updateOption', - 'bus_presence_partner_ids', - this._pinnedDmPartners - ); } this._threads.push(channel); if (data.last_message) { @@ -1041,10 +1037,6 @@ var MailManager = AbstractService.extend({ var index = this._pinnedDmPartners.indexOf(channel.getDirectPartnerID()); if (index > -1) { this._pinnedDmPartners.splice(index, 1); - this.call('bus_service', 'updateOption', - 'bus_presence_partner_ids', - this._pinnedDmPartners - ); } } this._threads = _.without(this._threads, channel); diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index b59d7b91434..b77fa3e65fd 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -493,22 +493,6 @@ MailManager.include({ self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID); }); }, - /** - * On receiving an update on user status (e.g. becoming 'online', 'offline', - * 'idle', etc.). - * - * @private - * @param {Object} data partner infos - * @param {integer} data.id partner ID - * @param {string} data.im_status partner new 'im status' (e.g. 'online') - */ - _handlePresenceNotification: function (data) { - var dmChat = this.getDMChatFromPartnerID(data.id); - if (dmChat) { - dmChat.setStatus(data.im_status); - this._mailBus.trigger('update_dm_presence', dmChat); - } - }, /** * @override * @private @@ -569,8 +553,6 @@ MailManager.include({ }); } else if (model === 'res.partner') { self._handlePartnerNotification(notif[1]); - } else if (model === 'bus.presence') { - self._handlePresenceNotification(notif[1]); } }); }, diff --git a/addons/mail/static/src/js/services/mail_service.js b/addons/mail/static/src/js/services/mail_service.js index 4f6b0c29dfc..2256e696c2f 100644 --- a/addons/mail/static/src/js/services/mail_service.js +++ b/addons/mail/static/src/js/services/mail_service.js @@ -5,6 +5,7 @@ var core = require('web.core'); var MailManager = require('mail.Manager'); +require('mail.Manager.Status'); require('mail.Manager.Notification'); require('mail.Manager.Window'); require('mail.Manager.DocumentThread'); diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js new file mode 100644 index 00000000000..2b75cdc6ee7 --- /dev/null +++ b/addons/mail/static/src/js/services/mail_status_manager.js @@ -0,0 +1,216 @@ +odoo.define('mail.Manager.Status', function (require) { +"use strict"; + +var core = require('web.core'); +var MailManager = require('mail.Manager'); +var QWeb = core.qweb; + +/** + * Mail Manager: IM Status + * + * This component handles im status of partners, which is useful for DM Chats, + * partner mention suggestions, and chatter messages that display the user icon. + */ +MailManager.include({ + _UPDATE_INTERVAL: 50, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the uin cache im status, and triggers an asynchronous throttled + * update of im_status cache for missings + * + * @param {Object} data + * @param {integer} data.partnerID + * @return {String} + */ + getImStatus: function (data) { + var self = this; + var partnerID = data.partnerID; + if (!this._imStatus[partnerID]) { + // Add to list to call it in next bus update or _fetchMissingImStatus + this._imStatus[partnerID] = undefined; + // fetch after some time if no other getImStatus occurs + this._clearStatusServiceTimeout(this._fetchStatusTimeout); + this._fetchStatusTimeout = this._setStatusServiceTimeout(function () { + self._fetchMissingImStatus(); + }, 500); + } + return this._imStatus[partnerID]; + }, + /** + * Update status manually, to avoid to do a rpc and an asynchronous update + * after getImStatus. Can be done by any caller knowing the last im_status + * state. + * + * @param {Object[]} statusList, A list of {id, im_status} + */ + updateImStatus: function (statusList) { + var updatedIDs = []; + var self = this; + _.each(statusList, function (status) { + if (self._imStatus[status.id] === status.im_status) { + return; + } + updatedIDs.push(status.id); + self._imStatus[status.id] = status.im_status; + }); + if (! _.isEmpty(updatedIDs)) { + this._mailBus.trigger('updated_im_status', updatedIDs); // useful for thread window header + this._renderImStatus(updatedIDs); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * A simple clearTimeout, useful for test + * + * @private + * @param {integer} ids + */ + _clearStatusServiceTimeout: function (id) { + clearTimeout(id); + }, + /** + * Fetch the list of im_status for partner with id in ids list and triggers + * an update. + * + * @private + * @param {Object} data + * @param {integer[]} data.partnerIDs + * @return {Deferred} + */ + _fetchImStatus: function (data) { + var self = this; + var partnerIDs = data.partnerIDs; + if (_.isEmpty(partnerIDs)) { + return $.when(); + } + return this._rpc({ + model: 'res.partner', + method: 'read', + args: [partnerIDs, ['id', 'im_status']], + }).then( function (results) { + self.updateImStatus(results); + }); + }, + /** + * Fetch the list of im_status for partner with an unknown im_status and + * triggers an update. + * + * @private + * @return {Deferred} + */ + _fetchMissingImStatus: function () { + var missing = []; + _.each(this._imStatus, function (value, key) { + if (value === undefined) { + missing.push(Number(key)); + } + }); + return this._fetchImStatus({ partnerIDs: missing }); + }, + /** + * @private + * @return {integer[]} a list of partner ids that needs update + */ + _getImStatusToUpdate: function () { + var toUpdate = []; + _.each(this._imStatus, function (status, key) { + //filter on im_partner and bot: useless to update them, status won't change + if (['im_partner', 'bot'].indexOf(status) === -1) { + toUpdate.push(Number(key)); + } + }); + return toUpdate; + }, + /** + * @override + * @private + */ + _initializeInternalState: function () { + this._super.apply(this, arguments); + this._fetchStatusTimeout = undefined; + this._imStatus = {}; + this._isTabFocused = true; + this._updateImStatusLoop(); + }, + /** + * @override + * @private + */ + _listenOnBuses: function () { + this._super.apply(this, arguments); + $(window).on("focus", this._onWindowFocusChange.bind(this, true)); + $(window).on("blur", this._onWindowFocusChange.bind(this, false)); + $(window).on("unload", this._onWindowFocusChange.bind(this, false)); + }, + /** + * @private + * @param {integer[]} updatedIds + */ + _renderImStatus: function (updatedIds) { + var self = this; + $('.o_updatable_im_status').each(function () { + var $this = $(this); + var partnerID = $this.data('partner-id'); + if (partnerID !== undefined && updatedIds.indexOf(partnerID) !== -1) { // todo instead add id on o_updatable_im_status and select only concerned ones + var status = QWeb.render('mail.UserStatus', { + status: self.getImStatus({ partnerID: partnerID }), + partnerID: partnerID, + }); + $this.replaceWith(status); + } + }); + }, + /** + * A simple setTimeout, useful for test + * + * @private + * @param {function} func + * @param {integer} duration + * @return {integer} + */ + _setStatusServiceTimeout: function (func, duration) { + return setTimeout(func, duration); + }, + /** + * Once initialised, this loop will update the im_status of registered + * users. + * + * @private + * @param {integer} [counter=0] The recursion loop counter + */ + _updateImStatusLoop: function (counter) { + var self = this; + if (!_.isNumber(counter)) { + counter = 0; + } + this._setStatusServiceTimeout(function () { + if (counter >= self._UPDATE_INTERVAL && self._isTabFocused) { + self._fetchImStatus({ partnerIDs: self._getImStatusToUpdate() }); + counter = 0; + } + self._updateImStatusLoop(counter+1); + }, 1000); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {boolean} focused + */ + _onWindowFocusChange: function (focused) { + this._isTabFocused = focused; + }, +}); + +}); diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 4971e7f3f4b..e047c71ab89 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -334,8 +334,8 @@ MailManager.include({ .on('new_channel', this, this._onNewChannel) .on('is_thread_bottom_visible', this, this._onIsThreadBottomVisible) .on('unsubscribe_from_channel', this, this._onUnsubscribeFromChannel) - .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter) - .on('update_dm_presence', this, this._onUpdateDmPresence); + .on('updated_im_status', this, this._onUpdatedImStatus) + .on('update_thread_unread_counter', this, this._onUpdateThreadUnreadCounter); core.bus.on('resize', this, _.debounce(this._repositionThreadWindows.bind(this), 100)); }, @@ -663,6 +663,28 @@ MailManager.include({ _onUnsubscribeFromChannel: function (channelID) { this._closeThreadWindow(channelID); }, + /** + * Called when there is a change of the im status of the partner. + * The header of the thread window should be updated accordingly, + * in order to display the correct new im status of this users. + * + * @private + * @param {integer} partnerID + */ + _onUpdatedImStatus: function (partnerIDs) { + var self = this; + _.each(partnerIDs, function (partnerID) { + var thread = self.getDMChatFromPartnerID(partnerID); + if (! thread) { + return; + } + var threadWindow = self._getThreadWindow(thread.getID()); + if (!threadWindow) { + return; + } + threadWindow.renderHeader(); + }); + }, /** * Called when a thread has its unread counter that has changed. * The unread counter on the thread windows should be updated. @@ -690,21 +712,6 @@ MailManager.include({ this._repositionHiddenWindowsDropdown(); } }, - /** - * Called when there is a change of the im status of the user linked to - * DMs. The header of the thread window should be updated accordingly, - * in order to display the correct new im status of this users. - * - * @private - * @param {mail.model.Thread} thread - */ - _onUpdateDmPresence: function (thread) { - _.each(this._threadWindows, function (threadWindow) { - if (thread.getID() === threadWindow.getID()) { - threadWindow.renderHeader(); - } - }); - }, /** * Called when a message has been updated. * diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss index 096c527aa16..1f2fffcee60 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -55,6 +55,9 @@ font-style: italic; color: gray; } + .fa { + font-size: 0.8em; + } } textarea { resize: none; diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index ff4680d7851..b9de0194e93 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -71,6 +71,10 @@ + + + + () diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index 98430c81ce1..10fb71a5110 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -154,6 +154,7 @@ + # @@ -176,11 +177,14 @@ - - - + + + + + + @@ -129,8 +130,10 @@ - + + + diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py index c715adccc49..2a6ec5eca55 100644 --- a/addons/mail_bot/models/res_partner.py +++ b/addons/mail_bot/models/res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models + class Partner(models.Model): _inherit = 'res.partner' diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml index e7f0650c867..95526e6b11a 100644 --- a/addons/mail_bot/views/discuss.xml +++ b/addons/mail_bot/views/discuss.xml @@ -1,10 +1,5 @@ - - - - - From b60224c41cb6c9817fd328bb60b21681e619fb26 Mon Sep 17 00:00:00 2001 From: XavierDo Date: Mon, 7 Jan 2019 11:24:51 +0000 Subject: [PATCH 2/8] [IMP] mail, base: add out of office status to user. This status can be edited by user in preferences, indicates if user is available or not. --- addons/mail/models/res_users.py | 5 +++-- addons/mail/views/res_users_views.xml | 6 +++++- odoo/addons/base/views/res_users_views.xml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 9e04c6372e9..6d03343a33d 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -38,6 +38,7 @@ class Users(models.Model): moderation_channel_ids = fields.Many2many( 'mail.channel', 'mail_channel_moderator_rel', string='Moderated channels') + out_of_office_message = fields.Char(string='Out of Office Message') @api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids') @api.multi @@ -74,10 +75,10 @@ GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)]) init_res = super(Users, self).__init__(pool, cr) # duplicate list to avoid modifying the original reference type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) - type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type']) + type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type', 'out_of_office_message']) # duplicate list to avoid modifying the original reference type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS) - type(self).SELF_READABLE_FIELDS.extend(['notification_type']) + type(self).SELF_READABLE_FIELDS.extend(['notification_type', 'out_of_office_message']) return init_res @api.model diff --git a/addons/mail/views/res_users_views.xml b/addons/mail/views/res_users_views.xml index d133fb0424b..8f4a376f0d9 100644 --- a/addons/mail/views/res_users_views.xml +++ b/addons/mail/views/res_users_views.xml @@ -12,6 +12,11 @@ + + + + + @@ -33,6 +38,5 @@ - diff --git a/odoo/addons/base/views/res_users_views.xml b/odoo/addons/base/views/res_users_views.xml index f75ba79e3ef..ae30ce89dff 100644 --- a/odoo/addons/base/views/res_users_views.xml +++ b/odoo/addons/base/views/res_users_views.xml @@ -354,7 +354,7 @@ - +