[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)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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 $('<span>').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();
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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 "".
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
.fa {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
|
||||
@@ -71,6 +71,10 @@
|
||||
</t>
|
||||
<t t-name="mail.MentionSuggestions.Partner" t-extend="mail.MentionSuggestions">
|
||||
<t t-jquery=".o_mention_proposition" t-operation="append">
|
||||
<t t-call="mail.UserStatus">
|
||||
<t t-set="status" t-value="suggestion.im_status"/>
|
||||
<t t-set="partnerID" t-value="suggestion.id"/>
|
||||
</t>
|
||||
<span class="o_mention_name"><t t-esc="suggestion.name"/></span>
|
||||
<t t-if="suggestion.email">
|
||||
<span class="o_mention_info">(<t t-esc="suggestion.email"/>)</span>
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
<span t-if="displayStatus">
|
||||
<t t-call="mail.UserStatus">
|
||||
<t t-set="status" t-value="channel.getStatus()"/>
|
||||
<t t-set="partnerID" t-value="channel.getDirectPartnerID()"/>
|
||||
</t>
|
||||
</span>
|
||||
<span t-if="displayHash" class="o_mail_hash">#</span>
|
||||
@@ -176,11 +177,14 @@
|
||||
|
||||
<!--
|
||||
@param {string} status
|
||||
@param {integer|undefined} [partnerID]
|
||||
-->
|
||||
<t t-name="mail.UserStatus">
|
||||
<i t-if="status == 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
|
||||
<i t-if="status == 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
|
||||
<i t-if="status == 'offline'" class="o_mail_user_status fa fa-circle-o" title="Offline" role="img" aria-label="User is offline"/>
|
||||
<span t-att-class="partnerID ? 'o_updatable_im_status' : ''" t-att-data-partner-id="partnerID">
|
||||
<i t-if="status == 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
|
||||
<i t-if="status == 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
|
||||
<i t-if="status == 'offline'" class="o_mail_user_status fa fa-circle" title="Offline" role="img" aria-label="User is offline"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@@ -300,8 +304,9 @@
|
||||
t-att-data-preview-id="preview.id" t-att-data-document-id="preview.documentID" t-att-data-document-model="preview.documentModel" t-att-data-unread-counter="preview.unreadCounter" t-att-data-message-ids="preview.messageIDs">
|
||||
<div t-attf-class="o_mail_preview_image #{preview.documentModel ? 'o_mail_preview_app' : ''}">
|
||||
<img class="o_mail_preview_image" t-att-src="preview.imageSRC" alt="Preview"/>
|
||||
<i t-if="preview.status === 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
|
||||
<i t-if="preview.status === 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle" role="img" aria-label="User is idle"/>
|
||||
<t t-call="mail.UserStatus">
|
||||
<t t-set="status" t-value="preview.status"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_preview_info">
|
||||
<div class="o_preview_title">
|
||||
|
||||
@@ -258,12 +258,18 @@
|
||||
<div t-att-class="'o_thread_message ' + (message.getID() === options.selectedMessageID ? 'o_thread_selected_message ' : ' ') + (message.isDiscussion() or message.isNotification() ? ' o_mail_discussion ' : ' o_mail_not_discussion ')" t-att-data-message-id="message.getID()">
|
||||
<div t-if="options.displayAvatars" class="o_thread_message_sidebar">
|
||||
<t t-if="message.hasAuthor()">
|
||||
<img t-if="displayAuthorMessages[message.getID()]"
|
||||
alt=""
|
||||
t-att-src="message.getAvatarSource()"
|
||||
data-oe-model="res.partner"
|
||||
t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
|
||||
t-attf-class="o_thread_message_avatar rounded-circle #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}"/>
|
||||
<div t-if="displayAuthorMessages[message.getID()]" class="o_thread_message_sidebar_image">
|
||||
<img
|
||||
alt=""
|
||||
t-att-src="message.getAvatarSource()"
|
||||
data-oe-model="res.partner"
|
||||
t-att-data-oe-id="message.shouldRedirectToAuthor() ? message.getAuthorID() : ''"
|
||||
t-attf-class="o_thread_message_avatar rounded-circle #{message.shouldRedirectToAuthor() ? 'o_mail_redirect' : ''}"/>
|
||||
<t t-call="mail.UserStatus">
|
||||
<t t-set="status" t-value="message.getAuthorImStatus()"/>
|
||||
<t t-set="partnerID" t-value="message.getAuthorID()"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<img t-if="displayAuthorMessages[message.getID()]"
|
||||
|
||||
@@ -27,6 +27,18 @@ QUnit.module('Chatter', {
|
||||
|
||||
this.services = mailTestUtils.getMailServices();
|
||||
this.data = {
|
||||
'res.partner': {
|
||||
fields: {
|
||||
im_status: {
|
||||
string: "im_status",
|
||||
type: "char",
|
||||
}
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
im_status: 'online',
|
||||
}]
|
||||
},
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
@@ -772,7 +784,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
|
||||
}
|
||||
if (args.method === 'get_mention_suggestions') {
|
||||
getSuggestionsDef.resolve();
|
||||
return $.when([{email: "test@odoo.com", id: 1, name: "Test User"}]);
|
||||
return $.when([[{email: "test@odoo.com", id: 1, name: "Test User"}], []]);
|
||||
}
|
||||
if (args.method === 'message_post') {
|
||||
var lastMessageData = _.max(this.data['mail.message'].records, function (messageData) {
|
||||
@@ -1308,7 +1320,7 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien
|
||||
}
|
||||
if (args.method === 'get_mention_suggestions') {
|
||||
getSuggestionsDef.resolve();
|
||||
return $.when([{email: "me@odoo.com", id: 42, name: "Me"}]);
|
||||
return $.when([[{email: "me@odoo.com", id: 42, name: "Me"}], []]);
|
||||
}
|
||||
if (args.method === 'message_format') {
|
||||
var requested_msgs = _.filter(messages, function (msg) {
|
||||
|
||||
@@ -61,6 +61,18 @@ QUnit.module('Discuss', {
|
||||
},
|
||||
},
|
||||
},
|
||||
'res.partner': {
|
||||
fields: {
|
||||
im_status: {
|
||||
string: "status",
|
||||
type: 'char',
|
||||
},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
im_status: 'online',
|
||||
}]
|
||||
},
|
||||
};
|
||||
this.services = mailTestUtils.getMailServices();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
odoo.define('mail.mailStatusServiceTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var mailTestUtils = require('mail.testUtils');
|
||||
|
||||
var core = require('web.core');
|
||||
var testUtils = require('web.test_utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('service', {}, function () {
|
||||
QUnit.module('Status manager', {
|
||||
beforeEach: function () {
|
||||
var self = this;
|
||||
this.services = mailTestUtils.getMailServices();
|
||||
this.timeouts = {};
|
||||
this.countTimeout = 0;
|
||||
this.defaultPatchData = {
|
||||
_clearStatusServiceTimeout: function (id) {
|
||||
self.timeouts[id] = false;
|
||||
},
|
||||
_setStatusServiceTimeout: function (func, duration) {
|
||||
var id = self.countTimeout;
|
||||
self.timeouts[id] = func;
|
||||
self.countTimeout++;
|
||||
return id;
|
||||
},
|
||||
_updateImStatusLoop: function () {}, // avoid spam and infinite loop
|
||||
};
|
||||
this.patchMailService = function (patch) {
|
||||
testUtils.mock.patch(this.services.mail_service, _.extend({}, this.defaultPatchData, patch));
|
||||
};
|
||||
this.resolveTimeouts = function () {
|
||||
var timeouts = _.extend({}, self.timeouts);
|
||||
self.timeouts = {}; // empty timeout before looping to avoid to remove loop timeout
|
||||
_.each(_.values(timeouts), function (func) {
|
||||
if (func !== false) {
|
||||
func();
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
afterEach: function () {
|
||||
testUtils.mock.unpatch(this.services.mail_service);
|
||||
},
|
||||
});
|
||||
QUnit.test('simple set im_status', function (assert) {
|
||||
assert.expect(1);
|
||||
this.patchMailService();
|
||||
var parent = testUtils.createParent({
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
|
||||
},
|
||||
});
|
||||
parent.call('mail_service', 'updateImStatus', [{
|
||||
id: 1,
|
||||
im_status: 'online',
|
||||
}]);
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
|
||||
|
||||
this.resolveTimeouts(); // alway resolve timeout
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('multi get_im_status', function (assert) {
|
||||
assert.expect(9);
|
||||
this.patchMailService();
|
||||
var readCount = 0;
|
||||
var parent = testUtils.createParent({
|
||||
//data: this.data,
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
if (args.method === 'read' && args.model === 'res.partner') {
|
||||
assert.deepEqual(args.args[0], [2,3]);
|
||||
assert.deepEqual(args.args[1], ['id', 'im_status']);
|
||||
readCount++;
|
||||
return $.when([
|
||||
{id: 2, im_status: 'away'},
|
||||
{id: 3, im_status: 'im_partner'}
|
||||
]);
|
||||
}
|
||||
throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
|
||||
},
|
||||
});
|
||||
parent.call('mail_service', 'updateImStatus', [{
|
||||
id: 1,
|
||||
im_status: 'online',
|
||||
}]);
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), undefined);
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), undefined);
|
||||
|
||||
this.resolveTimeouts();
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away');
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), 'im_partner');
|
||||
|
||||
this.resolveTimeouts();
|
||||
assert.strictEqual(readCount, 1, 'Only one read on partner should have been performed');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('update loop', function (assert) {
|
||||
assert.expect(11);
|
||||
delete this.defaultPatchData['_updateImStatusLoop']; // we want to test default behaviour of loop
|
||||
this.patchMailService();
|
||||
var readCount = 0;
|
||||
var parent = testUtils.createParent({
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
if (args.method === 'read' && args.model === 'res.partner') {
|
||||
assert.deepEqual(args.args[0], [1, 2]);
|
||||
assert.deepEqual(args.args[1], ['id', 'im_status']);
|
||||
readCount++;
|
||||
return $.when([
|
||||
{"id": 1, "im_status": "online"},
|
||||
{"id": 2, "im_status": "away"}
|
||||
]);
|
||||
}
|
||||
throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
|
||||
},
|
||||
});
|
||||
// set initial status
|
||||
parent.call('mail_service', 'updateImStatus', [
|
||||
{ id: 1, im_status: 'offline' },
|
||||
{ id: 2, im_status: 'offline' },
|
||||
{ id: 3, im_status: 'im_partner' }, //shouldn't be updated !!!!
|
||||
]);
|
||||
//_updateImStatusLoop should be running at one second per iteration, lets make a minute pass.
|
||||
// this test is strongly dependant on the fact that the update loop as 1 second tick and update every 50 seconds.
|
||||
assert.strictEqual(readCount, 0);
|
||||
for (var i = 0; i < 50; i++){
|
||||
this.resolveTimeouts();
|
||||
}
|
||||
assert.strictEqual(readCount, 0);
|
||||
this.resolveTimeouts(); //one more second pass
|
||||
assert.strictEqual(readCount, 1, 'one call should have been made after 50 seconds' );
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online');
|
||||
assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away');
|
||||
|
||||
//simulate change of focus
|
||||
//original listener: $(window).on("blur", this._onWindowFocusChange.bind(this, false); + unload, ...
|
||||
parent.call('mail_service', '_onWindowFocusChange', false); // remove focus from tab
|
||||
for (var i = 0; i < 200; i++){ // a lot of time has passed
|
||||
this.resolveTimeouts();
|
||||
}
|
||||
assert.strictEqual(readCount, 1, 'No more call should have been performed');
|
||||
|
||||
//simulate change of focus
|
||||
//original listener: $(window).on("focus", this._onWindowFocusChange.bind(this, true);
|
||||
parent.call('mail_service', '_onWindowFocusChange', true); // give focus to tab
|
||||
this.resolveTimeouts(); //one more second pass
|
||||
assert.strictEqual(readCount, 2, 'One more call should have been done once tab focused');
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('update status', function (assert) {
|
||||
// the current solution to look for updatable im_status in dom is not perfect, but waiting for
|
||||
// ability to include widgets in views, this is the most simple solution
|
||||
assert.expect(2);
|
||||
this.patchMailService();
|
||||
var StatusWidget = Widget.extend({
|
||||
start: function () {
|
||||
this.render();
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
render: function () {
|
||||
var status = QWeb.render('mail.UserStatus', {
|
||||
status: 'online',
|
||||
partnerID: 1,
|
||||
});
|
||||
this.$el.html(status);
|
||||
}
|
||||
});
|
||||
var statusWidget = new StatusWidget();
|
||||
testUtils.mock.addMockEnvironment(statusWidget, {
|
||||
services: this.services,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route));
|
||||
},
|
||||
});
|
||||
statusWidget.appendTo($('#qunit-fixture'));
|
||||
//Render unknow im_status:
|
||||
// set initial status
|
||||
assert.ok(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online'));
|
||||
|
||||
statusWidget.call('mail_service', 'updateImStatus', [
|
||||
{ id: 1, im_status: 'offline' },
|
||||
]);
|
||||
assert.notOk(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online'));
|
||||
this.resolveTimeouts();
|
||||
statusWidget.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@
|
||||
<!-- Services -->
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_document_thread_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_status_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_notification_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_window_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/js/services/mail_service.js"></script>
|
||||
@@ -129,8 +130,10 @@
|
||||
<script type="text/javascript" src="/mail/static/tests/models/cc_throttle_function_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/models/timer_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/tests/models/timers_tests.js"></script>
|
||||
<!-- activity view -->
|
||||
<!-- activity view -->
|
||||
<script type="text/javascript" src="/mail/static/tests/activity_tests.js"></script>
|
||||
<!-- services -->
|
||||
<script type="text/javascript" src="/mail/static/tests/mail_status_manager_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-extend="mail.Preview">
|
||||
<t t-jquery="div[t-attf-class*='o_mail_preview_image']" t-operation="append">
|
||||
<i t-if="preview.status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-extend="mail.UserStatus">
|
||||
<t t-jquery="i:last" t-operation="after">
|
||||
<i t-if="status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
|
||||
|
||||
Reference in New Issue
Block a user