Permalink
Browse files

[IMP] mail: channel seen icon indication ('v' and 'vv')

This commit adds the seen icon next to a message for all channels
except livechat.

It tracks received and seen messages for all members of the channel.
Icon is either 1 or 2 check(s) ('v' or 'vv').

Those are the different states of this 'seen' icon:

  ICON:                           DESCRIPTION:

  no check .................. no one has received the message.
  1 check ................... some members have received the
                              message, no one has seen it.
  2 checks (greyed-out) ..... some members have seen the message,
                              some or everyone has received it
  2 checks (green) .......... all members have seen the message

This icon is not shown for messages preceding the last message seen
by everyone, in order to reduce visual noise.

Task-ID 1881001
  • Loading branch information...
alexkuhn committed Dec 5, 2018
1 parent b954b4a commit 1e863ee5b1658c90df20d097331cb41669fdc79f
@@ -28,6 +28,7 @@ class ChannelPartner(models.Model):
partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade')
partner_email = fields.Char('Email', related='partner_id.email', readonly=False)
channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade')
fetched_message_id = fields.Many2one('mail.message', string='Last Fetched')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimized")
@@ -594,12 +595,16 @@ def channel_info(self, extra_info = False):
partner_channel = partner_channel[0]
info['state'] = partner_channel.fold_state or 'open'
info['is_minimized'] = partner_channel.is_minimized
info['seen_message_id'] = partner_channel.seen_message_id.id
info['custom_channel_name'] = partner_channel.custom_channel_name
# add members infos
partner_ids = channel_partners.mapped('partner_id').ids
info['members'] = [partner_info for partner_info in partner_infos if partner_info['id'] in partner_ids]
info['seen_partners_info'] = [{
'partner_id': cp.partner_id.id,
'fetched_message_id': cp.fetched_message_id.id,
'seen_message_id': cp.seen_message_id.id,
} for cp in channel_partners]
channel_infos.append(info)
return channel_infos
@@ -717,10 +722,36 @@ def channel_seen(self):
self.ensure_one()
if self.channel_message_ids.ids:
last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id})
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id})
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({
'seen_message_id': last_message_id,
'fetched_message_id': last_message_id,
})
data = {
'info': 'channel_seen',
'last_message_id': last_message_id,
'partner_id': self.env.user.partner_id.id,
}
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]])
return last_message_id
@api.multi
def channel_fetched(self):
""" Broadcast the channel_fetched notification to channel members
"""
self.ensure_one()
if not self.channel_message_ids.ids:
return
last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({
'fetched_message_id': last_message_id,
})
data = {
'info': 'channel_fetched',
'last_message_id': last_message_id,
'partner_id': self.env.user.partner_id.id,
}
self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', self.id), data]])
@api.multi
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
@@ -1078,7 +1078,8 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
.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);
.on('update_typing_partners', this, this._onTypingPartnersUpdated)
.on('update_channel', this, this._onUpdateChannel);
},
/**
* @private
@@ -1646,6 +1647,16 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
_onUnstarAllClicked: function () {
this.call('mail_service', 'unstarAll');
},
/**
* @private
* @param {integer} channelID
*/
_onUpdateChannel: function (channelID) {
if (this._thread.getID() !== channelID) {
return;
}
this._fetchAndRenderThread();
},
/**
* @private
*/
@@ -102,6 +102,16 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, {
hasMessages: function () {
return !_.isEmpty(this.getMessages());
},
/**
* States whether this thread is compatible with the 'seen' feature.
* By default, threads do not have thsi feature active.
* @see {mail.model.ThreadSeenMixin} to enable this feature on a thread.
*
* @returns {boolean}
*/
hasSeenFeature: function () {
return false;
},
/**
* States whether this thread is compatible with the 'is typing...' feature.
* By default, threads do not have this feature active.
@@ -1,6 +1,7 @@
odoo.define('mail.model.Channel', function (require) {
"use strict";
var ChannelSeenMixin = require('mail.model.ChannelSeenMixin');
var SearchableThread = require('mail.model.SearchableThread');
var ThreadTypingMixin = require('mail.model.ThreadTypingMixin');
var mailUtils = require('mail.utils');
@@ -16,7 +17,7 @@ var time = require('web.time');
* Any piece of code in JS that make use of channels must ideally interact with
* such objects, instead of direct data from the server.
*/
var Channel = SearchableThread.extend(ThreadTypingMixin, {
var Channel = SearchableThread.extend(ChannelSeenMixin, ThreadTypingMixin, {
/**
* @override
* @param {Object} params
@@ -36,6 +37,10 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
* @param {integer} [params.data.message_unread_counter]
* @param {boolean} [params.data.moderation=false] whether the channel is
* moderated or not
* @param {Object[]} [params.data.partners_info=[]]
* @param {integer} [params.data.partners_info[i].partner_id]
* @param {integer} [params.data.partners_info[i].fetched_message_id]
* @param {integer} [params.data.partners_info[i].seen_message_id]
* @param {string} params.data.state
* @param {string} [params.data.uuid]
* @param {Object} params.options
@@ -45,7 +50,8 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
init: function (params) {
var self = this;
this._super.apply(this, arguments);
ThreadTypingMixin.init.call(this, arguments);
ChannelSeenMixin.init.apply(this, arguments);
ThreadTypingMixin.init.apply(this, arguments);
var data = params.data;
var options = params.options;
@@ -72,7 +78,6 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
// number of messages in this channel that are in inbox.
this._needactionCounter = data.message_needaction_counter || 0;
this._serverType = data.channel_type;
this._throttleFetchSeen = _.throttle(this._fetchSeen.bind(this), 3000);
// unique identifier for this channel, which is required for some rpc
this._uuid = data.uuid;
@@ -335,23 +340,6 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
// Private
//--------------------------------------------------------------------------
/**
* @private
* @returns {$.Promise<integer>} resolved with ID of last seen message
*/
_fetchSeen: function () {
var self = this;
return this._rpc({
model: 'mail.channel',
method: 'channel_seen',
args: [[this._id]],
}, {
shadow: true
}).then(function (lastSeenMessageID) {
self._lastSeenMessageID = lastSeenMessageID;
return lastSeenMessageID;
});
},
/**
* Override so that it tells whether the channel is moderated or not. This
* is useful in order to display pending moderation messages when the
@@ -399,9 +387,23 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
*/
_markAsRead: function () {
var superDef = this._super.apply(this, arguments);
var seenDef = this._throttleFetchSeen();
var seenDef = this._throttleNotifySeen();
return $.when(superDef, seenDef);
},
/**
* @override {mail.model.ThreadSeenMixin}
* @private
* @returns {$.Promise}
*/
_notifyFetched: function () {
return this._rpc({
model: 'mail.channel',
method: 'channel_fetched',
args: [[this._id]],
}, {
shadow: true
});
},
/**
* @override {mail.model.ThreadTypingMixin}
* @private
@@ -417,6 +419,25 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
kwargs: { is_typing: params.typing },
}, { shadow: true });
},
/**
* @override {mail.model.ThreadSeenMixin}
* @private
* @returns {$.Promise<integer>} resolved with ID of last seen message
*/
_notifySeen: function () {
var self = this;
this._cancelThrottledNotifyFetched();
return this._rpc({
model: 'mail.channel',
method: 'channel_seen',
args: [[this._id]],
}, {
shadow: true
}).then(function (lastSeenMessageID) {
self._lastSeenMessageID = lastSeenMessageID;
return lastSeenMessageID;
});
},
/**
* Prepare and send a message to the server on this channel.
*
Oops, something went wrong.

0 comments on commit 1e863ee

Please sign in to comment.