[IMP] mail: navbar: messaging menu

The navbar notification is now a dropdown containing a preview of all pinned
channels (except Inbox and Starred), channels with unread messages being on the
top.

It allows to quickly detach a channel without going back to Discuss.

The channels preview fetching is performed only once, and it is done in
background without blocking the whole UI (using shadow option or session.rpc()).

Also fixes a synchronization problem between server and client about the folded
state of channels (the info wasn't stored client side, so it produced weird
situations, e.g. unfolding a channel didn't actually unfold it).
This commit is contained in:
Aaron Bohy
2016-01-14 11:22:25 +01:00
parent 0301735354
commit d93231afe2
6 changed files with 337 additions and 32 deletions
+17
View File
@@ -600,3 +600,20 @@ class Channel(models.Model):
INNER JOIN mail_channel C ON CP.channel_id = C.id
WHERE C.uuid = %s""", (uuid,))
return self._cr.dictfetchall()
@api.multi
def channel_fetch_preview(self):
""" Return the last message of the given channels """
self._cr.execute("""
SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id
FROM mail_message_mail_channel_rel
WHERE mail_channel_id IN %s
GROUP BY mail_channel_id
""", (tuple(self.ids),))
channels_preview = dict((r['message_id'], r) for r in self._cr.dictfetchall())
last_messages = self.env['mail.message'].browse(channels_preview.keys()).message_format()
for message in last_messages:
channel = channels_preview[message['id']]
del(channel['message_id'])
channel['last_message'] = message
return channels_preview.values()
+60 -10
View File
@@ -21,6 +21,7 @@ var UserModel = new Model('res.users', session.context);
//----------------------------------------------------------------------------------
var messages = [];
var channels = [];
var channels_preview_def;
var channel_defs = {};
var emojis = [];
var emoji_substitutions = {};
@@ -112,6 +113,14 @@ function strip_html (node, transform_children) {
return transform_children();
}
function inline (node, transform_children) {
if (node.nodeType === 3) return node.data;
if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV)$/)) return transform_children();
node.innerHTML = transform_children();
return node.outerHTML;
}
// Message and channel manipulation helpers
//----------------------------------------------------------------------------------
@@ -126,7 +135,6 @@ function add_message (data, options) {
messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg);
_.each(msg.channel_ids, function (channel_id) {
var channel = chat_manager.get_channel(channel_id);
var is_chat = channel && !_.contains(["public", "private"], channel.type);
if (channel) {
add_to_cache(msg, []);
if (options.domain && options.domain !== []) {
@@ -135,15 +143,15 @@ function add_message (data, options) {
if (options.increment_unread) {
update_channel_unread_counter(channel, channel.unread_counter+1);
}
}
if (channel && channel.hidden) {
channel.hidden = false;
chat_manager.bus.trigger('new_channel', channel);
}
if (is_chat && options.show_notification && (!msg.author_id || msg.author_id[0] !== session.partner_id)) {
var query = {is_displayed: false};
chat_manager.bus.trigger('anyone_listening', channel, query);
notify_incoming_message(msg, query);
if (channel.hidden) {
channel.hidden = false;
chat_manager.bus.trigger('new_channel', channel);
}
if (channel.is_chat && options.show_notification && (!msg.author_id || msg.author_id[0] !== session.partner_id)) {
var query = {is_displayed: false};
chat_manager.bus.trigger('anyone_listening', channel, query);
notify_incoming_message(msg, query);
}
}
});
if (!options.silent) {
@@ -316,6 +324,7 @@ function make_channel (data, options) {
pinned_dm_partners.push(channel.direct_partner_id);
bus.update_option('bus_presence_partner_ids', pinned_dm_partners);
}
channel.is_chat = !channel.type.match(/^(public|private|static)$/);
return channel;
}
@@ -578,11 +587,14 @@ function on_chat_session_notification (chat_session) {
if ((chat_session.state === "open") || (chat_session.state === "folded")) {
channel = chat_session.is_minimized && chat_manager.get_channel(chat_session.id);
if (channel) {
channel.is_detached = true;
channel.is_folded = (chat_session.state === "folded");
chat_manager.bus.trigger("open_chat", channel);
}
} else if (chat_session.state === "closed") {
channel = chat_manager.get_channel(chat_session.id);
if (channel) {
channel.is_detached = false;
chat_manager.bus.trigger("close_chat", channel);
}
}
@@ -845,6 +857,44 @@ var chat_manager = {
});
}
},
get_channels_preview: function (channels) {
var channels_preview = _.map(channels, function (channel) {
var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
info.last_message = _.last(channel.cache['[]'].messages);
if (!info.is_chat) {
info.image_src = '/web/image/mail.channel/'+channel.id+'/image_small';
} else if (channel.direct_partner_id) {
info.image_src = '/web/image/res.partner/'+channel.direct_partner_id+'/image_small';
} else {
info.image_src = '/mail/static/src/img/smiley/avatar.jpg';
}
return info;
});
if (!channels_preview_def) {
var missing_channel_ids = _.pluck(_.where(channels_preview, {last_message: undefined}), 'id');
if (missing_channel_ids.length) {
channels_preview_def = ChannelModel
.call('channel_fetch_preview', [missing_channel_ids], {}, {shadow: true})
.then(function (channels) {
_.each(channels, function (channel) {
var msg = add_message(channel.last_message);
_.findWhere(channels_preview, {id: channel.id}).last_message = msg;
});
});
} else {
channels_preview_def = $.when();
}
}
return channels_preview_def.then(function () {
return _.filter(channels_preview, function (channel) {
return channel.last_message; // remove empty channels
});
});
},
get_message_body_preview: function (message_body) {
return parse_and_transform(message_body, inline);
},
};
// Initialization
+89 -9
View File
@@ -7,37 +7,117 @@ var Widget = require('web.Widget');
var chat_manager = require('mail.chat_manager');
var QWeb = core.qweb;
/**
* Widget Top Menu Notification Counter
* Menu item appended in the systray part of the navbar
*
* Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to
* increment (or not) the counter. On click, should redirect to the client action.
* The menu item indicates the counter of needactions + unread messages in chat channels. When
* clicking on it, it toggles a dropdown containing a preview of each pinned channels (except
* static and mass mailing channels) with a quick link to open them in chat windows. It also
* contains a direct link to the Inbox in Discuss.
**/
var NotificationTopButton = Widget.extend({
template:'mail.chat.NotificationTopButton',
var MessagingMenu = Widget.extend({
template:'mail.chat.MessagingMenu',
events: {
"click": "on_click",
"click .o_filter_button": "on_click_filter_button",
"click .o_mail_channel_preview": "on_click_channel",
"click .o_redirect_to_discuss": "on_click_redirect_to_discuss",
},
start: function () {
this.$filter_buttons = this.$('.o_filter_button');
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
this.filter = false;
chat_manager.bus.on("update_needaction", this, this.update_counter);
this.update_counter(chat_manager.get_needaction_counter());
return this._super();
},
update_counter: function (counter) {
this.$('.o_notification_counter').html(counter);
is_open: function () {
return this.$el.hasClass('open');
},
on_click: function (event) {
update_counter: function (counter) {
this.$('.o_notification_counter').text(counter);
if (this.is_open()) {
this.render_channels_preview();
}
},
render_channels_preview: function () {
var self = this;
var channels = _.filter(chat_manager.get_channels(), function (channel) {
if (self.filter === 'chat') {
return channel.is_chat;
} else if (self.filter === 'channels') {
return !channel.is_chat && channel.type !== 'static' && !channel.mass_mailing;
} else {
return channel.type !== 'static' && !channel.mass_mailing;
}
});
// Update inbox counter and display spinner while waiting for channels preview
var inbox_counter = chat_manager.get_needaction_counter();
this.$('.o_inbox_counter').html(inbox_counter ? '(' + inbox_counter + ')' : '');
this.$channels_preview.html(QWeb.render('mail.chat.Spinner'));
chat_manager.get_channels_preview(channels).then(function (channels_preview) {
// Sort channels: 1. channels with unread messages, 2. chat, 3. by date of last msg
channels_preview.sort(function (c1, c2) {
return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) ||
c2.is_chat - c1.is_chat ||
c2.last_message.date.diff(c1.last_message.date);
});
// Generate last message preview (inline message body and compute date to display)
_.each(channels_preview, function (channel) {
channel.last_message_preview =
channel.last_message.displayed_author + ': ' +
chat_manager.get_message_body_preview(channel.last_message.body);
if (channel.last_message.date.isSame(new Date(), 'd')) { // today
channel.last_message_date = channel.last_message.date.format('LT');
} else {
channel.last_message_date = channel.last_message.date.format('lll');
}
});
self.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', {
channels: channels_preview,
}));
});
},
on_click: function () {
if (!this.is_open()) {
this.render_channels_preview(); // we are opening the dropdown so render its content
}
},
on_click_filter_button: function (event) {
event.stopPropagation();
this.$filter_buttons.removeClass('o_selected');
var $target = $(event.currentTarget);
$target.addClass('o_selected');
this.filter = $target.data('filter');
this.render_channels_preview();
},
on_click_channel: function (event) {
var channel_id = $(event.currentTarget).data('channel_id');
var channel = chat_manager.get_channel(channel_id);
if (channel) {
chat_manager.bus.trigger('detach_channel', channel);
}
},
on_click_redirect_to_discuss: function (event) {
event.preventDefault();
this.discuss_redirect();
},
discuss_redirect: _.debounce(function () {
var self = this;
var discuss_ids = chat_manager.get_discuss_ids();
this.do_action(discuss_ids.action_id, {clear_breadcrumbs: true}).then(function () {
self.trigger_up('hide_app_switcher');
core.bus.trigger('change_menu_section', discuss_ids.menu_id);
});
}, 1000, true),
});
SystrayMenu.Items.push(NotificationTopButton);
SystrayMenu.Items.push(MessagingMenu);
});
+21 -9
View File
@@ -25,16 +25,12 @@ var display_state = {
function open_chat (session) {
var chat_session = _.findWhere(chat_sessions, {id: session.id});
if (!chat_session) {
var channel = chat_manager.get_channel(session.id);
var chat_session = {
chat_session = {
id: session.id,
uuid: session.uuid,
name: session.name,
window: new ChatWindow(web_client, session.id, session.name, session.is_folded, channel.unread_counter),
window: new ChatWindow(web_client, session.id, session.name, session.is_folded, session.unread_counter),
};
if (!session.is_folded) {
chat_manager.mark_channel_as_seen(channel);
}
chat_session.window.on("close_chat_session", null, function () {
close_chat(chat_session);
chat_manager.close_chat_session(chat_session.id);
@@ -52,7 +48,7 @@ function open_chat (session) {
chat_manager.post_message(message, {channel_id: channel_id});
});
chat_session.window.on("messages_read", null, function () {
chat_manager.mark_channel_as_seen(channel);
chat_manager.mark_channel_as_seen(session);
});
chat_session.window.on("redirect", null, function (res_model, res_id) {
chat_manager.redirect(res_model, res_id, open_chat);
@@ -79,9 +75,16 @@ function open_chat (session) {
}).then(function (messages) {
chat_session.window.render(messages);
chat_session.window.scrollBottom();
if (!session.is_folded) {
chat_manager.mark_channel_as_seen(session);
}
});
} else if (chat_session.window.is_hidden) {
make_session_visible(chat_session);
} else {
if (chat_session.window.is_hidden) {
make_session_visible(chat_session);
} else if (session.is_folded !== chat_session.window.folded) {
chat_session.window.toggle_fold(session.is_folded);
}
}
}
@@ -253,6 +256,15 @@ core.bus.on('web_client_ready', null, function () {
});
});
chat_manager.bus.on('detach_channel', null, function (channel) {
var chat_session = _.findWhere(chat_sessions, {id: channel.id});
if (!chat_session || chat_session.window.folded) {
chat_manager.detach_channel(channel);
} else if (chat_session && chat_session.window.is_hidden) {
make_session_visible(chat_session);
}
});
core.bus.on('resize', null, reposition_windows);
});
@@ -259,3 +259,108 @@
color: orange;
}
}
// Navbar icon and dropdown (FIXME: move to systray.less)
.o_mail_navbar_item {
&.open .o_mail_navbar_dropdown {
.o-flex-display();
.o-flex-flow(column, nowrap);
}
.o_mail_navbar_dropdown {
width: 350px;
padding: 0;
.o_spinner {
.o-flex-display();
.o-align-items(center);
.o-justify-content(center);
height: 50px;
}
.o_filter_button,
.o_redirect_to_discuss {
.btn-link;
padding: 5px;
}
.o_mail_navbar_dropdown_top {
.o-flex(0, 0, auto);
border-bottom: 1px solid lightgray;
.o_filter_button {
color: @odoo-main-color-muted;
&:hover, &.o_selected {
color: @odoo-brand-optional;
}
&.o_selected {
cursor: default;
font-weight: bold;
}
}
}
.o_mail_navbar_dropdown_bottom {
.o-flex(0, 0, auto);
border-top: 1px solid lightgray;
height: 35px;
.o_redirect_to_discuss {
height: 100%;
width: 100%;
}
}
.o_mail_navbar_dropdown_channels {
.o-flex(0, 1, auto);
max-height: 400px;
min-height: 50px;
overflow-y: auto;
.o_mail_channel_preview {
.o-flex-display();
height: 50px;
align-items: center;
padding: 5px;
cursor: pointer;
font-size: 12px;
&~.o_mail_channel_preview {
border-top: 1px solid lightgray;
}
&.o_channel_unread {
background-color: lighten(lightgray, 10%);
}
&:hover {
background-color: lighten(lightgray, 5%);
}
.o_channel_image {
.o-flex(0, 0, 36px);
max-height: 36px;
}
.o_channel_info {
.o-flex(1, 1, 100%);
margin-left: 5px;
overflow: hidden;
.o_channel_title {
.o-flex-display();
.o_channel_name {
.o-flex(1, 1, auto);
.o-text-overflow();
}
.o_last_message_date {
.o-flex(0, 0, auto);
padding-top: 3px;
font-size: xx-small;
color: @odoo-main-color-muted;
}
}
.o_last_message_preview {
width: 100%;
color: @odoo-main-color-muted;
.o-text-overflow();
}
}
}
}
}
}
+45 -4
View File
@@ -1,13 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="mail.chat.NotificationTopButton">
<li title="Notifications">
<a href="#">
<i class="fa fa-comment"/> <span class="o_notification_counter"/>
<t t-name="mail.chat.MessagingMenu">
<li class="o_mail_navbar_item">
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" title="Messages" href="#">
<i class="fa fa-comments"/> <span class="o_notification_counter"/>
</a>
<ul class="o_mail_navbar_dropdown dropdown-menu" role="menu">
<li class="o_mail_navbar_dropdown_top">
<button type="button" class="btn btn-sm o_filter_button o_selected"> All </button>
<button type="button" class="btn btn-sm o_filter_button" data-filter='chat'> Chat </button>
<button type="button" class="btn btn-sm o_filter_button" data-filter='channels'> Channels </button>
</li>
<li class="o_mail_navbar_dropdown_channels"/>
<li class="o_mail_navbar_dropdown_bottom">
<button type="button" class="btn o_redirect_to_discuss">
@Inbox <span class="o_inbox_counter"/>
</button>
</li>
</ul>
</li>
</t>
<t t-name="mail.chat.ChannelsPreview">
<t t-foreach="channels" t-as="channel">
<div t-attf-class="o_mail_channel_preview #{channel.unread_counter ? 'o_channel_unread' : ''}"
t-att-data-channel_id="channel.id">
<img class="o_channel_image" t-att-src="channel.image_src"/>
<div class="o_channel_info">
<div class="o_channel_title">
<span class="o_channel_name">
<t t-if="channel.status" t-call="mail.chat.UserStatus">
<t t-set="status" t-value="channel.status"/>
</t>
<t t-esc="channel.name"/>
<t t-if="channel.unread_counter">(<t t-esc="channel.unread_counter"/>)</t>
</span>
<span class="o_last_message_date"> <t t-esc="channel.last_message_date"/> </span>
</div>
<div class="o_last_message_preview">
<t t-raw="channel.last_message_preview"/>
</div>
</div>
</div>
</t>
</t>
<t t-name="mail.chat.Spinner">
<div class="o_spinner"> <i class="fa fa-spinner fa-spin"/> </div>
</t>
</templates>