From d93231afe201997305759767e24e6df0972175fe Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 31 Dec 2015 15:22:26 +0100 Subject: [PATCH] [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). --- addons/mail/models/mail_channel.py | 17 +++ addons/mail/static/src/js/chat_manager.js | 70 ++++++++++-- addons/mail/static/src/js/systray.js | 98 ++++++++++++++-- addons/mail/static/src/js/window_manager.js | 30 +++-- .../mail/static/src/less/client_action.less | 105 ++++++++++++++++++ addons/mail/static/src/xml/systray.xml | 49 +++++++- 6 files changed, 337 insertions(+), 32 deletions(-) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index f577354a8c7..08b827b6cc7 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -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() diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 8e5d9d51f4c..150e803920c 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -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 diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index 293521ec045..74209a967ec 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -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); }); diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js index 7c26dc88c82..0459e13f4b7 100644 --- a/addons/mail/static/src/js/window_manager.js +++ b/addons/mail/static/src/js/window_manager.js @@ -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); }); diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less index 8ab1c9f221c..117047a527d 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -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(); + } + } + } + } + } +} diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index e503f87e095..f818969db98 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -1,13 +1,54 @@ - -
  • - - + +
  • + +
  • + + +
    + +
    +
    + + + + + + () + + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +