diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-01-27 12:24:04 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-01-27 12:24:04 +0100 |
| commit | 78d20b552b4374b94aec7097c0e6a48a3dd2d961 (patch) | |
| tree | 5acd7cbf1cc9e2f277034f44fc0ebebaa3a1a79c /addons/mail/static/src | |
| parent | 740a6eadd379cc3fa26aefb9350f1c95b3e05ebd (diff) | |
| parent | 7e44ddcda3f21c7e6401173539dc0fe93f9a7dd2 (diff) | |
[MERGE] forward port of branch saas-8 up to 7e44ddc
Diffstat (limited to 'addons/mail/static/src')
| -rw-r--r-- | addons/mail/static/src/img/odoo_o.png | bin | 0 -> 556 bytes | |||
| -rw-r--r-- | addons/mail/static/src/js/chat_manager.js | 242 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chat_window.js | 49 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chatter.js | 25 | ||||
| -rw-r--r-- | addons/mail/static/src/js/client_action.js | 71 | ||||
| -rw-r--r-- | addons/mail/static/src/js/composer.js | 33 | ||||
| -rw-r--r-- | addons/mail/static/src/js/systray.js | 137 | ||||
| -rw-r--r-- | addons/mail/static/src/js/thread.js | 3 | ||||
| -rw-r--r-- | addons/mail/static/src/js/window_manager.js | 289 | ||||
| -rw-r--r-- | addons/mail/static/src/less/chat_window.less | 86 | ||||
| -rw-r--r-- | addons/mail/static/src/less/client_action.less | 140 | ||||
| -rw-r--r-- | addons/mail/static/src/less/composer.less | 2 | ||||
| -rw-r--r-- | addons/mail/static/src/less/thread.less | 13 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/chat_window.xml | 56 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/client_action.xml | 21 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/composer.xml | 6 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/systray.xml | 57 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/thread.xml | 8 |
18 files changed, 1011 insertions, 227 deletions
diff --git a/addons/mail/static/src/img/odoo_o.png b/addons/mail/static/src/img/odoo_o.png Binary files differnew file mode 100644 index 00000000000..d1839f6df41 --- /dev/null +++ b/addons/mail/static/src/img/odoo_o.png diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 0bed20620b3..6fd85e33719 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -16,12 +16,15 @@ var preview_msg_max_size = 350; // optimal for native english speakers var MessageModel = new Model('mail.message', session.context); var ChannelModel = new Model('mail.channel', session.context); var UserModel = new Model('res.users', session.context); +var PartnerModel = new Model('res.partner', session.context); // Private model //---------------------------------------------------------------------------------- var messages = []; var channels = []; +var channels_preview_def; var channel_defs = {}; +var chat_unread_counter = 0; var emojis = []; var emoji_substitutions = {}; var needaction_counter = 0; @@ -50,9 +53,9 @@ bus.on("window_focus", null, function() { function notify_incoming_message (msg, options) { var title = _t('New message'); if (msg.author_id[1]) { - title += _t(' from ') + _.escape(msg.author_id[1]); + title = _.escape(msg.author_id[1]); } - var content = $(msg.body).text().substr(0, preview_msg_max_size); + var content = parse_and_transform(msg.body, strip_html).substr(0, preview_msg_max_size); if (bus.is_odoo_focused()) { if (!options.is_displayed) { @@ -65,7 +68,7 @@ function notify_incoming_message (msg, options) { if (Notification && Notification.permission === "granted") { if (bus.is_master) { - new Notification(title, {body: content, icon: "/web/static/src/img/odoo.png", silent: false}); + new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png", silent: false}); } } else { web_client.do_notify(title, content); @@ -98,7 +101,7 @@ function add_link (node, transform_children) { if (node.nodeType === 3) { // text node return node.data.replace(url_regexp, function (url) { var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url; - return '<a href="' + href + '">' + url + '</a>'; + return '<a target="_blank" href="' + href + '">' + url + '</a>'; }); } if (node.tagName === "A") return node.outerHTML; @@ -106,6 +109,20 @@ function add_link (node, transform_children) { return node.outerHTML; } +function strip_html (node, transform_children) { + if (node.nodeType === 3) return node.data; // text node + if (node.tagName === "BR") return "\n"; + 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|PRE)$/)) return transform_children(); + node.innerHTML = transform_children(); + return node.outerHTML; +} + // Message and channel manipulation helpers //---------------------------------------------------------------------------------- @@ -116,28 +133,29 @@ function add_message (data, options) { if (!msg) { msg = make_message(data); - // Keep the array ordered by date when inserting the new message + // Keep the array ordered by id when inserting the new message 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 !== []) { add_to_cache(msg, options.domain); } - if (options.increment_unread) { - update_channel_unread_counter(channel, channel.unread_counter+1); + if (channel.hidden) { + channel.hidden = false; + chat_manager.bus.trigger('new_channel', channel); + } + if (!msg.author_id || msg.author_id[0] !== session.partner_id) { + if (options.increment_unread) { + update_channel_unread_counter(channel, channel.unread_counter+1); + } + if (channel.is_chat && options.show_notification) { + var query = {is_displayed: false}; + chat_manager.bus.trigger('anyone_listening', channel, query); + notify_incoming_message(msg, query); + } } - } - 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 (!options.silent) { @@ -289,7 +307,7 @@ function make_channel (data, options) { display_needactions: options.display_needactions, mass_mailing: data.mass_mailing, needaction_counter: data.message_needaction_counter || 0, - unread_counter: data.message_unread_counter || 0, + unread_counter: 0, last_seen_message_id: data.seen_message_id, cache: {'[]': { all_history_loaded: false, @@ -309,6 +327,12 @@ function make_channel (data, options) { channel.status = data.direct_partner[0].im_status; pinned_dm_partners.push(channel.direct_partner_id); bus.update_option('bus_presence_partner_ids', pinned_dm_partners); + } else if ('anonymous_name' in data) { + channel.name = data.anonymous_name; + } + channel.is_chat = !channel.type.match(/^(public|private|static)$/); + if (data.message_unread_counter) { + update_channel_unread_counter(channel, data.message_unread_counter); } return channel; } @@ -425,37 +449,56 @@ function fetch_document_messages (ids, options) { } function update_channel_unread_counter (channel, counter) { + if (channel.is_chat) { + chat_unread_counter = chat_unread_counter - channel.unread_counter + counter; + } channel.unread_counter = counter; chat_manager.bus.trigger("update_channel_unread_counter", channel); } var channel_seen = _.throttle(function (channel) { - return ChannelModel.call('channel_seen', [[channel.id]]).then(function (last_seen_message_id) { - channel.last_seen_message_id = last_seen_message_id; - }); + return ChannelModel.call('channel_seen', [[channel.id]], {}, {shadow: true}); }, 3000); // Notification handlers // --------------------------------------------------------------------------------- -function on_notification (notification) { - var model = notification[0][1]; - if (model === 'ir.needaction') { - // new message in the inbox - on_needaction_notification(notification[1]); - } else if (model === 'mail.channel') { - // new message in a channel - on_channel_notification(notification[1]); - } else if (model === 'res.partner') { - // channel joined/left, message marked as read/(un)starred, chat open/closed - on_partner_notification(notification[1]); - } else if (model === 'bus.presence') { - // update presence of users - on_presence_notification(notification[1]); +function on_notification (notifications) { + // sometimes, the web client receives unsubscribe notification and an extra + // notification on that channel. This is then followed by an attempt to + // rejoin the channel that we just left. The next few lines remove the + // extra notification to prevent that situation to occur. + var unsubscribed_notif = _.find(notifications, function (notif) { + return notif[1].info === "unsubscribe"; + }); + if (unsubscribed_notif) { + notifications = _.reject(notifications, function (notif) { + return notif[0][1] === "mail.channel" && notif[0][2] === unsubscribed_notif[1].id; + }); } + _.each(notifications, function (notification) { + var model = notification[0][1]; + if (model === 'ir.needaction') { + // new message in the inbox + on_needaction_notification(notification[1]); + } else if (model === 'mail.channel') { + // new message in a channel + on_channel_notification(notification[1]); + } else if (model === 'res.partner') { + // channel joined/left, message marked as read/(un)starred, chat open/closed + on_partner_notification(notification[1]); + } else if (model === 'bus.presence') { + // update presence of users + on_presence_notification(notification[1]); + } + }); } function on_needaction_notification (message) { - message = add_message(message, { channel_id: 'channel_inbox', show_notification: true} ); + message = add_message(message, { + channel_id: 'channel_inbox', + show_notification: true, + increment_unread: true, + }); invalidate_caches(message.channel_ids); needaction_counter++; _.each(message.channel_ids, function (channel_id) { @@ -490,6 +533,8 @@ function on_partner_notification (data) { on_mark_as_read_notification(data); } else if (data.type === 'mark_as_unread') { on_mark_as_unread_notification(data); + } else if (data.info === 'channel_seen') { + on_channel_seen_notification(data); } else { on_chat_session_notification(data); } @@ -526,7 +571,7 @@ function on_mark_as_read_notification (data) { _.each(data.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); if (channel) { - channel.needaction_counter -= data.message_ids.length; + channel.needaction_counter = Math.max(channel.needaction_counter - data.message_ids.length, 0); } }); } else { // if no channel_ids specified, this is a 'mark all read' in the inbox @@ -534,7 +579,7 @@ function on_mark_as_read_notification (data) { channel.needaction_counter = 0; }); } - needaction_counter -= data.message_ids.length; + needaction_counter = Math.max(needaction_counter - data.message_ids.length, 0); chat_manager.bus.trigger('update_needaction', needaction_counter); } @@ -560,6 +605,16 @@ function on_mark_as_unread_notification (data) { chat_manager.bus.trigger('update_needaction', needaction_counter); } +function on_channel_seen_notification (data) { + var channel = chat_manager.get_channel(data.id); + if (channel) { + channel.last_seen_message_id = data.last_message_id; + if (channel.unread_counter) { + update_channel_unread_counter(channel, 0); + } + } +} + function on_chat_session_notification (chat_session) { var channel; if ((chat_session.channel_type === "channel") && (chat_session.state === "open")) { @@ -572,18 +627,21 @@ 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); } } } function on_presence_notification (data) { - var dm = _.findWhere(channels, {direct_partner_id: data.id}); + var dm = chat_manager.get_dm_from_partner_id(data.id); if (dm) { dm.status = data.im_status; chat_manager.bus.trigger('update_dm_presence', dm); @@ -696,7 +754,7 @@ var chat_manager = { return MessageModel.call('mark_as_unread', [message_ids, [channel.id]]); }, mark_channel_as_seen: function (channel) { - if (channel.unread_counter > 0) { + if (channel.unread_counter > 0 && channel.type !== 'static') { update_channel_unread_counter(channel, 0); channel_seen(channel); } @@ -710,6 +768,10 @@ var chat_manager = { return _.findWhere(channels, {id: id}); }, + get_dm_from_partner_id: function (partner_id) { + return _.findWhere(channels, {direct_partner_id: partner_id}); + }, + all_history_loaded: function (channel, domain) { return get_channel_cache(channel, domain).all_history_loaded; }, @@ -720,7 +782,7 @@ var chat_manager = { } if (!channel.members_deferred) { channel.members_deferred = ChannelModel - .call("channel_fetch_listeners", [channel.uuid]) + .call("channel_fetch_listeners", [channel.uuid], {}, {shadow: true}) .then(function (members) { var suggestions = []; _.each(mention_partner_suggestions, function (partners) { @@ -729,7 +791,7 @@ var chat_manager = { })); }); - return [members].concat(suggestions); + return [members]; }); } return channel.members_deferred; @@ -742,13 +804,31 @@ var chat_manager = { get_needaction_counter: function () { return needaction_counter; }, + get_chat_unread_counter: function () { + return chat_unread_counter; + }, + + get_last_seen_message: function (channel) { + if (channel.last_seen_message_id) { + var messages = channel.cache['[]'].messages; + var msg = _.findWhere(messages, {id: channel.last_seen_message_id}); + if (msg) { + var i = _.sortedIndex(messages, msg, 'id') + 1; + while (i < messages.length && messages[i].author_id && messages[i].author_id[0] === session.partner_id) { + msg = messages[i]; + i++; + } + return msg; + } + } + }, get_discuss_ids: function () { return discuss_ids; }, detach_channel: function (channel) { - return ChannelModel.call("channel_minimize", [channel.uuid, true]); + return ChannelModel.call("channel_minimize", [channel.uuid, true], {}, {shadow: true}); }, remove_chatter_messages: function (model) { messages = _.reject(messages, function (message) { @@ -783,6 +863,9 @@ var chat_manager = { } return channel_defs[channel_id]; }, + open_and_detach_dm: function (partner_id) { + return ChannelModel.call('channel_get_and_minimize', [[partner_id]]).then(add_channel); + }, unsubscribe: function (channel) { var def; @@ -797,13 +880,16 @@ var chat_manager = { }, close_chat_session: function (channel_id) { var channel = this.get_channel(channel_id); - ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}); + ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}, {shadow: true}); }, - fold_channel: function (channel_id) { - var channel = this.get_channel(channel_id); - return ChannelModel.call("channel_fold", [], {uuid : channel.uuid}).then(function () { - channel.is_folded = !channel.is_folded; - }); + fold_channel: function (channel_id, folded) { + var args = { + uuid: this.get_channel(channel_id).uuid, + }; + if (_.isBoolean(folded)) { + args.state = folded ? 'folded' : 'open'; + } + return ChannelModel.call("channel_fold", [], args, {shadow: true}); }, /** * Special redirection handling for given model and id @@ -839,6 +925,58 @@ 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); + }, + + search_partner: function (search_val, limit) { + return PartnerModel.call('im_search', [search_val, limit || 20], {}, {shadow: true}).then(function(result) { + var values = []; + _.each(result, function(user) { + var escaped_name = _.escape(user.name); + values.push(_.extend(user, { + 'value': escaped_name, + 'label': escaped_name, + })); + }); + return values; + }); + }, }; // Initialization @@ -864,13 +1002,13 @@ function init () { mention_partner_suggestions = result.mention_partner_suggestions; emojis = result.emoji; _.each(emojis, function(emoji) { - emoji_substitutions[emoji.source] = emoji.substitution; + emoji_substitutions[_.escape(emoji.source)] = emoji.substitution; }); }); var ir_model = new Model("ir.model.data"); - var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]); - var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]); + var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"], {}, {shadow: true}); + var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"], {}, {shadow: true}); bus.on('notification', null, on_notification); diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index ca36b430791..95ac9dd8897 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -3,6 +3,7 @@ odoo.define('mail.ChatWindow', function (require) { var ChatThread = require('mail.ChatThread'); +var config = require('web.config'); var core = require('web.core'); var Widget = require('web.Widget'); @@ -23,15 +24,15 @@ return Widget.extend({ this._super(parent); this.title = title; this.channel_id = channel_id; - this.placeholder = _t("Say something"); this.folded = is_folded; this.options = _.defaults(options || {}, { display_stars: true, + placeholder: _t("Say something"), }); this.unread_msgs = unread_msgs || 0; + this.is_hidden = false; }, start: function () { - this.$content = this.$('.o_chat_content'); this.$input = this.$('.o_chat_input input'); this.thread = new ChatThread(this, { @@ -45,28 +46,21 @@ return Widget.extend({ if (this.folded) { this.$el.css('height', HEIGHT_FOLDED); + } else { + this.focus_input(); } - var def = this.thread.appendTo(this.$content); + var def = this.thread.replace(this.$('.o_chat_content')); return $.when(this._super(), def); }, render: function (messages) { - this.update_header(); + this.update_unread(this.unread_msgs); this.thread.render(messages, {display_load_more: false}); }, - update_header: function () { - var title = this.unread_msgs > 0 ? - this.title + ' (' + this.unread_msgs + ')' : this.title; - this.$('.o_chat_title').text(title); - }, update_unread: function (counter) { this.unread_msgs = counter; - this.update_header(); - }, - scrollBottom: function () { - this.$content.scrollTop(this.$content[0].scrollHeight); + this.$('.o_unread_counter').text(counter > 0 ? '(' + counter + ')' : ''); }, fold: function () { - this.update_header(); this.$el.animate({ height: this.folded ? HEIGHT_FOLDED : HEIGHT_OPEN }); @@ -74,11 +68,28 @@ return Widget.extend({ toggle_fold: function (fold) { this.folded = _.isBoolean(fold) ? fold : !this.folded; if (!this.folded) { - this.unread_msgs = 0; - this.trigger('messages_read'); + this.thread.scroll_to(); + this.focus_input(); } this.fold(); }, + focus_input: function () { + if (!config.device.touch) { + this.$input.focus(); + } + }, + do_show: function () { + this.is_hidden = false; + this._super.apply(this, arguments); + }, + do_hide: function () { + this.is_hidden = true; + this._super.apply(this, arguments); + }, + do_toggle: function (display) { + this.is_hidden = _.isBoolean(display) ? !display : !this.is_hidden; + this._super.apply(this, arguments); + }, on_keydown: function (event) { // ENTER key (avoid requiring jquery ui for external livechat) if (event.which === 13) { @@ -100,8 +111,10 @@ return Widget.extend({ this.trigger("close_chat_session"); }, on_click_fold: function () { - this.trigger("fold_channel", this.channel_id); - this.toggle_fold(); + if (config.device.size_class !== config.device.SIZES.XS) { + this.toggle_fold(); + this.trigger("fold_channel", this.channel_id, this.folded); + } }, }); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 28a269379df..dc528b2b1aa 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -451,6 +451,7 @@ var ChatterComposer = composer.BasicComposer.extend({ record_name: false, is_log: false, internal_subtypes: [], + default_body: '', }); if (this.options.is_log) { this.options.send_text = _('Log'); @@ -467,6 +468,17 @@ var ChatterComposer = composer.BasicComposer.extend({ return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients()); }, + start: function () { + var self = this; + return this._super().then(function () { + self.$input.val(self.options.default_body); + }); + }, + + should_send: function () { + return false; + }, + preprocess_message: function () { var self = this; var def = $.Deferred(); @@ -862,6 +874,8 @@ var Chatter = form_common.AbstractField.extend({ // destroy current composer, if any if (this.composer) { this.composer.destroy(); + this.composer = undefined; + this.mute_new_message_button(false); } // fetch and render messages of current document @@ -891,6 +905,7 @@ var Chatter = form_common.AbstractField.extend({ internal_subtypes: this.options.internal_subtypes, is_log: options && options.is_log, record_name: this.record_name, + default_body: old_composer && old_composer.$input.val(), }); this.composer.on('input_focused', this, function () { this.composer.mention_set_prefetched_partners(this.mention_suggestions || []); @@ -906,11 +921,19 @@ var Chatter = form_common.AbstractField.extend({ self.composer.on('post_message', self, self.on_post_message); self.composer.on('need_refresh', self, self.refresh_followers); }); - + this.mute_new_message_button(true); }, close_composer: function () { if (this.composer.is_empty()) { this.composer.do_hide(); + this.mute_new_message_button(false); + } + }, + mute_new_message_button: function (mute) { + if (mute) { + this.$('.o_chatter_button_new_message').removeClass('btn-primary').addClass('btn-default'); + } else if (!mute) { + this.$('.o_chatter_button_new_message').removeClass('btn-default').addClass('btn-primary'); } }, diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 9856e8f3e75..d6a5b5ac128 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -182,8 +182,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.searchview = new SearchView(this, dataset, view_id, {}, options); this.searchview.on('search_data', this, this.on_search); - this.basic_composer = new composer.BasicComposer(this); - this.extended_composer = new composer.ExtendedComposer(this); + this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true}); + this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true}); this.thread = new ChatThread(this, { display_help: true, shorten_messages: false, @@ -202,6 +202,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { chat_manager.mark_all_as_read(); }); this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all); + this.$buttons.on('click', '.o_mail_chat_button_new_message', this.on_click_new_message); this.thread.on('redirect', this, function (res_model, res_id) { chat_manager.redirect(res_model, res_id, this.set_channel.bind(this)); @@ -224,7 +225,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, { var def1 = this.thread.appendTo(this.$('.o_mail_chat_content')); var def2 = this.basic_composer.appendTo(this.$('.o_mail_chat_content')); var def3 = this.extended_composer.appendTo(this.$('.o_mail_chat_content')); - var def4 = this.searchview.appendTo($("<div>")); + var def4 = this.searchview.appendTo($("<div>")).then(function () { + self.$searchview_buttons = self.searchview.$buttons.contents(); + }); this.render_sidebar(); @@ -235,12 +238,17 @@ var ChatAction = Widget.extend(ControlPanelMixin, { chat_manager.bus.on('update_message', self, self.on_update_message); chat_manager.bus.on('new_channel', self, self.on_new_channel); chat_manager.bus.on('anyone_listening', self, function (channel, query) { - query.is_displayed = query.is_displayed || channel.id === self.channel.id; + query.is_displayed = query.is_displayed || (channel.id === self.channel.id && self.thread.is_at_bottom()); }); chat_manager.bus.on('unsubscribe_from_channel', self, self.render_sidebar); chat_manager.bus.on('update_needaction', self, self.throttled_render_sidebar); chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_render_sidebar); chat_manager.bus.on('update_dm_presence', self, self.throttled_render_sidebar); + self.thread.$el.on("scroll", null, _.debounce(function () { + if (self.thread.is_at_bottom()) { + chat_manager.mark_channel_as_seen(self.channel); + } + }, 100)); }); }, @@ -282,9 +290,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({ source: function(request, response) { self.last_search_val = _.escape(request.term); - self.do_search_partner(self.last_search_val).done(function(result){ - response(result); - }); + chat_manager.search_partner(self.last_search_val).done(response); }, select: function(event, ui) { var partner_id = ui.item.id; @@ -329,21 +335,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }); }, - do_search_partner: function (search_val) { - var Partner = new Model("res.partner"); - return Partner.call('im_search', [search_val, 20]).then(function(result){ - var values = []; - _.each(result, function(user){ - var escaped_name = _.escape(user.name); - values.push(_.extend(user, { - 'value': escaped_name, - 'label': escaped_name, - })); - }); - return values; - }); - }, - set_channel: function (channel) { var self = this; // Store scroll position of previous channel @@ -380,13 +371,13 @@ var ChatAction = Widget.extend(ControlPanelMixin, { // Hide 'detach' button in static channels self.$buttons .find('.o_mail_chat_button_detach') - .toggle(channel.type !== "static"); + .toggle(channel.type !== "static" && !channel.mass_mailing); // Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM self.$buttons .find('.o_mail_chat_button_invite, .o_mail_chat_button_unsubscribe, .o_mail_chat_button_settings') .toggle(channel.type !== "dm" && channel.type !== 'static'); self.$buttons - .find('.o_mail_chat_button_mark_read') + .find('.o_mail_chat_button_mark_read, .o_mail_chat_button_new_message') .toggle(channel.id === "channel_inbox"); self.$buttons .find('.o_mail_chat_button_unstar_all') @@ -443,13 +434,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, { // Compute position of the 'New messages' separator, only once when joining // a channel to keep it in the thread when new messages arrive if (_.isUndefined(this.messages_separator_position)) { - var msg_id = this.last_seen_message_id; if (!this.unread_counter) { this.messages_separator_position = false; // no unread message -> don't display separator - } else if ((msg_id === false) || !_.findWhere(messages, {id: msg_id})) { - this.messages_separator_position = 'top'; // all displayed messages are unread } else { - this.messages_separator_position = msg_id; // last read message is msg_id + var msg = chat_manager.get_last_seen_message(this.channel); + this.messages_separator_position = msg ? msg.id : 'top'; } } return { @@ -508,7 +497,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { cp_content: { $buttons: this.$buttons, $searchview: this.searchview.$el, - $searchview_buttons: this.searchview.$buttons.contents(), + $searchview_buttons: this.$searchview_buttons, }, searchview: this.searchview, }); @@ -533,9 +522,13 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }, on_post_message: function (message) { + var self = this; var options = {channel_id: this.channel.id}; chat_manager .post_message(message, options) + .then(function() { + self.thread.scroll_to(); + }) .fail(function () { // todo: display notification }); @@ -543,11 +536,15 @@ var ChatAction = Widget.extend(ControlPanelMixin, { on_new_message: function (message) { var self = this; if (_.contains(message.channel_ids, this.channel.id)) { - if (this.channel.type !== 'static') { + if (this.channel.type !== 'static' && this.thread.is_at_bottom()) { chat_manager.mark_channel_as_seen(this.channel); } + + var should_scroll = this.thread.is_at_bottom(); this.fetch_and_render_thread().then(function () { - self.thread.scroll_to({id: message.id}); + if (should_scroll) { + self.thread.scroll_to({id: message.id}); + } }); } // Re-render sidebar to indicate that there is a new message in the corresponding channels @@ -583,7 +580,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }, on_click_button_invite: function () { - var title = _.str.sprintf(_t('Invite people to %s'), this.channel.name); + var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); new PartnerInviteDialog(this, title, this.channel.id).open(); }, on_click_button_detach: function () { @@ -602,6 +599,16 @@ var ChatAction = Widget.extend(ControlPanelMixin, { target: 'current' }); }, + on_click_new_message: function () { + this.do_action({ + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + }); + }, }); diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index a02a3966333..5d6de2bad50 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -315,7 +315,7 @@ var BasicComposer = Widget.extend({ template: "mail.ChatComposer", events: { - "keydown .o_composer_input": "on_keydown", + "keydown .o_composer_input textarea": "on_keydown", "keyup .o_composer_input": "on_keyup", "change input.o_form_input_file": "on_attachment_change", "click .o_composer_button_send": "send_message", @@ -331,6 +331,7 @@ var BasicComposer = Widget.extend({ input_max_height: 150, input_min_height: 28, mention_fetch_limit: 8, + mention_partners_restricted: false, // set to true to only suggest prefetched partners send_text: _('Send'), }); this.context = this.options.context; @@ -354,6 +355,10 @@ var BasicComposer = Widget.extend({ model: 'mail.channel', redirect_classname: 'o_channel_redirect', }); + + // Emojis + this.emoji_container_classname = 'o_composer_emoji'; + this.PartnerModel = new Model('res.partner'); this.ChannelModel = new Model('mail.channel'); }, @@ -363,7 +368,7 @@ var BasicComposer = Widget.extend({ this.$attachment_button = this.$(".o_composer_button_add_attachment"); this.$attachments_list = this.$('.o_composer_attachments_list'); - this.$input = this.$('.o_composer_input'); + this.$input = this.$('.o_composer_input textarea'); this.$input.focus(function () { self.trigger('input_focused'); }); @@ -386,7 +391,7 @@ var BasicComposer = Widget.extend({ return self.$emojis; }, html: true, - container: '.o_composer_emoji', + container: '.' + self.emoji_container_classname, trigger: 'focus', }); @@ -443,8 +448,8 @@ var BasicComposer = Widget.extend({ /** * Send the message on ENTER, but go to new line on SHIFT+ENTER */ - prevent_send: function (event) { - return event.shiftKey; + should_send: function (event) { + return !event.shiftKey; }, on_keydown: function (event) { @@ -465,9 +470,12 @@ var BasicComposer = Widget.extend({ case $.ui.keyCode.ENTER: if (this.mention_manager.is_open()) { event.preventDefault(); - } else if (!this.prevent_send(event)) { - event.preventDefault(); - this.send_message(); + } else { + var send_message = event.ctrlKey || this.should_send(event); + if (send_message) { + event.preventDefault(); + this.send_message(); + } } break; } @@ -613,7 +621,7 @@ var BasicComposer = Widget.extend({ } } }); - if (!suggestions.length) { + if (!suggestions.length && !self.options.mention_partners_restricted) { // no result found among prefetched partners, fetch other suggestions var kwargs = { limit: limit, @@ -648,8 +656,9 @@ var ExtendedComposer = BasicComposer.extend({ options = _.defaults(options || {}, { input_min_height: 120, }); + this._super(parent, options); this.extended = true; - return this._super(parent, options); + this.emoji_container_classname = 'o_extended_composer_emoji'; }, start: function () { @@ -667,8 +676,8 @@ var ExtendedComposer = BasicComposer.extend({ }); }, - prevent_send: function () { - return true; + should_send: function () { + return false; }, }); diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index 19e225c364f..21d22a12eac 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -3,71 +3,128 @@ odoo.define('mail.systray', function (require) { var core = require('web.core'); var SystrayMenu = require('web.SystrayMenu'); -var web_client = require('web.web_client'); 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_new_message": "on_click_new_message", + "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()); + chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter); + this.update_counter(); return this._super(); }, - update_counter: function (counter) { - this.$('.o_notification_counter').html(counter); + is_open: function () { + return this.$el.hasClass('open'); + }, + update_counter: function () { + var counter = chat_manager.get_needaction_counter() + chat_manager.get_chat_unread_counter(); + this.$('.o_notification_counter').text(counter); + this.$el.toggleClass('o_no_notification', !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: function (event) { + 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_new_message: function () { + chat_manager.bus.trigger('open_chat'); + }, + 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); - - - -/** - * * Global ComposeMessage Top Button - * * - * * Add a link on the top user bar to write a full mail. It opens the form view - * * of the mail.compose.message (in a modal). - * */ -var ComposeMessageTopButton = Widget.extend({ - template:'mail.ComposeMessageTopButton', - events: { - "click": "on_compose_message", - }, - on_compose_message: function (ev) { - ev.preventDefault(); - web_client.action_manager.do_action({ - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - }); - }, -}); - -// Put the ComposeMessageTopButton widget in the systray menu -SystrayMenu.Items.push(ComposeMessageTopButton); +SystrayMenu.Items.push(MessagingMenu); }); diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 99b4d6ba1c2..439b1a8beb3 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -165,6 +165,9 @@ var Thread = Widget.extend({ get_scrolltop: function () { return this.$el.scrollTop(); }, + is_at_bottom: function () { + return this.el.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight() < 5; + }, }); Thread.ORDER = ORDER; diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js index f217d929c20..65488129884 100644 --- a/addons/mail/static/src/js/window_manager.js +++ b/addons/mail/static/src/js/window_manager.js @@ -2,29 +2,49 @@ odoo.define('mail.window_manager', function (require) { "use strict"; var chat_manager = require('mail.chat_manager'); -var ChatWindow = require('mail.ChatWindow'); +var ExtendedChatWindow = require('mail.ExtendedChatWindow'); +var config = require('web.config'); var core = require('web.core'); +var utils = require('web.utils'); var web_client = require('web.web_client'); +var _t = core._t; +var QWeb = core.qweb; // chat window management //---------------------------------------------------------------- +var CHAT_WINDOW_WIDTH = 300 + 5; // 5 pixels between windows var chat_sessions = []; -var CHAT_WINDOW_WIDTH = 300; +var new_chat_session; +var display_state = { + hidden_sessions: [], + hidden_unread_counter: 0, // total number of unread msgs in hidden chat windows + nb_slots: 0, + space_left: 0, + windows_dropdown_is_open: false, // used to keep dropdown open when closing chat windows +}; + +function add_chat_session (chat_session) { + // adds chat_session such that it will be the left-most visible window + compute_available_slots(chat_sessions.length+1); + chat_sessions.splice(display_state.nb_slots-1, 0, chat_session); +} function open_chat (session) { - if (!_.findWhere(chat_sessions, {id: session.id})) { - var channel = chat_manager.get_channel(session.id); - var chat_session = { + if (!session) { + open_chat_without_session(); + return; + } + var chat_session = _.findWhere(chat_sessions, {id: session.id}); + if (!chat_session) { + var prefix = !session.is_chat ? "#" : ""; + 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 ExtendedChatWindow(web_client, session.id, prefix + 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); @@ -33,16 +53,20 @@ function open_chat (session) { chat_manager.toggle_star_status(message_id); }); - chat_session.window.on("fold_channel", null, function (channel_id) { - chat_manager.fold_channel(channel_id); + chat_session.window.on("fold_channel", null, function (channel_id, folded) { + chat_manager.fold_channel(channel_id, folded); }); chat_session.window.on("post_message", null, function (message, channel_id) { message.content = _.escape(message.content); - chat_manager.post_message(message, {channel_id: channel_id}); + chat_manager + .post_message(message, {channel_id: channel_id}) + .then(function () { + chat_session.window.thread.scroll_to(); + }); }); 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); @@ -58,50 +82,198 @@ function open_chat (session) { } }); - chat_sessions.push(chat_session); + var remove_new_chat = false; + if (new_chat_session && new_chat_session.partner_id && new_chat_session.partner_id === session.direct_partner_id) { + chat_sessions[_.indexOf(chat_sessions, new_chat_session)] = chat_session; + remove_new_chat = true; + } else { + add_chat_session(chat_session); + } + chat_session.window.appendTo($('body')) - .then(reposition_windows) .then(function () { + reposition_windows({remove_new_chat: remove_new_chat}); return chat_manager.get_messages({channel_id: chat_session.id}); }).then(function (messages) { chat_session.window.render(messages); - chat_session.window.scrollBottom(); + chat_session.window.thread.scroll_to(); + setTimeout(function () { + chat_session.window.thread.$el.on("scroll", null, _.debounce(function () { + if (chat_session.window.thread.is_at_bottom()) { + chat_manager.mark_channel_as_seen(session); + } + }, 100)); + }, 0); // setTimeout to prevent to execute handler on first scroll_to, which is asynchronous + 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 (session.is_folded !== chat_session.window.folded) { + chat_session.window.toggle_fold(session.is_folded); + } } } -function close_chat (chat_session) { - var session = _.find(chat_sessions, {id: chat_session.id}); - if (session) { - chat_sessions = _.without(chat_sessions, session); - session.window.destroy(); - reposition_windows(); +function open_chat_without_session () { + if (!new_chat_session) { + new_chat_session = { + id: '_open', + window: new ExtendedChatWindow(web_client, undefined, _t('New message'), false, 0, {thread_less: true}), + }; + new_chat_session.window.on("close_chat_session", null, close_new_chat); + new_chat_session.window.on('open_dm_session', null, function (partner_id) { + new_chat_session.partner_id = partner_id; + var dm = chat_manager.get_dm_from_partner_id(partner_id); + if (!dm) { + chat_manager.open_and_detach_dm(partner_id); + } else { + var dm_session = _.findWhere(chat_sessions, {id: dm.id}); + if (!dm_session) { + chat_manager.detach_channel(dm); + } else { + close_chat(dm_session); + dm.is_folded = false; + open_chat(dm); + } + } + }); + add_chat_session(new_chat_session); + new_chat_session.window.appendTo($('body')).then(reposition_windows); + } else { + if (new_chat_session.window.is_hidden) { + make_session_visible(new_chat_session); + } else if (new_chat_session.window.folded) { + new_chat_session.window.toggle_fold(false); + } } } +function close_chat (chat_session) { + chat_sessions = _.without(chat_sessions, chat_session); + chat_session.window.destroy(); + reposition_windows(); +} + +function close_new_chat () { + chat_sessions = _.without(chat_sessions, new_chat_session); + reposition_windows({remove_new_chat: true}); +} + +function destroy_new_chat () { + new_chat_session.window.destroy(); + new_chat_session = undefined; +} + function toggle_fold_chat (channel) { var session = _.find(chat_sessions, {id: channel.id}); if (session) { - session.window.toggle_fold(); + session.window.toggle_fold(channel.is_folded); } } -function reposition_windows () { +function compute_available_slots (nb_windows) { + if (config.device.size_class === config.device.SIZES.XS) { + display_state.nb_slots = 1; // one chat window full screen in mobile + return; + } + var width = window.innerWidth; + var nb_slots = Math.floor(width/CHAT_WINDOW_WIDTH); + var space_left = width - (Math.min(nb_slots, nb_windows)*CHAT_WINDOW_WIDTH); + if (nb_slots < nb_windows && space_left < 50) { + nb_slots--; // leave at least 50px for the hidden windows dropdown button + space_left += CHAT_WINDOW_WIDTH; + } + display_state.nb_slots = nb_slots; + display_state.space_left = space_left; +} + +var reposition_windows = function (options) { + if (options && options.remove_new_chat) { + destroy_new_chat(); + } + compute_available_slots(chat_sessions.length); + var hidden_sessions = []; + var hidden_unread_counter = 0; + var nb_slots = display_state.nb_slots; _.each(chat_sessions, function (session, index) { - session.window.$el.css({right: (CHAT_WINDOW_WIDTH + 5) * index, bottom: 0}); + if (index < nb_slots) { + session.window.$el.css({right: CHAT_WINDOW_WIDTH*index, bottom: 0}); + session.window.do_show(); + } else { + hidden_sessions.push(session); + hidden_unread_counter += session.window.unread_msgs; + session.window.do_hide(); + } }); + display_state.hidden_sessions = hidden_sessions; + display_state.hidden_unread_counter = hidden_unread_counter; + + if (display_state.$hidden_windows_dropdown) { + display_state.$hidden_windows_dropdown.remove(); + } + if (hidden_sessions.length) { + display_state.$hidden_windows_dropdown = render_hidden_sessions_dropdown(); + var $hidden_windows_dropdown = display_state.$hidden_windows_dropdown; + $hidden_windows_dropdown.css({right: CHAT_WINDOW_WIDTH * nb_slots, bottom: 0}) + .appendTo($('body')); + reposition_hidden_sessions_dropdown(); + display_state.windows_dropdown_is_open = false; + + $hidden_windows_dropdown.on('click', '.o_chat_header', function (event) { + var session_id = $(event.currentTarget).data('session-id'); + var session = _.findWhere(hidden_sessions, {id: session_id}); + if (session) { + make_session_visible(session); + } + }); + $hidden_windows_dropdown.on('click', '.o_chat_window_close', function (event) { + var session_id = $(event.currentTarget).closest('.o_chat_header').data('session-id'); + var session = _.findWhere(hidden_sessions, {id: session_id}); + if (session) { + session.window.on_click_close(event); + display_state.windows_dropdown_is_open = true; // keep the dropdown open + } + }); + } +}; + +function make_session_visible (session) { + utils.swap(chat_sessions, session, chat_sessions[display_state.nb_slots-1]); + reposition_windows(); + session.window.toggle_fold(false); +} + +function render_hidden_sessions_dropdown () { + var $dropdown = $(QWeb.render("mail.ChatWindowsDropdown", { + sessions: display_state.hidden_sessions, + open: display_state.windows_dropdown_is_open, + unread_counter: display_state.hidden_unread_counter, + })); + return $dropdown; +} + +function reposition_hidden_sessions_dropdown () { + // Unfold dropdown to the left if there is enough place + var $dropdown_ul = display_state.$hidden_windows_dropdown.children('ul'); + if (display_state.space_left > $dropdown_ul.width() + 10) { + $dropdown_ul.addClass('dropdown-menu-right'); + } } function update_sessions (message, scrollBottom) { _.each(chat_sessions, function (session) { if (_.contains(message.channel_ids, session.id)) { - if (!session.window.folded) { + var message_visible = !session.window.folded && !session.window.is_hidden && session.window.thread.is_at_bottom(); + if (message_visible) { chat_manager.mark_channel_as_seen(chat_manager.get_channel(session.id)); } chat_manager.get_messages({channel_id: session.id}).then(function (messages) { session.window.render(messages); - if (scrollBottom) { - session.window.scrollBottom(); + if (scrollBottom && message_visible) { + session.window.thread.scroll_to(); } }); } @@ -111,7 +283,12 @@ function update_sessions (message, scrollBottom) { core.bus.on('web_client_ready', null, function () { chat_manager.bus.on('open_chat', null, open_chat); - chat_manager.bus.on('close_chat', null, close_chat); + chat_manager.bus.on('close_chat', null, function (channel) { + var session = _.find(chat_sessions, {id: channel.id}); + if (session) { + close_chat(session); + } + }); chat_manager.bus.on('channel_toggle_fold', null, toggle_fold_chat); chat_manager.bus.on('new_message', null, function (message) { @@ -124,7 +301,7 @@ core.bus.on('web_client_ready', null, function () { chat_manager.bus.on('anyone_listening', null, function (channel, query) { _.each(chat_sessions, function (session) { - if (channel.id === session.id) { + if (channel.id === session.id && session.window.thread.is_at_bottom()) { query.is_displayed = true; } }); @@ -139,11 +316,19 @@ core.bus.on('web_client_ready', null, function () { }); chat_manager.bus.on('update_channel_unread_counter', null, function (channel) { + display_state.hidden_unread_counter = 0; _.each(chat_sessions, function (session) { if (channel.id === session.id) { session.window.update_unread(channel.unread_counter); } + if (session.window.is_hidden) { + display_state.hidden_unread_counter += session.window.unread_msgs; + } }); + if (display_state.$hidden_windows_dropdown) { + display_state.$hidden_windows_dropdown.html(render_hidden_sessions_dropdown().html()); + reposition_hidden_sessions_dropdown(); + } }); chat_manager.is_ready.then(function() { @@ -153,6 +338,50 @@ 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.window.is_hidden) { + make_session_visible(chat_session); + } else { + chat_session.window.focus_input(); + } + }); + + core.bus.on('resize', null, _.debounce(reposition_windows, 100)); +}); + +}); + +// FIXME: move this to its own file in master +odoo.define('mail.ExtendedChatWindow', function (require) { +"use strict"; + +var chat_manager = require('mail.chat_manager'); +var ChatWindow = require('mail.ChatWindow'); + +return ChatWindow.extend({ + template: "mail.ExtendedChatWindow", + start: function () { + var self = this; + return this._super().then(function () { + if (self.options.thread_less) { + self.$el.addClass('o_thread_less'); + self.$('.o_chat_search_input input') + .autocomplete({ + source: function(request, response) { + chat_manager.search_partner(request.term, 10).done(response); + }, + select: function(event, ui) { + self.trigger('open_dm_session', ui.item.id); + }, + }) + .focus(); + } + }); + }, }); }); diff --git a/addons/mail/static/src/less/chat_window.less b/addons/mail/static/src/less/chat_window.less index ca2e8ca74f1..a4024c93dc1 100644 --- a/addons/mail/static/src/less/chat_window.less +++ b/addons/mail/static/src/less/chat_window.less @@ -1,10 +1,11 @@ @o-chat-window-bg: white; +@o-chat-window-width: 300px; .o_chat_window { .o-flex-display(); .o-flex-flow(column, nowrap); position: fixed; - width: 300px; + width: @o-chat-window-width; max-width: 100%; height: 400px; max-height: 100%; @@ -15,7 +16,11 @@ z-index: 2; @media (max-width: @screen-xs-max) { - display:none; + width: 100%; + height: 100%!important; + &.o_folded { + display: none; + } } .o_chat_header { @@ -25,26 +30,35 @@ background-color: @odoo-brand-primary; padding: 5px; + @media (max-width: @screen-xs-max) { + height: @navbar-height; + .o_chat_title, .o_chat_window_close { + font-size: 22px; + } + } + .o_chat_title { cursor: pointer; .o-flex(1, 1, auto); + .o-text-overflow(); } .o_chat_window_close { color: white; padding: 3px; + margin-left: 5px; &:hover { background-color: darken(@odoo-brand-primary, 10%); } } } - .o_chat_content { + .o_mail_thread { .o-flex(1, 1, auto); overflow: auto; padding: 5px; - .o_mail_thread .o_thread_date_separator { + .o_thread_date_separator { margin-top: 0px; margin-bottom: 15px; border-color: lightgray; @@ -67,3 +81,67 @@ } } } + +.o_chat_window_dropdown { + width: auto; + height: 28px; + color: white; + background-color: @odoo-brand-primary; + cursor: pointer; + + @media (max-width: @screen-xs-max) { + display: none; + } + + .o_chat_window_dropdown_toggler { + padding: 5px; + + .o_total_unread_counter { + .o-position-absolute(-10px, 0, auto, auto); + background-color: @odoo-brand-optional; + padding: 0 2px; + font-size: smaller; + } + } + &.open .o_chat_window_dropdown_toggler .o_total_unread_counter { + display: none; + } + + > ul { + max-width: @o-chat-window-width; + padding: 0; + + > li.o_chat_header { + font-size: 12px; + padding: 3px 5px; + &~li.o_chat_header { + border-top: 1px solid white; + } + &:hover { + background-color: darken(@odoo-brand-primary, 10%); + color: white; + } + } + } +} + +// FIXME: move this to its own file in master +.o_chat_window.o_thread_less { + .o_chat_search_input { + .o-flex-display(); + padding: 5px; + > span { + .o-flex(0, 0, auto); + } + > input { + margin-left: 5px; + .o-flex(1, 1, 100%); + } + } + .o_mail_thread { + background-color: lightgray; + } + .o_chat_input { + display: none; + } +} diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less index 6ad0c80a785..aa469649f2a 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -131,9 +131,11 @@ .o_composer_input { .o-flex(1, 1, auto); - padding: 5px; margin-right: 5px; - border: none; + > textarea { + padding: 5px; + border: none; + } } &.o_extended_composer .o_composer_input { margin-right: 0; @@ -265,3 +267,137 @@ color: orange; } } + +// Navbar icon and dropdown (FIXME: move to systray.less) +.o_mail_navbar_item { + > a { + opacity: 1; + > i { + font-size: larger; + } + } + &.o_no_notification > a { + opacity: 0.5; + .o_notification_counter { + display: none; + } + } + + &.open .o_mail_navbar_dropdown { + .o-flex-display(); + .o-flex-flow(column, nowrap); + } + .o_mail_navbar_dropdown { + width: 350px; + padding: 0; + @media (max-width: @screen-xs-max) { + background-color: white!important; + max-height: none!important; + position: fixed!important; + top: @navbar-height; + right: 0; + bottom: 0; + left: 0; + } + + .o_spinner { + .o-flex-display(); + .o-align-items(center); + .o-justify-content(center); + height: 50px; + } + + .o_filter_button, + .o_new_message, + .o_redirect_to_discuss { + .btn-link; + padding: 5px; + } + + .o_mail_navbar_dropdown_top { + .o-flex-display(); + .o-flex(0, 0, auto); + justify-content: space-between; + 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; + + @media (max-width: @screen-xs-max) { + max-height: none; + } + + .o_mail_channel_preview { + .o-flex-display(); + height: 50px; + align-items: center; + padding: 5px; + cursor: pointer; + font-size: 12px; + overflow: hidden; + &~.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/less/composer.less b/addons/mail/static/src/less/composer.less index 6dad1dac3d2..88b44fec311 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -10,7 +10,7 @@ } .o_composer { - .o_composer_input { + .o_composer_input > textarea { resize: none; } } diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index f2971dcf17e..be86e577b08 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -97,6 +97,9 @@ .o_thread_message_core { .o-flex(1, 1, auto); + min-width: 0; + max-width: 100%; + word-wrap: break-word; &.o_mail_note { background-color: @gray-lighter; @@ -111,12 +114,20 @@ color: gray; } - > p { + [summary~=o_mail_notification] { // name conflicts with channel notifications, but is odoo notification buttons to hide in chatter if present + display: none; + } + + p { margin: 0 0 9px; // Required by the old design to override a general rule on p's &:last-child { margin-bottom: 0; } } + a { + display: inline-block; + word-break: break-all; + } .o_mail_body_long { display: none; diff --git a/addons/mail/static/src/xml/chat_window.xml b/addons/mail/static/src/xml/chat_window.xml index 01e475a6eb8..eb003cd59cd 100644 --- a/addons/mail/static/src/xml/chat_window.xml +++ b/addons/mail/static/src/xml/chat_window.xml @@ -1,23 +1,63 @@ <?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> - <t t-name="mail.ChatWindow"> <div class="o_chat_window"> <div class="o_chat_header"> - <span class="o_chat_title"> - <t t-esc="widget.title"/> - </span> - <span> - <a href="#" class="o_chat_window_close fa fa-close"></a> - </span> + <t t-call="mail.ChatWindowHeaderContent"> + <t t-set="title" t-value="widget.title"/> + <t t-set="unread_counter" t-value="widget.unread_msgs"/> + </t> </div> <div class="o_chat_content"> </div> <div class="o_chat_input"> - <input t-att-placeholder="widget.placeholder"/> + <input t-att-placeholder="widget.options.placeholder"/> + </div> + </div> + </t> + + <!-- FIXME: move this to its own file in master --> + <t t-name="mail.ExtendedChatWindow" t-extend="mail.ChatWindow"> + <t t-jquery=".o_chat_header" t-operation="after"> + <div t-if="widget.options.thread_less" class="o_chat_search_input"> + <span> To: </span> + <input type="text"/> </div> + </t> + </t> + + <t t-name="mail.ChatWindowsDropdown"> + <div t-attf-class="o_chat_window o_chat_window_dropdown dropup #{open ? 'open' : ''}"> + <span class="o_chat_window_dropdown_toggler dropdown-toggle" data-toggle="dropdown"> + <i class="fa fa-comments-o"/> <t t-esc="sessions.length"/> + <span class="o_total_unread_counter"> + <t t-if="unread_counter"><t t-esc="unread_counter"/></t> + </span> + </span> + <ul class="dropdown-menu"> + <t t-foreach="sessions" t-as="session"> + <li class="o_chat_header" t-att-data-session-id="session.id"> + <t t-call="mail.ChatWindowHeaderContent"> + <t t-set="title" t-value="session.window.title"/> + <t t-set="unread_counter" t-value="session.window.unread_msgs"/> + </t> + </li> + </t> + </ul> </div> </t> + <t t-name="mail.ChatWindowHeaderContent"> + <span class="o_chat_title"> + <t t-esc="title"/> + <span class="o_unread_counter"> + <t t-if="unread_counter">(<t t-esc="unread_counter"/>)</t> + </span> + </span> + <span> + <a href="#" class="o_chat_window_close fa fa-close"/> + </span> + </t> + </templates> diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml index c8db64a7d56..e76e56a7810 100644 --- a/addons/mail/static/src/xml/client_action.xml +++ b/addons/mail/static/src/xml/client_action.xml @@ -75,13 +75,13 @@ <t t-if="!disable_add_channel || !empty"> <div class="o_mail_sidebar_title"> <h4 t-att-class="channel_type == 'public' ? 'o_mail_open_channels' : ''"><t t-esc="channel_title"/></h4> - <span t-if="!disable_add_channel" class="fa fa-plus o_add" t-att-title="_t('Add')" t-attf-data-type="#{channel_type}"/> + <span t-if="!disable_add_channel" class="fa fa-plus o_add" title="Add" t-attf-data-type="#{channel_type}"/> </div> </t> </t> <t t-name="mail.chat.SidebarItems"> <t t-foreach="channels" t-as="channel"> - <t t-set="counter" t-value="channel.needaction_counter"/> + <t t-set="counter" t-value="channel.is_chat ? channel.unread_counter : channel.needaction_counter"/> <div t-if="channel.type === channel_type" t-att-data-channel-id="channel.id" t-attf-class="o_mail_chat_channel_item #{channel.unread_counter ? ' o_unread_message' : ''} #{(active_channel_id == channel.id) ? 'o_active': ''}"> <span t-if="display_status"> @@ -94,7 +94,7 @@ <i t-if="channel.mass_mailing" class="fa fa-envelope-o" title="Sends messages by email"/> <t t-call="mail.chat.SidebarNeedaction"/> <span t-attf-class="fa fa-times pull-right o_mail_partner_unpin #{counter ? 'hide' : ''}" - t-att-title="_t('Leave this channel')" t-att-data-channel-id="channel.id"/> + title="Leave this channel" t-att-data-channel-id="channel.id"/> </div> </t> <div t-if="!disable_add_channel" class="o_mail_add_channel" t-attf-data-type="#{channel_type}"> @@ -119,13 +119,14 @@ <!-- Buttons of the Control Panel --> <t t-name="mail.chat.ControlButtons"> <div> - <button type="button" class="btn btn-primary btn-sm o_mail_toggle_channels visible-xs-inline" t-att-title='_t("Toggle Channels")'>Channels</button> - <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button> - <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_detach hidden-xs" t-att-title='_t("Detach the conversation")'>Detach</button> - <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_mark_read" t-att-title='_t("Mark all as read")'>Mark all read</button> - <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_unstar_all" t-att-title='_t("Unstar all messages")'>Unstar all</button> - <button type="button" class="btn btn-default btn-sm o_mail_chat_button_unsubscribe" t-att-title='_t("Unsubscribe from channel")'>Unsubscribe</button> - <button type="button" class="btn btn-default btn-sm o_mail_chat_button_settings" t-att-title='_t("Open channel settings")'>Settings</button> + <button type="button" class="btn btn-primary btn-sm o_mail_toggle_channels visible-xs-inline" title="Toggle Channels">Channels</button> + <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" title="Invite people">Invite</button> + <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_detach hidden-xs" title="Detach the conversation">Detach</button> + <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_mark_read" title="Mark all as read">Mark all read</button> + <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_unstar_all" title="Unstar all messages">Unstar all</button> + <button type="button" class="btn btn-default btn-sm o_mail_chat_button_new_message" title="Compose a new message">Send mail</button> + <button type="button" class="btn btn-default btn-sm o_mail_chat_button_unsubscribe" title="Unsubscribe from channel">Unsubscribe</button> + <button type="button" class="btn btn-default btn-sm o_mail_chat_button_settings" title="Open channel settings">Settings</button> </div> </t> diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 9af882c4062..25f9ba6bfda 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -14,7 +14,9 @@ </div> </t> <div t-attf-class="o_composer #{widget.extended ? 'o_extended_composer' : ''}"> - <textarea class="o_composer_input" placeholder="Write something..."/> + <div class="o_composer_input"> + <textarea placeholder="Write something..."/> + </div> <t t-if="!widget.extended"> <div class="btn-group o_composer_buttons"> <button class="btn btn-sm btn-primary o_composer_button_send" type="button"><t t-esc="widget.options.send_text"/></button> @@ -33,7 +35,7 @@ <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> </t> </span> - <span class="o_composer_emoji"></span> + <span t-att-class="widget.emoji_container_classname"></span> </div> <t t-name="mail.ChatComposer.emojis"> diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index 58a735e80c5..247096b3b6e 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -1,19 +1,56 @@ <?xml version="1.0" encoding="UTF-8"?> <templates> - <t t-name="mail.ComposeMessageTopButton"> - <li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email"> - <a href="#"> - <i class="fa fa-envelope-o fa-fw"></i> + + <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"> + <div> + <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> + </div> + <button type="button" class="btn btn-sm o_new_message"> New message </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.NotificationTopButton"> - <li t-att-title='_t("Notifications")'> - <a href="#"> - <i class="fa fa-comment"/> <span class="o_notification_counter"/> - </a> - </li> + <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> diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 0952fcf0e7f..970c8ff6a6f 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -65,7 +65,7 @@ </span> <i t-if="!message.display_author && options.display_stars && message.message_type != 'notification'" t-att-class="'fa o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')" - t-att-data-message-id="message.id" t-att-title="_t('Mark as Todo')"/> + t-att-data-message-id="message.id" title="Mark as Todo"/> </div> <div t-att-class="'o_thread_message_core' + (message.is_note ? ' o_mail_note' : '')"> <p t-if="message.display_author" class="o_mail_info"> @@ -99,10 +99,10 @@ <span> <i t-if="options.display_stars && message.message_type != 'notification'" t-att-class="'fa fa-lg o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')" - t-att-data-message-id="message.id" t-att-title="_t('Mark as Todo')"/> + t-att-data-message-id="message.id" title="Mark as Todo"/> <i t-if="message.is_needaction && options.display_needactions" class="fa fa-check o_thread_message_needaction" - t-att-data-message-id="message.id" t-att-title="_t('Mark as Read')"/> + t-att-data-message-id="message.id" title="Mark as Read"/> </span> </p> <t t-if="message.tracking_value_ids && message.tracking_value_ids.length > 0"> @@ -122,7 +122,7 @@ </div> </t> <t t-if="!options.shorten_messages || !message.body_short"> - <t t-raw="message.body"/> + <div><t t-raw="message.body"/></div> </t> </t> <t t-foreach="message.attachment_ids" t-as="attachment"> |
