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 @@ - -
  • - - + +
  • + +
  • + + +
    + +
    +
    + + + + + + () + + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +