diff --git a/addons/bus/__openerp__.py b/addons/bus/__openerp__.py index 2489f6a38f972993800bc7ce4c07fe21ae44b44d..2a7185fdaf7b8e570e35406ba07a2628a8413d0e 100644 --- a/addons/bus/__openerp__.py +++ b/addons/bus/__openerp__.py @@ -6,6 +6,7 @@ 'description': "Instant Messaging Bus allow you to send messages to users, in live.", 'depends': ['base', 'web'], 'data': [ + 'bus_presence_cron.xml', 'views/bus.xml', 'security/ir.model.access.csv', ], diff --git a/addons/bus/bus_presence_cron.xml b/addons/bus/bus_presence_cron.xml new file mode 100644 index 0000000000000000000000000000000000000000..309c641e3f79f9d27612f597ef6b544a6b19e1bc --- /dev/null +++ b/addons/bus/bus_presence_cron.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding='UTF-8'?> +<odoo> + <record model="ir.cron" id="account_asset_cron"> + <field name="name">Check User Disconnections</field> + <field name="interval_number">5</field> + <field name="interval_type">minutes</field> + <field name="numbercall">-1</field> + <field name="doall" eval="False"/> + <field name="model" eval="'bus.presence'"/> + <field name="function" eval="'check_users_disconnection'"/> + <field name="args" eval="'()'" /> + </record> +</odoo> diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index 050eb684625992f2d3f23881a217e5bab0ca3162..d755cabe54fe766c9d0b37b187b443bec5f0b6ff 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -19,6 +19,7 @@ class BusController(openerp.http.Controller): # override to add channels def _poll(self, dbname, channels, last, options): + channels.append((request.db, 'bus.presence')) # update the user presence if request.session.uid and 'im_presence' in options: request.env['bus.presence'].update(options.get('im_presence')) diff --git a/addons/bus/models/bus_presence.py b/addons/bus/models/bus_presence.py index 445e9491a7550915fa452011775c895d9667d296..70aa9ef97269858323d70946b6a2aaecaf1c5f00 100644 --- a/addons/bus/models/bus_presence.py +++ b/addons/bus/models/bus_presence.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import datetime -import random import time from openerp import api, fields, models @@ -11,6 +10,8 @@ from openerp.addons.bus.models.bus import TIMEOUT DISCONNECTION_TIMER = TIMEOUT + 5 AWAY_TIMER = 600 # 10 minutes +DISCONNECTIONS_CHECK_PERIOD = datetime.timedelta(minutes=1) # check for user disconnections every minute +last_disconnections_check = datetime.datetime.utcnow() class BusPresence(models.Model): @@ -34,7 +35,7 @@ class BusPresence(models.Model): @api.model def update(self, user_presence=True): """ Register the given presence of the current user, and trigger a im_status change if necessary. - The status will not be written or sent if not necessary. + The status will not be sent if not necessary. :param user_presence : True, if the user (self._uid) is still detected using its browser. :type user_presence : boolean """ @@ -51,37 +52,31 @@ class BusPresence(models.Model): values['user_id'] = self._uid self.create(values) else: # write the user presence if necessary - if user_presence: - values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - values['status'] = 'online' - else: - threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) - if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold: - values['status'] = 'away' + values['status'] = 'online' if user_presence else 'away' send_notification = presence.status != values['status'] - # write only if the last_poll is passed TIMEOUT, or if the status has changed - delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT) - if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification: - # Hide transaction serialization errors, which can be ignored, the presence update is not essential - with tools.mute_logger('openerp.sql_db'): - presence.write(values) + # Hide transaction serialization errors, which can be ignored, the presence update is not essential + with tools.mute_logger('openerp.sql_db'): + presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary # notify if the status has changed if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence - self.env['bus.bus'].sendone((self._cr.dbname, 'bus.presence'), {'id': self._uid, 'im_status': values['status']}) - # gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it. - if random.random() < 0.01: - self.check_users_disconnection() + self.env['bus.bus'].sendone((self._cr.dbname, 'bus.presence'), {'id': self.env.user.partner_id.id, 'im_status': values['status']}) + # check for disconnected users + self.check_users_disconnection() return True @api.model def check_users_disconnection(self): """ Disconnect the users having a too old last_poll """ - limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) - presences.write({'status': 'offline'}) + global last_disconnections_check + now = datetime.datetime.utcnow() notifications = [] - for presence in presences: - notifications.append([(self._cr.dbname, 'bus.presence'), {'id': presence.user_id.id, 'im_status': presence.status}]) + if (now - DISCONNECTIONS_CHECK_PERIOD) > last_disconnections_check: + last_disconnections_check = now + limit_date = (now - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')]) + presences.write({'status': 'offline'}) + for presence in presences: + notifications.append([(self._cr.dbname, 'bus.presence'), {'id': presence.user_id.partner_id.id, 'im_status': presence.status}]) self.env['bus.bus'].sendmany(notifications) diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js index 9373b7c7b1c034758912d0cb7fb1a2a5c9e7b2e2..29540b36cdff3d74331adc7b0374b034c1c1bf3e 100644 --- a/addons/bus/static/src/js/bus.js +++ b/addons/bus/static/src/js/bus.js @@ -7,11 +7,15 @@ var Widget = require('web.Widget'); var bus = {}; bus.ERROR_DELAY = 10000; +bus.AWAY_TIMEOUT = 300000; // 5 minutes bus.Bus = Widget.extend({ init: function(){ + var self = this; this._super(); - this.options = {}; + this.options = { + im_presence: true, + }; this.activated = false; this.channels = []; this.last = 0; @@ -21,9 +25,14 @@ bus.Bus = Widget.extend({ // bus presence this.set("window_focus", true); this.on("change:window_focus", this, function () { - this.options.im_presence = this.get("window_focus"); + clearTimeout(self.away_timeout); if (this.get("window_focus")) { + this.options.im_presence = true; this.trigger('window_focus', this.is_master); + } else { + this.away_timeout = setTimeout(function () { + self.options.im_presence = false; + }, bus.AWAY_TIMEOUT); } }); $(window).on("focus", _.bind(this.window_focus, this)); @@ -120,13 +129,6 @@ var CrossTabBus = bus.Bus.extend({ } on("storage", this.on_storage.bind(this)); - if (this.is_master) { - setItem('bus.channels', this.channels); - setItem('bus.options', this.options); - } else { - this.channels = getItem('bus.channels', this.channels); - this.options = getItem('bus.options', this.options); - } }, start_polling: function(){ var self = this; @@ -139,6 +141,13 @@ var CrossTabBus = bus.Bus.extend({ self.is_master = false; self.stop_polling(); }); + if (this.is_master) { + setItem('bus.channels', this.channels); + setItem('bus.options', this.options); + } else { + this.channels = getItem('bus.channels', this.channels); + this.options = getItem('bus.options', this.options); + } return; // start_polling will be called again on tab registration } diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 909a4a0ff5b04d19d74292e31acec70b1d159bba..cd758790d64d8b5f76cf4700db6b83b56bfe26be 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -272,6 +272,7 @@ function make_channel (data, options) { if ('direct_partner' in data) { channel.type = "dm"; channel.name = data.direct_partner[0].name; + channel.direct_partner_id = data.direct_partner[0].id; channel.status = data.direct_partner[0].im_status; } return channel; @@ -399,6 +400,9 @@ function on_notification (notification) { } else if (model === 'res.partner') { // channel joined/left, message marked as read/(un)starred, chat open/closed on_partner_notification(notification[1]); + } else if (model === 'bus.presence') { + // update presence of users + on_presence_notification(notification[1]); } } @@ -530,6 +534,14 @@ function on_chat_session_notification (chat_session) { } } +function on_presence_notification (data) { + var dm = _.findWhere(channels, {direct_partner_id: data.id}); + if (dm) { + dm.status = data.im_status; + chat_manager.bus.trigger('update_dm_presence', dm); + } +} + // Public interface //---------------------------------------------------------------------------------- var chat_manager = { diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 1a12ff36912f6b8d1a9a01ed089fb385f727ce60..3d97a35af0e80c91df0aee55d592619570d87cb3 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -52,8 +52,8 @@ var PartnerInviteDialog = Dialog.extend({ width: '100%', allowClear: true, multiple: true, - formatResult: function(item){ - var css_class = "fa-circle" + (item.im_status === 'online' ? "" : "-o"); + formatResult: function(item) { + var css_class = (item.im_status === 'away' ? "fa-clock-o" : "fa-circle" + (item.im_status === 'online' ? "" : "-o")); return $('<span class="fa">').addClass(css_class).text(item.text); }, query: function (query) { @@ -135,6 +135,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.action = action; this.options = options || {}; this.channels_scrolltop = {}; + this.throttled_render_sidebar = _.throttle(this.render_sidebar.bind(this), 100, { leading: false }); }, willStart: function () { @@ -213,9 +214,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, { chat_manager.bus.on('anyone_listening', self, function (channel, query) { query.is_displayed = query.is_displayed || channel.id === self.channel.id; }); - chat_manager.bus.on('update_needaction', self, self.render_sidebar); chat_manager.bus.on('unsubscribe_from_channel', self, self.render_sidebar); - chat_manager.bus.on('update_channel_unread_counter', self, self.render_sidebar); + chat_manager.bus.on('update_needaction', self, self.throttled_render_sidebar); + chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_render_sidebar); + chat_manager.bus.on('update_dm_presence', self, self.throttled_render_sidebar); }); }, diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml index 11b32ee0e8dc5cb0895e3809b2709a502f0158f0..bb29d8cca2706e5c2b44185fc888ae6864e15ee9 100644 --- a/addons/mail/static/src/xml/client_action.xml +++ b/addons/mail/static/src/xml/client_action.xml @@ -77,7 +77,7 @@ <t t-set="counter" t-value="channel.needaction_counter"/> <div t-if="channel.type === channel_type" t-att-data-channel-id="channel.id" t-attf-class="o_mail_chat_channel_item #{channel.unread_counter ? ' o_unread_message' : ''} #{(active_channel_id == channel.id) ? 'o_active': ''}"> - <span><i t-if="display_status" t-att-class="'o_user_status fa ' + (channel.status == 'online' ? 'fa-circle' : 'fa-circle-o')"/></span> + <span><i t-if="display_status" t-att-class="'o_user_status fa ' + (channel.status == 'online' ? 'fa-circle' : (channel.status == 'away' ? 'fa-clock-o' : 'fa-circle-o'))" t-attf-title="#{channel.status}"/></span> <span t-if="display_hash" class="o_mail_hash">#</span> <t t-esc="channel.name"/> <i t-if="channel.mass_mailing" class="fa fa-envelope-o"/>