From 7a91d87614f9a72df1d81ee28a5f425517e9b218 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 15 Sep 2017 11:28:13 +0200 Subject: [PATCH] [IMP] im_livechat, mail: Discuss UI in mobile - Extract mobile specific code for the client action to a specific file - Massive clean of less rules - Unify channels preview of the systray dropdown and the mobile UI of Discuss - Ensure to only load the last message of each channel in mobile, as this is not used in desktop (unless the user opens the systray menu) - Make livechat available in Discuss mobile UI --- .../static/src/xml/im_livechat_backend.xml | 11 + addons/mail/models/mail_channel.py | 10 +- addons/mail/static/src/js/chat_manager.js | 43 +- addons/mail/static/src/js/chat_window.js | 9 +- .../static/src/js/client_action_mobile.js | 283 ++++++++++ addons/mail/static/src/js/composer.js | 2 +- addons/mail/static/src/js/systray.js | 42 +- addons/mail/static/src/js/window_manager.js | 1 + addons/mail/static/src/less/chat_window.less | 108 +--- .../mail/static/src/less/client_action.less | 497 ++++++++---------- addons/mail/static/src/less/composer.less | 5 + .../static/src/less/extended_chat_window.less | 34 +- addons/mail/static/src/less/systray.less | 201 ++----- addons/mail/static/src/less/thread.less | 87 +-- addons/mail/static/src/xml/chat_window.xml | 4 +- addons/mail/static/src/xml/client_action.xml | 166 +++--- addons/mail/static/src/xml/composer.xml | 4 +- .../static/src/xml/extended_chat_window.xml | 5 +- addons/mail/static/src/xml/systray.xml | 39 +- addons/mail/static/src/xml/thread.xml | 2 +- .../mail/static/tests/client_action_tests.js | 89 ++-- addons/mail/views/mail_templates.xml | 1 + 22 files changed, 795 insertions(+), 848 deletions(-) create mode 100644 addons/mail/static/src/js/client_action_mobile.js diff --git a/addons/im_livechat/static/src/xml/im_livechat_backend.xml b/addons/im_livechat/static/src/xml/im_livechat_backend.xml index e686a55c08e..3240bc63447 100644 --- a/addons/im_livechat/static/src/xml/im_livechat_backend.xml +++ b/addons/im_livechat/static/src/xml/im_livechat_backend.xml @@ -7,9 +7,20 @@ Livechat + fa-comments + + + +
+ + Livechat +
+
+
+ diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index e9e9969a017..bcf42cbb934 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -355,9 +355,11 @@ class Channel(models.Model): .filtered(lambda p: p.id != self.env.user.partner_id.id) .read(['id', 'name', 'im_status'])) - last_message = channel.channel_fetch_preview() - if last_message: - info['last_message'] = last_message[0].get('last_message') + # add last message preview (only used in mobile) + if self._context.get('isMobile', False): + last_message = channel.channel_fetch_preview() + if last_message: + info['last_message'] = last_message[0].get('last_message') # add user session state, if available and if user is logged if partner_channels.ids: @@ -577,9 +579,9 @@ class Channel(models.Model): 'email_send': False, 'channel_partner_ids': [(4, self.env.user.partner_id.id)] }) - channel_info = new_channel.channel_info('creation')[0] notification = _('
created #%s
') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") + channel_info = new_channel.channel_info('creation')[0] self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 128309bc77d..a868137d95b 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -82,8 +82,11 @@ function add_message (data, options) { _.each(msg.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); if (channel) { - // For mobile tabs view, we show recent message for channel. - channel.last_message = msg; + // update the channel's last message (displayed in the channel + // preview, in mobile) + if (!channel.last_message || msg.id > channel.last_message.id) { + channel.last_message = msg; + } add_to_cache(msg, []); if (options.domain && options.domain !== []) { add_to_cache(msg, options.domain); @@ -97,7 +100,7 @@ function add_message (data, options) { update_channel_unread_counter(channel, channel.unread_counter+1); } if (channel.is_chat && options.show_notification) { - if (!client_action_open && config.device.size_class !== config.device.SIZES.XS) { + if (!client_action_open && !config.isMobile) { // automatically open chat window chat_manager.bus.trigger('open_chat', channel, { passively: true }); } @@ -260,6 +263,9 @@ function add_channel (data, options) { } else { channel = chat_manager.make_channel(data, options); channels.push(channel); + if (data.last_message) { + channel.last_message = add_message(data.last_message); + } // In case of a static channel (Inbox, Starred), the name is translated thanks to _lt // (lazy translate). In this case, channel.name is an object, not a string. channels = _.sortBy(channels, function (channel) { return _.isString(channel.name) ? channel.name.toLowerCase() : '' }); @@ -290,7 +296,6 @@ function make_channel (data, options) { group_based_subscription: data.group_based_subscription, needaction_counter: data.message_needaction_counter || 0, unread_counter: 0, - last_message: data.last_message, last_seen_message_id: data.seen_message_id, cache: {'[]': { all_history_loaded: false, @@ -641,7 +646,8 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { start: function () { this.is_ready = session.is_bound.then(function(){ - return session.rpc('/mail/client_action'); + var context = _.extend({isMobile: config.isMobile}, session.user_context); + return session.rpc('/mail/client_action', {context: context}); }).then(this._onMailClientAction.bind(this)); add_channel({ @@ -1026,11 +1032,12 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { create_channel: function (name, type) { var method = type === "dm" ? "channel_get" : "channel_create"; var args = type === "dm" ? [[name]] : [name, type]; - + var context = _.extend({isMobile: config.isMobile}, session.user_context); return this._rpc({ model: 'mail.channel', method: method, args: args, + kwargs: {context: context}, }) .then(add_channel); }, @@ -1150,9 +1157,10 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { get_channels_preview: function (channels) { var channels_preview = _.map(channels, function (channel) { + var info; if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) { // map inbox(mail_message) data with existing channel/chat template - var info = _.pick(channel, 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', 'subject','date', 'record_name', 'status', 'displayed_author', 'email_from', 'unread_counter'); + info = _.pick(channel, 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', 'subject','date', 'record_name', 'status', 'displayed_author', 'email_from', 'unread_counter'); info.last_message = { body: info.body, date: info.date, @@ -1164,8 +1172,8 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { info.id = 'channel_inbox'; return info; } - var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter'); - info.last_message = _.last(channel.cache['[]'].messages); + info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter'); + info.last_message = channel.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) { @@ -1197,9 +1205,22 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { channel_preview.last_message = add_message(channel.last_message); } }); - return _.filter(channels_preview, function (channel) { - return channel.last_message; // remove empty channels + // sort channels: 1. unread, 2. chat, 3. 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 - !!c1.last_message || + (c2.last_message && 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) { + if (channel.last_message) { + channel.last_message_preview = chat_manager.get_message_body_preview(channel.last_message.body); + channel.last_message_date = channel.last_message.date.fromNow(); + } + }); + return channels_preview; }); }, get_message_body_preview: function (message_body) { diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 9d54a6df0b2..d65660ef90c 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -77,13 +77,6 @@ return Widget.extend({ this.unread_msgs = counter; this.render_header(); }, - - /** - * When user status (online/offline/away) changed, this method update - * user status with last seen and render header again. - * - * @param {string} status - */ update_status: function (status) { this.status = status; this.render_header(); @@ -92,8 +85,8 @@ return Widget.extend({ this.$header.html(QWeb.render('mail.ChatWindowHeaderContent', { status: this.status, title: this.title, - isMobile: this.isMobile, unread_counter: this.unread_msgs, + widget: this, })); }, fold: function () { diff --git a/addons/mail/static/src/js/client_action_mobile.js b/addons/mail/static/src/js/client_action_mobile.js new file mode 100644 index 00000000000..345a2bccbb2 --- /dev/null +++ b/addons/mail/static/src/js/client_action_mobile.js @@ -0,0 +1,283 @@ +odoo.define('mail.chat_client_action_mobile', function (require) { +"use strict"; + +var ChatAction = require('mail.chat_client_action'); +var chat_manager = require('mail.chat_manager'); + +var config = require('web.config'); +var core = require('web.core'); +var session = require('web.session'); + +var QWeb = core.qweb; + +if (!config.isMobile) { + return; +} + +ChatAction.include({ + template: 'mail.client_action_mobile', + need_control_panel: false, // in mobile, we use a custom control panel + events: _.extend(ChatAction.prototype.events, { + 'click .o_mail_mobile_tab': '_onMobileTabClicked', + 'click .o_channel_inbox_item': '_onMobileInboxButtonClicked', + 'click .o_mail_channel_preview': '_onMobileChannelClicked', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.currentState = this.defaultChannelID; + }, + /** + * @override + */ + start: function () { + this.$mainContent = this.$('.o_mail_chat_content'); + return this._super.apply(this, arguments) + .then(this._updateControlPanel.bind(this)); + }, + /** + * @override + */ + on_attach_callback: function () { + if (this.channel && this._isInInboxTab()) { + this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); + } + }, + /** + * @override + */ + on_detach_callback: function () { + if (this._isInInboxTab()) { + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Boolean} true iff we currently are in the Inbox tab + */ + _isInInboxTab: function () { + return _.contains(['channel_inbox', 'channel_starred'], this.currentState); + }, + /** + * @override + * @private + */ + _renderButtons: function () { + var self = this; + this._super.apply(this, arguments); + _.each(['dm', 'public', 'private'], function (type) { + var selector = '.o_mail_chat_button_' + type; + self.$buttons.on('click', selector, self._onAddChannel.bind(self)); + }); + }, + /** + * Overrides to only store the channel state if we are in the Inbox tab, as + * this is the only tab in which we actually have a displayed channel + * + * @override + * @private + */ + _restoreChannelState: function () { + if (this._isInInboxTab()) { + this._super.apply(this, arguments); + } + }, + /** + * Overrides to toggle the visibility of the tabs when a message is selected + * + * @override + * @private + */ + _selectMessage: function () { + this._super.apply(this, arguments); + this.$('.o_mail_mobile_tabs').addClass('o_hidden'); + }, + /** + * @override + * @private + */ + _setChannel: function (channel) { + if (channel.type !== 'static') { + chat_manager.detach_channel(channel); + } else { + this._super.apply(this, arguments); + } + }, + /** + * Overrides to only store the channel state if we are in the Inbox tab, as + * this is the only tab in which we actually have a displayed channel + * + * @override + * @private + */ + _storeChannelState: function () { + if (this.channel && this._isInInboxTab()) { + this._super.apply(this, arguments); + } + }, + /** + * @private + */ + _toggleSearchView: function () { + this.searchviewDisplayed = !this.searchviewDisplayed; + this.searchview.$el.toggleClass('o_hidden', !this.searchviewDisplayed); + this.$buttons.toggleClass('o_hidden', this.searchviewDisplayed); + }, + /** + * Overrides to toggle the visibility of the tabs when a message is unselected + * + * @override + * @private + */ + _unselectMessage: function () { + this._super.apply(this, arguments); + this.$('.o_mail_mobile_tabs').removeClass('o_hidden'); + }, + /** + * @override + * @private + */ + _updateChannels: function () { + return this._updateContent(this.currentState); + }, + /** + * Redraws the content of the client action according to its current state. + * + * @private + * @param {string} type the channel's type to display (e.g. 'channel_inbox', + * 'channel_starred', 'dm'...). + */ + _updateContent: function (type) { + var self = this; + var inInbox = type === 'channel_inbox' || type === 'channel_starred'; + if (!inInbox && this._isInInboxTab()) { + // we're leaving the inbox, so store the thread scrolltop + this._storeChannelState(); + } + var previouslyInInbox = this._isInInboxTab(); + this.currentState = type; + + // fetch content to display + var def; + if (inInbox) { + def = this._fetchAndRenderThread(); + } else { + var channels = _.where(chat_manager.get_channels(), {type: type}); + def = chat_manager.get_channels_preview(channels); + } + return $.when(def).then(function (channelsPreview) { + // update content + if (inInbox) { + if (!previouslyInInbox) { + self.$('.o_mail_chat_tab_pane').remove(); + self.$mainContent.append(self.thread.$el); + self.$mainContent.append(self.extended_composer.$el); + } + self._restoreChannelState(); + } else { + self.thread.$el.detach(); + self.extended_composer.$el.detach(); + var $content = $(QWeb.render("mail.chat.MobileTabPane", { + channels: channelsPreview, + get_message_body_preview: chat_manager.get_message_body_preview, + moment: moment, + partner_id: session.partner_id, + type: type, + widget: self, + })); + self._prepareAddChannelInput($content.find('.o_mail_add_channel input'), type); + self.$mainContent.html($content); + } + + // update control panel + self.$buttons.find('button').addClass('o_hidden'); + self.$buttons.find('.o_mail_chat_button_' + type).removeClass('o_hidden'); + self.$buttons.find('.o_mail_chat_button_mark_read').toggleClass('o_hidden', type !== 'channel_inbox'); + self.$buttons.find('.o_mail_chat_button_unstar_all').toggleClass('o_hidden', type !== 'channel_starred'); + self.$('.o_enable_searchview').toggleClass('o_hidden', !inInbox); + if (!inInbox && self.searchviewDisplayed) { + self._toggleSearchView(); // close the searchview when leaving Inbox + } + + // update Inbox page buttons + if (inInbox) { + self.$('.o_mail_chat_mobile_inbox_buttons').removeClass('o_hidden'); + self.$('.o_channel_inbox_item').removeClass('btn-primary').addClass('btn-default'); + self.$('.o_channel_inbox_item[data-type=' + type + ']').removeClass('btn-default').addClass('btn-primary'); + } else { + self.$('.o_mail_chat_mobile_inbox_buttons').addClass('o_hidden'); + } + + // update bottom buttons + self.$('.o_mail_mobile_tab').removeClass('active'); + // channel_inbox and channel_starred share the same tab + type = type === 'channel_starred' ? 'channel_inbox' : type; + self.$('.o_mail_mobile_tab[data-type=' + type + ']').addClass('active'); + }); + }, + /** + * @override + */ + _updateControlPanel: function () { + this.$buttons.appendTo(this.$('.o_mail_chat_mobile_control_panel')); + this.searchview.$el.appendTo(this.$('.o_mail_chat_mobile_control_panel')); + var $enable_searchview = $(' +
+ - - - + + +
@@ -145,86 +145,84 @@ - -
- -
-
- -
- - - -
-
- - Inbox + + +
+
+
+
-
- - Chat -
-
- - Channels -
-
- - Private Channels -
-
- - - - -
-
- # - Add a channel - Open chat - +
+
+
+ + Inbox +
+
+ + Chat +
+
+ + Channels +
+
+ + Private Channels
- - - - - -
- - - - -
-
- - - - - -
- - -
-

-

- - - - - - attachment(s) - - - - - : - - - - -

+ + +
+
+ # + Add a channel + Open chat + +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + + +  () + + +
+
+ + You: + + + : + + +
+
+
+
+ diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 9305728ebfe..a7b2e7cb02f 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -13,14 +13,14 @@
- +
diff --git a/addons/mail/static/src/xml/extended_chat_window.xml b/addons/mail/static/src/xml/extended_chat_window.xml index 865bb7c2968..3e183d99bd2 100644 --- a/addons/mail/static/src/xml/extended_chat_window.xml +++ b/addons/mail/static/src/xml/extended_chat_window.xml @@ -11,9 +11,8 @@ - - + + diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index ebc65ecfe2f..653faa4dc6d 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -6,7 +6,7 @@ -
    +