diff options
| author | Aaron Bohy <aab@odoo.com> | 2017-09-15 15:25:16 +0200 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2017-09-15 15:25:16 +0200 |
| commit | 13a44d33bda1d7ff21e3de5de8b3c84f6a482a0d (patch) | |
| tree | bc30e753ecc419f93a7f156d10b557ffb9e4c6c1 | |
| parent | a8e7d096b85709ccf52411c450d0574f9238fd6f (diff) | |
| parent | 7a91d87614f9a72df1d81ee28a5f425517e9b218 (diff) | |
[MERGE] im_livechat, mail: new Discuss mobile UI
- Brand new UI for Discuss client action (with tabs for for
Discuss client action (with tabs for Inbox, chats,
channels...)
- In mobile, conversations always open in Chat Windows
- Chat window UI improved (both mobile and desktop)
- Channels preview (used in the systray menu, and now in the mobile
UI of Discuss) improved
- Other minor less tweaks
Task #31446
PR #16995
23 files changed, 1629 insertions, 646 deletions
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 @@ <t t-set="disable_add_channel" t-value="true"/> <t t-call="mail.chat.SidebarTitle"> <t t-set="channel_title">Livechat</t> + <t t-set="channel_icon">fa-comments</t> </t> <t t-call="mail.chat.SidebarItems"/> </t> </t> + <!-- Mobile templates --> + <t t-extend="mail.client_action_mobile"> + <t t-jquery=".o_mail_mobile_tabs" t-operation="append"> + <div class="o_mail_mobile_tab" data-type="livechat"> + <span class="fa fa-comments"/> + <span class="o_tab_title">Livechat</span> + </div> + </t> + </t> + </template> diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 31e03b99081..bcf42cbb934 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -354,6 +354,13 @@ class Channel(models.Model): .channel_partner_ids .filtered(lambda p: p.id != self.env.user.partner_id.id) .read(['id', 'name', 'im_status'])) + + # 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: partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id) @@ -572,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 = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (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 807d5352180..a868137d95b 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -82,6 +82,11 @@ function add_message (data, options) { _.each(msg.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); if (channel) { + // 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); @@ -95,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 }); } @@ -258,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() : '' }); @@ -638,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({ @@ -1023,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); }, @@ -1147,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, @@ -1161,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) { @@ -1194,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 9f91baee764..d65660ef90c 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -11,7 +11,7 @@ var QWeb = core.qweb; var _t = core._t; var HEIGHT_OPEN = '400px'; -var HEIGHT_FOLDED = '28px'; +var HEIGHT_FOLDED = '34px'; return Widget.extend({ template: "mail.ChatWindow", @@ -43,6 +43,7 @@ return Widget.extend({ this.status = this.options.status; this.unread_msgs = unread_msgs || 0; this.is_hidden = false; + this.isMobile = config.isMobile; }, start: function () { this.$input = this.$('.o_composer_text_field'); @@ -85,12 +86,13 @@ return Widget.extend({ status: this.status, title: this.title, unread_counter: this.unread_msgs, + widget: this, })); }, fold: function () { this.$el.animate({ height: this.folded ? HEIGHT_FOLDED : HEIGHT_OPEN - }); + }, 200); }, toggle_fold: function (fold) { this.folded = _.isBoolean(fold) ? fold : !this.folded; diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index a27790f9629..279042e152c 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -2,8 +2,8 @@ odoo.define('mail.chat_client_action', function (require) { "use strict"; var chat_manager = require('mail.chat_manager'); -var composer = require('mail.composer'); var ChatThread = require('mail.ChatThread'); +var composer = require('mail.composer'); var utils = require('mail.utils'); var config = require('web.config'); @@ -12,9 +12,9 @@ var core = require('web.core'); var data = require('web.data'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); - var pyeval = require('web.pyeval'); var SearchView = require('web.SearchView'); +var session = require('web.session'); var Widget = require('web.Widget'); var QWeb = core.qweb; @@ -24,12 +24,17 @@ var _t = core._t; * Widget : Invite People to Channel Dialog * * Popup containing a 'many2many_tags' custom input to select multiple partners. - * Search user according to the input, and trigger event when selection is validated. - **/ + * Searches user according to the input, and triggers event when selection is + * validated. + */ var PartnerInviteDialog = Dialog.extend({ dialog_title: _t('Invite people'), template: "mail.PartnerInviteDialog", - init: function(parent, title, channel_id){ + + /** + * @override + */ + init: function (parent, title, channel_id) { this.channel_id = channel_id; this._super(parent, { @@ -39,17 +44,20 @@ var PartnerInviteDialog = Dialog.extend({ text: _t("Invite"), close: true, classes: "btn-primary", - click: _.bind(this.on_click_add, this), + click: this._addChannel.bind(this), }], }); }, - start: function(){ + /** + * @override + */ + start: function (){ this.$input = this.$('.o_mail_chat_partner_invite_input'); this.$input.select2({ width: '100%', allowClear: true, multiple: true, - formatResult: function(item) { + formatResult: function (item) { var status = QWeb.render('mail.chat.UserStatus', {status: item.im_status}); return $('<span>').text(item.text).prepend(status); }, @@ -65,17 +73,25 @@ var PartnerInviteDialog = Dialog.extend({ }); return this._super.apply(this, arguments); }, - on_click_add: function(){ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Deferred} + */ + _addChannel: function () { var self = this; var data = this.$input.select2('data'); - if(data.length >= 1){ + if (data.length >= 1) { return this._rpc({ model: 'mail.channel', method: 'channel_invite', args: [this.channel_id], kwargs: {partner_ids: _.pluck(data, 'id')}, - }) - .then(function(){ + }).then(function () { var names = _.escape(_.pluck(data, 'text').join(', ')); var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names); self.do_notify(_t('New people'), notification); @@ -89,80 +105,24 @@ var PartnerInviteDialog = Dialog.extend({ var ChatAction = Widget.extend(ControlPanelMixin, { template: 'mail.client_action', - custom_events: { search: '_onSearch', }, events: { - "click .o_mail_chat_channel_item": function (event) { - event.preventDefault(); - var channel_id = this.$(event.currentTarget).data('channel-id'); - this.set_channel(chat_manager.get_channel(channel_id)); - }, - "click .o_mail_sidebar_title .o_add": function (event) { - event.preventDefault(); - var type = $(event.target).data("type"); - this.$('.o_mail_add_channel[data-type=' + type + ']') - .show() - .find("input").focus(); - }, - "blur .o_mail_add_channel input": function () { - this.$('.o_mail_add_channel') - .hide(); - }, - "click .o_mail_partner_unpin": function (event) { - event.stopPropagation(); - var channel_id = $(event.target).data("channel-id"); - chat_manager.unsubscribe(chat_manager.get_channel(channel_id)); - }, - "click .o_mail_annoying_notification_bar .fa-close": function () { - this.$(".o_mail_annoying_notification_bar").slideUp(); - }, - "click .o_mail_request_permission": function (event) { - var self = this; - event.preventDefault(); - this.$(".o_mail_annoying_notification_bar").slideUp(); - var def = window.Notification && window.Notification.requestPermission(); - if (def) { - def.then(function (value) { - if (value === 'denied') { - utils.send_notification(self, _t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.')); - } else { - utils.send_notification(self, _t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.')); - } - }); - } - }, - "keydown": function (event) { - if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) { - this.unselect_message(); - } - }, - "click .o_mail_open_channels": function () { - this.do_action({ - name: _t('Public Channels'), - type: 'ir.actions.act_window', - res_model: "mail.channel", - views: [[false, 'kanban'], [false, 'form']], - domain: [['public', '!=', 'private']], - }, { - on_reverse_breadcrumb: this.on_reverse_breadcrumb, - }); - }, - }, - - on_attach_callback: function () { - chat_manager.bus.trigger('client_action_open', true); - if (this.channel) { - this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); - } - }, - on_detach_callback: function () { - chat_manager.bus.trigger('client_action_open', false); - this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); + 'blur .o_mail_add_channel input': '_onAddChannelBlur', + 'click .o_mail_annoying_notification_bar .fa-close': '_onCloseNotificationBar', + 'click .o_mail_chat_channel_item': '_onChannelClicked', + 'click .o_mail_open_channels': '_onPublicChannelsClick', + 'click .o_mail_partner_unpin': '_onUnpinChannel', + 'click .o_mail_request_permission': '_onRequestNotificationPermission', + 'click .o_mail_sidebar_title .o_add': '_onAddChannel', + 'keydown': '_onKeydown', }, - init: function(parent, action, options) { + /** + * @override + */ + init: function (parent, action, options) { this._super.apply(this, arguments); this.action_manager = parent; this.dataset = new data.DataSetSearch(this, 'mail.message'); @@ -170,12 +130,18 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.action = action; this.options = options || {}; this.channels_scrolltop = {}; - this.throttled_renderSidebar = _.throttle(this.renderSidebar.bind(this), 100, { leading: false }); + this.throttledUpdateChannels = _.throttle(this._updateChannels.bind(this), 100, { leading: false }); this.notification_bar = (window.Notification && window.Notification.permission === "default"); this.selected_message = null; this.composerStates = {}; + this.defaultChannelID = this.options.active_id || + this.action.context.active_id || + this.action.params.default_active_id || + 'channel_inbox'; }, - + /** + * @override + */ willStart: function () { var self = this; var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0]; @@ -186,92 +152,33 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }); return $.when(this._super(), chat_manager.is_ready, def); }, - - start: function() { + /** + * @override + */ + start: function () { var self = this; - // create searchview - var options = { - $buttons: $("<div>"), - action: this.action, - disable_groupby: true, - }; - var default_channel_id = this.options.active_id || - this.action.context.active_id || - this.action.params.default_active_id || - 'channel_inbox'; - var default_channel = chat_manager.get_channel(default_channel_id) || + var default_channel = chat_manager.get_channel(this.defaultChannelID) || chat_manager.get_channel('channel_inbox'); - - this.searchview = new SearchView(this, this.dataset, this.fields_view, options); - 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, - }); - - this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); - this.$buttons.find('button').css({display:"inline-block"}); - this.$buttons.on('click', '.o_mail_chat_button_invite', this.on_click_button_invite); - this.$buttons.on('click', '.o_mail_chat_button_unsubscribe', this.on_click_button_unsubscribe); - this.$buttons.on('click', '.o_mail_chat_button_settings', this.on_click_button_settings); - this.$buttons.on('click', '.o_mail_toggle_channels', function () { - self.$('.o_mail_chat_sidebar').slideToggle(200); - }); - this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () { - chat_manager.mark_all_as_read(self.channel, self.domain); - }); - this.$buttons.on('click', '.o_mail_chat_button_unstar_all', function () { - chat_manager.unstar_all(); - }); - - this.thread.on('redirect', this, function (res_model, res_id) { - chat_manager.redirect(res_model, res_id, this.set_channel.bind(this)); - }); - this.thread.on('redirect_to_channel', this, function (channel_id) { - chat_manager.join_channel(channel_id).then(this.set_channel.bind(this)); - }); - this.thread.on('load_more_messages', this, this.load_more_messages); - this.thread.on('mark_as_read', this, function (message_id) { - chat_manager.mark_as_read([message_id]); - }); - this.thread.on('toggle_star_status', this, function (message_id) { - chat_manager.toggle_star_status(message_id); - }); - this.thread.on('select_message', this, this.select_message); - this.thread.on('unselect_message', this, this.unselect_message); - - this.basic_composer.on('post_message', this, this.on_post_message); - this.basic_composer.on('input_focused', this, this.on_composer_input_focused); - this.extended_composer.on('post_message', this, this.on_post_message); - this.extended_composer.on('input_focused', this, this.on_composer_input_focused); - - 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>")).then(function () { - self.$searchview_buttons = self.searchview.$buttons.contents(); - }); - - this.renderSidebar(); - - return $.when(def1, def2, def3, def4) - .then(this.set_channel.bind(this, default_channel)) + this.basic_composer.on('post_message', this, this._onPostMessage); + this.basic_composer.on('input_focused', this, this._onComposerFocused); + this.extended_composer.on('post_message', this, this._onPostMessage); + this.extended_composer.on('input_focused', this, this._onComposerFocused); + + var defs = []; + defs.push(this._renderButtons()); + defs.push(this._renderThread()); + defs.push(this.basic_composer.appendTo(this.$('.o_mail_chat_content'))); + defs.push(this.extended_composer.appendTo(this.$('.o_mail_chat_content'))); + defs.push(this._renderSearchView()); + + return $.when.apply($, defs) + .then(this._setChannel.bind(this, default_channel)) + .then(this._updateChannels.bind(this)) .then(function () { - chat_manager.bus.on('open_channel', self, self.set_channel); - chat_manager.bus.on('new_message', self, self.on_new_message); - 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 && self.thread.is_at_bottom()); - }); - chat_manager.bus.on('unsubscribe_from_channel', self, self.on_channel_unsubscribed); - chat_manager.bus.on('update_needaction', self, self.throttled_renderSidebar); - chat_manager.bus.on('update_starred', self, self.throttled_renderSidebar); - chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_renderSidebar); - chat_manager.bus.on('update_dm_presence', self, self.throttled_renderSidebar); - chat_manager.bus.on('activity_updated', self, self.throttled_renderSidebar); + self._startListening(); self.thread.$el.on("scroll", null, _.debounce(function () { if (self.thread.is_at_bottom()) { chat_manager.mark_channel_as_seen(self.channel); @@ -279,107 +186,216 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }, 100)); }); }, - - select_message: function(message_id) { - this.$el.addClass('o_mail_selection_mode'); - var message = chat_manager.get_message(message_id); - this.selected_message = message; - var subject = "Re: " + message.record_name; - this.extended_composer.set_subject(subject); - - if (this.channel.type !== 'static') { - this.basic_composer.do_hide(); + /** + * @override + */ + do_show: function () { + this._super.apply(this, arguments); + this._updateControlPanel(); + this.action_manager.do_push_state({ + action: this.action.id, + active_id: this.channel.id, + }); + }, + /** + * @override + */ + destroy: function () { + this.$buttons.off().destroy(); + this._super.apply(this, arguments); + }, + /** + * @override + */ + on_attach_callback: function () { + chat_manager.bus.trigger('client_action_open', true); + if (this.channel) { + this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); } - this.extended_composer.do_show(); - - this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true}); - this.extended_composer.focus('body'); + }, + /** + * @override + */ + on_detach_callback: function () { + chat_manager.bus.trigger('client_action_open', false); + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); }, - unselect_message: function() { - this.basic_composer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing); - this.extended_composer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing); + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- - if (!config.device.touch) { - var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; - composer.focus(); + /** + * @private + * @returns {Deferred} + */ + _fetchAndRenderThread: function () { + var self = this; + return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (result) { + self.thread.render(result, self._getThreadRenderingOptions(result)); + self._updateButtonStatus(result.length === 0); + }); + }, + /** + * @private + * @param {Object} messages + * @returns {Object} + */ + _getThreadRenderingOptions: function (messages) { + // 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)) { + if (!this.unread_counter) { + this.messages_separator_position = false; // no unread message -> don't display separator + } else { + var msg = chat_manager.get_last_seen_message(this.channel); + this.messages_separator_position = msg ? msg.id : 'top'; + } } - this.$el.removeClass('o_mail_selection_mode'); - this.thread.unselect(); - this.selected_message = null; + return { + channel_id: this.channel.id, + display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain), + display_needactions: this.channel.display_needactions, + messages_separator_position: this.messages_separator_position, + squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing, + display_empty_channel: !messages.length && !this.domain.length, + display_no_match: !messages.length && this.domain.length, + display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", + display_email_icon: false, + display_reply_icon: true, + }; }, - - renderSidebar: function () { + /** + * @private + * @returns {Deferred} + */ + _loadMoreMessages: function () { var self = this; - var $sidebar = this._renderSidebar({ - active_channel_id: this.channel ? this.channel.id: undefined, - channels: chat_manager.get_channels(), - needaction_counter: chat_manager.get_needaction_counter(), - starred_counter: chat_manager.get_starred_counter(), - }); - this.$(".o_mail_chat_sidebar").html($sidebar.contents()); - - this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({ - source: function(request, response) { - self.last_search_val = _.escape(request.term); - self.do_search_channel(self.last_search_val).done(function(result){ - result.push({ - 'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"#'+self.last_search_val+'"</em>'), - 'value': '_create', + var oldest_msg_id = this.$('.o_thread_message').first().data('messageId'); + var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]'; + var offset = -dom.getPosition(document.querySelector(oldest_msg_selector)).top; + return chat_manager + .get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true}) + .then(function (result) { + if (self.messages_separator_position === 'top') { + self.messages_separator_position = undefined; // reset value to re-compute separator position + } + self.thread.render(result, self._getThreadRenderingOptions(result)); + offset += dom.getPosition(document.querySelector(oldest_msg_selector)).top; + self.thread.scroll_to({offset: offset}); + }); + }, + /** + * Binds handlers on the given $input to make them autocomplete and/or + * create channels. + * + * @private + * @param {JQuery} $input the input to prepare + * @param {string} type the type of channel to create ('dm', 'public' or + * 'private') + */ + _prepareAddChannelInput: function ($input, type) { + var self = this; + if (type === 'public') { + $input.autocomplete({ + source: function (request, response) { + self.last_search_val = _.escape(request.term); + self._searchChannel(self.last_search_val).done(function (result){ + result.push({ + 'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"#'+self.last_search_val+'"</em>'), + 'value': '_create', + }); + response(result); }); - response(result); - }); - }, - select: function(event, ui) { - if (self.last_search_val) { - if (ui.item.value === '_create') { - chat_manager.create_channel(self.last_search_val, "public"); - } else { - chat_manager.join_channel(ui.item.id); + }, + select: function (event, ui) { + if (self.last_search_val) { + if (ui.item.value === '_create') { + chat_manager.create_channel(self.last_search_val, "public"); + } else { + chat_manager.join_channel(ui.item.id); + } } + }, + focus: function (event) { + event.preventDefault(); + }, + html: true, + }); + } else if (type === 'private') { + $input.on('keyup', this, function (event) { + var name = _.escape($(event.target).val()); + if(event.which === $.ui.keyCode.ENTER && name) { + chat_manager.create_channel(name, "private"); } - }, - focus: function(event) { - event.preventDefault(); - }, - html: true, - }); - - this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({ - source: function(request, response) { - self.last_search_val = _.escape(request.term); - chat_manager.search_partner(self.last_search_val, 10).done(response); - }, - select: function(event, ui) { - var partner_id = ui.item.id; - var dm = chat_manager.get_dm_from_partner_id(partner_id); - if (dm) { - self.set_channel(dm); - } else { - chat_manager.create_channel(partner_id, "dm"); - } - // clear the input - $(this).val(''); - return false; - }, - focus: function(event) { - event.preventDefault(); - }, - }); - - this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) { - var name = _.escape($(event.target).val()); - if(event.which === $.ui.keyCode.ENTER && name) { - chat_manager.create_channel(name, "private"); - } + }); + } else if (type === 'dm') { + $input.autocomplete({ + source: function (request, response) { + self.last_search_val = _.escape(request.term); + chat_manager.search_partner(self.last_search_val, 10).done(response); + }, + select: function (event, ui) { + var partner_id = ui.item.id; + var dm = chat_manager.get_dm_from_partner_id(partner_id); + if (dm) { + self._setChannel(dm); + } else { + chat_manager.create_channel(partner_id, "dm"); + } + // clear the input + $(this).val(''); + return false; + }, + focus: function (event) { + event.preventDefault(); + }, + }); + } + }, + /** + * @private + */ + _renderButtons: function () { + this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {debug: session.debug})); + this.$buttons.find('button').css({display:"inline-block"}); + this.$buttons.on('click', '.o_mail_chat_button_invite', this._onInviteButtonClicked.bind(this)); + this.$buttons.on('click', '.o_mail_chat_button_unsubscribe', this._onUnsubscribeButtonClicked.bind(this)); + this.$buttons.on('click', '.o_mail_chat_button_settings', this._onSettingsButtonClicked.bind(this)); + this.$buttons.on('click', '.o_mail_chat_button_mark_read', this._onMarkAllReadClicked.bind(this)); + this.$buttons.on('click', '.o_mail_chat_button_unstar_all', this._onUnstarAllClicked.bind(this)); + }, + /** + * @private + * @returns {Deferred} + */ + _renderSearchView: function () { + var self = this; + var options = { + $buttons: $("<div>"), + action: this.action, + disable_groupby: true, + }; + this.searchview = new SearchView(this, this.dataset, this.fields_view, options); + return this.searchview.appendTo($("<div>")).then(function () { + self.$searchview_buttons = self.searchview.$buttons.contents(); }); }, - + /** + * @private + * @param {Object} options + * @returns {JQuery} + */ _renderSidebar: function (options) { return $(QWeb.render("mail.chat.Sidebar", options)); }, - - render_snackbar: function (template, context, timeout) { + /** + * @private + * @param {string} template + * @param {Object} context rendering context + * @param {integer} [timeout=20000] the delay before the snackbar disappears + */ + _renderSnackbar: function (template, context, timeout) { if (this.$snackbar) { this.$snackbar.remove(); } @@ -388,18 +404,78 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.$('.o_mail_chat_content').append(this.$snackbar); // Hide snackbar after [timeout] milliseconds (by default, 20s) var $snackbar = this.$snackbar; - setTimeout(function() { $snackbar.fadeOut(); }, timeout); + setTimeout(function () { $snackbar.fadeOut(); }, timeout); }, + /** + * Renders, binds events and appends a thread widget. + * + * @private + * @returns {Deferred} + */ + _renderThread: function () { + this.thread = new ChatThread(this, {display_help: true}); + + this.thread.on('redirect', this, function (res_model, res_id) { + chat_manager.redirect(res_model, res_id, this._setChannel.bind(this)); + }); + this.thread.on('redirect_to_channel', this, function (channel_id) { + chat_manager.join_channel(channel_id).then(this._setChannel.bind(this)); + }); + this.thread.on('load_more_messages', this, this._loadMoreMessages); + this.thread.on('mark_as_read', this, function (message_id) { + chat_manager.mark_as_read([message_id]); + }); + this.thread.on('toggle_star_status', this, function (message_id) { + chat_manager.toggle_star_status(message_id); + }); + this.thread.on('select_message', this, this._selectMessage); + this.thread.on('unselect_message', this, this._unselectMessage); - do_search_channel: function(search_val){ + return this.thread.appendTo(this.$('.o_mail_chat_content')); + }, + /** + * Restores the scroll position and composer state of the current channel + * + * @private + */ + _restoreChannelState: function () { + var $new_messages_separator = this.$('.o_thread_new_messages_separator'); + if ($new_messages_separator.length) { + this.thread.$el.scrollTo($new_messages_separator); + } else { + var new_channel_scrolltop = this.channels_scrolltop[this.channel.id]; + this.thread.scroll_to({offset: new_channel_scrolltop}); + } + this._restoreComposerState(this.channel); + }, + /** + * @private + * @param {Object} channel + */ + _restoreComposerState: function (channel) { + if (channel.type === 'static') { + return; // no composer in static channels + } + var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer; + var composerState = this.composerStates[channel.uuid]; + if (composerState) { + composer.setState(composerState); + } + }, + /** + * @private + * @param {string} search_val + * @returns {Deferred<Array>} + */ + _searchChannel: function (search_val){ return this._rpc({ model: 'mail.channel', method: 'channel_search_to_join', args: [search_val] }) - .then(function(result){ + .then(function (result){ var values = []; - _.each(result, function(channel){ + _.each(result, function (channel){ var escaped_name = _.escape(channel.name); values.push(_.extend(channel, { 'value': escaped_name, @@ -409,32 +485,37 @@ var ChatAction = Widget.extend(ControlPanelMixin, { return values; }); }, + /** + * @private + * @param {integer} message_id + */ + _selectMessage: function (message_id) { + this.$el.addClass('o_mail_selection_mode'); + var message = chat_manager.get_message(message_id); + this.selected_message = message; + var subject = "Re: " + message.record_name; + this.extended_composer.set_subject(subject); - storeComposerState: function (channel) { - var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer; - this.composerStates[channel.uuid] = composer.getState(); - composer.clear_composer(); - }, - - restoreComposerState: function (channel) { - var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer; - var composerState = this.composerStates[channel.uuid]; - if (composerState) { - composer.setState(composerState); + if (this.channel.type !== 'static') { + this.basic_composer.do_hide(); } - }, + this.extended_composer.do_show(); - set_channel: function (channel) { + this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true}); + this.extended_composer.focus('body'); + }, + /** + * @private + * @param {Object} channel + * @returns {Deferred} + */ + _setChannel: function (channel) { var self = this; - // Store scroll position of previous channel - if (this.channel) { - this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); - this.storeComposerState(this.channel); - } - var new_channel_scrolltop = this.channels_scrolltop[channel.id]; + + // Store scroll position and composer state of the previous channel + this._storeChannelState(); this.channel = channel; - this.restoreComposerState(this.channel); this.messages_separator_position = undefined; // reset value on channel change this.unread_counter = this.channel.unread_counter; this.last_seen_message_id = this.channel.last_seen_message_id; @@ -445,51 +526,23 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.action.context.active_id = channel.id; this.action.context.active_ids = [channel.id]; - return this.fetch_and_render_thread().then(function () { + return this._fetchAndRenderThread().then(function () { // Mark channel's messages as read and clear needactions if (channel.type !== 'static') { chat_manager.mark_channel_as_seen(channel); } - // Update control panel - self.set("title", '#' + channel.name); - // Hide 'unsubscribe' button in state channels and DM and channels with group-based subscription - self.$buttons - .find('.o_mail_chat_button_unsubscribe') - .toggle(channel.type !== "dm" && channel.type !== 'static' && ! channel.group_based_subscription); - // Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM - self.$buttons - .find('.o_mail_chat_button_invite, .o_mail_chat_button_settings') - .toggle(channel.type !== "dm" && channel.type !== 'static'); - self.$buttons - .find('.o_mail_chat_button_mark_read') - .toggle(channel.id === "channel_inbox"); - self.$buttons - .find('.o_mail_chat_button_unstar_all') - .toggle(channel.id === "channel_starred"); - - self.$('.o_mail_chat_channel_item') - .removeClass('o_active') - .filter('[data-channel-id=' + channel.id + ']') - .removeClass('o_unread_message') - .addClass('o_active'); - - var $new_messages_separator = self.$('.o_thread_new_messages_separator'); - if ($new_messages_separator.length) { - self.thread.$el.scrollTo($new_messages_separator); - } else { - self.thread.scroll_to({offset: new_channel_scrolltop}); - } + // Restore scroll position and composer of the new current channel + self._restoreChannelState(); // Update control panel before focusing the composer, otherwise focus is on the searchview - self.update_cp(); - if (config.device.size_class === config.device.SIZES.XS) { - self.$('.o_mail_chat_sidebar').hide(); - } + self.set("title", '#' + channel.name); + self._updateControlPanel(); + self._updateControlPanelButtons(channel); // Display and focus the adequate composer, and unselect possibly selected message // to prevent sending messages as reply to that message - self.unselect_message(); + self._unselectMessage(); self.action_manager.do_push_state({ action: self.action.id, @@ -497,41 +550,90 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }); }); }, - - get_thread_rendering_options: function (messages) { - // 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)) { - if (!this.unread_counter) { - this.messages_separator_position = false; // no unread message -> don't display separator - } else { - var msg = chat_manager.get_last_seen_message(this.channel); - this.messages_separator_position = msg ? msg.id : 'top'; - } + /** + * Binds handlers on chat_manager events + * + * @private + */ + _startListening: function () { + chat_manager.bus.on('open_channel', this, this._setChannel); + chat_manager.bus.on('new_message', this, this._onNewMessage); + chat_manager.bus.on('update_message', this, this._onMessageUpdated); + chat_manager.bus.on('new_channel', this, this._onNewChannel); + chat_manager.bus.on('anyone_listening', this, function (channel, query) { + query.is_displayed = query.is_displayed || + (channel.id === this.channel.id && this.thread.is_at_bottom()); + }); + chat_manager.bus.on('unsubscribe_from_channel', this, this._onChannelLeft); + chat_manager.bus.on('update_needaction', this, this.throttledUpdateChannels); + chat_manager.bus.on('update_starred', this, this.throttledUpdateChannels); + chat_manager.bus.on('update_channel_unread_counter', this, this.throttledUpdateChannels); + chat_manager.bus.on('update_dm_presence', this, this.throttledUpdateChannels); + chat_manager.bus.on('activity_updated', this, this.throttledUpdateChannels); + }, + /** + * Stores the scroll position and composer state of the current channel + * + * @private + */ + _storeChannelState: function () { + if (this.channel) { + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); + this._storeComposerState(this.channel); } - return { - channel_id: this.channel.id, - display_load_more: !chat_manager.all_history_loaded(this.channel, this.domain), - display_needactions: this.channel.display_needactions, - messages_separator_position: this.messages_separator_position, - squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing, - display_empty_channel: !messages.length && !this.domain.length, - display_no_match: !messages.length && this.domain.length, - display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", - display_email_icon: false, - display_reply_icon: true, - }; }, + /** + * @private + * @param {Object} channel + */ + _storeComposerState: function (channel) { + if (channel.type === 'static') { + return; // no composer in static channels + } + var composer = channel.mass_mailing ? this.extended_composer : this.basic_composer; + this.composerStates[channel.uuid] = composer.getState(); + composer.clear_composer(); + }, + /** + * @private + */ + _unselectMessage: function () { + this.basic_composer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing); + this.extended_composer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing); - fetch_and_render_thread: function () { + if (!config.device.touch) { + var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; + composer.focus(); + } + this.$el.removeClass('o_mail_selection_mode'); + this.thread.unselect(); + this.selected_message = null; + }, + /** + * Renders the mainside bar with current channels + * + * @private + */ + _updateChannels: function () { var self = this; - return chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function(result) { - self.thread.render(result, self.get_thread_rendering_options(result)); - self.update_button_status(result.length === 0); + var $sidebar = this._renderSidebar({ + active_channel_id: this.channel ? this.channel.id: undefined, + channels: chat_manager.get_channels(), + needaction_counter: chat_manager.get_needaction_counter(), + starred_counter: chat_manager.get_starred_counter(), + }); + this.$(".o_mail_chat_sidebar").html($sidebar.contents()); + _.each(['dm', 'public', 'private'], function (type) { + var $input = self.$('.o_mail_add_channel[data-type=' + type + '] input'); + self._prepareAddChannelInput($input, type); }); }, - - update_button_status: function (disabled, type) { + /** + * @private + * @param {boolean} disabled + * @param {string} type + */ + _updateButtonStatus: function (disabled, type) { if (this.channel.id === "channel_inbox") { this.$buttons .find('.o_mail_chat_button_mark_read') @@ -551,25 +653,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, { .toggleClass('disabled', disabled); } }, - - load_more_messages: function () { - var self = this; - var oldest_msg_id = this.$('.o_thread_message').first().data('messageId'); - var oldest_msg_selector = '.o_thread_message[data-message-id="' + oldest_msg_id + '"]'; - var offset = -dom.getPosition(document.querySelector(oldest_msg_selector)).top; - return chat_manager - .get_messages({channel_id: this.channel.id, domain: this.domain, load_more: true}) - .then(function(result) { - if (self.messages_separator_position === 'top') { - self.messages_separator_position = undefined; // reset value to re-compute separator position - } - self.thread.render(result, self.get_thread_rendering_options(result)); - offset += dom.getPosition(document.querySelector(oldest_msg_selector)).top; - self.thread.scroll_to({offset: offset}); - }); - }, - - update_cp: function () { + /** + * @private + */ + _updateControlPanel: function () { this.update_control_panel({ breadcrumbs: this.action_manager.get_breadcrumbs(), cp_content: { @@ -580,17 +667,152 @@ var ChatAction = Widget.extend(ControlPanelMixin, { searchview: this.searchview, }); }, + /** + * Updates the control panel buttons visibility based on channel type + * + * @private + * @param {Object} channel + */ + _updateControlPanelButtons: function (channel) { + // Hide 'unsubscribe' button in state channels and DM and channels with group-based subscription + this.$buttons + .find('.o_mail_chat_button_unsubscribe') + .toggle(channel.type !== "dm" && channel.type !== 'static' && ! channel.group_based_subscription); + // Hide 'invite', 'unsubscribe' and 'settings' buttons in static channels and DM + this.$buttons + .find('.o_mail_chat_button_invite, .o_mail_chat_button_settings') + .toggle(channel.type !== "dm" && channel.type !== 'static'); + this.$buttons + .find('.o_mail_chat_button_mark_read') + .toggle(channel.id === "channel_inbox") + .removeClass("o_hidden"); + this.$buttons + .find('.o_mail_chat_button_unstar_all') + .toggle(channel.id === "channel_starred") + .removeClass("o_hidden"); + + this.$('.o_mail_chat_channel_item') + .removeClass('o_active') + .filter('[data-channel-id=' + channel.id + ']') + .removeClass('o_unread_message') + .addClass('o_active'); + }, - do_show: function () { - this._super.apply(this, arguments); - this.update_cp(); - this.action_manager.do_push_state({ - action: this.action.id, - active_id: this.channel.id, - }); + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} event + */ + _onAddChannel: function (event) { + event.preventDefault(); + var type = $(event.target).data("type"); + this.$('.o_mail_add_channel[data-type=' + type + ']') + .show() + .find("input").focus(); + }, + /** + * @private + */ + _onAddChannelBlur: function () { + this.$('.o_mail_add_channel').hide(); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onChannelClicked: function (event) { + event.preventDefault(); + var channel_id = $(event.currentTarget).data('channel-id'); + this._setChannel(chat_manager.get_channel(channel_id)); + }, + /** + * @private + * @param {integer || string} channel_id + */ + _onChannelLeft: function (channel_id) { + if (this.channel.id === channel_id) { + this._setChannel(chat_manager.get_channel("channel_inbox")); + } + this._updateChannels(); + delete this.channels_scrolltop[channel_id]; + }, + /** + * @private + */ + _onComposerFocused: function () { + var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; + var commands = chat_manager.get_commands(this.channel); + var partners = chat_manager.get_mention_partner_suggestions(this.channel); + composer.mention_set_enabled_commands(commands); + composer.mention_set_prefetched_partners(partners); + }, + /** + * @private + */ + _onMarkAllReadClicked: function () { + chat_manager.mark_all_as_read(this.channel, this.domain); + }, + /** + * @private + * @param {Object} message + * @param {string} type the channel type + */ + _onMessageUpdated: function (message, type) { + var self = this; + var current_channel_id = this.channel.id; + if ((current_channel_id === "channel_starred" && !message.is_starred) || + (current_channel_id === "channel_inbox" && !message.is_needaction)) { + chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) { + var options = self._getThreadRenderingOptions(messages); + self.thread.remove_message_and_render(message.id, messages, options).then(function () { + self._updateButtonStatus(messages.length === 0, type); + }); + }); + } else if (_.contains(message.channel_ids, current_channel_id)) { + this._fetchAndRenderThread(); + } }, + /** + * @private + * @param {Object} channel + */ + _onNewChannel: function (channel) { + this._updateChannels(); + if (channel.autoswitch) { + this._setChannel(channel); + } + }, + /** + * @private + * @param {Object} message + */ + _onNewMessage: function (message) { + var self = this; + if (_.contains(message.channel_ids, this.channel.id)) { + if (this.channel.type !== 'static' && this.thread.is_at_bottom()) { + chat_manager.mark_channel_as_seen(this.channel); + } - on_post_message: function (message) { + var should_scroll = this.thread.is_at_bottom(); + this._fetchAndRenderThread().then(function () { + 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 + this._updateChannels(); + // Dump scroll position of channels in which the new message arrived + this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids); + }, + /** + * @private + * @param {Object} message + */ + _onPostMessage: function (message) { var self = this; var options = this.selected_message ? {} : {channel_id: this.channel.id}; if (this.selected_message) { @@ -604,10 +826,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, { } chat_manager .post_message(message, options) - .then(function() { + .then(function () { if (self.selected_message) { - self.render_snackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000); - self.unselect_message(); + self._renderSnackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000); + self._unselectMessage(); } else { self.thread.scroll_to(); } @@ -616,74 +838,80 @@ var ChatAction = Widget.extend(ControlPanelMixin, { // todo: display notification }); }, - on_new_message: function (message) { - var self = this; - if (_.contains(message.channel_ids, this.channel.id)) { - 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 () { - 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 - this.renderSidebar(); - // Dump scroll position of channels in which the new message arrived - this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids); + /** + * @private + */ + _onPublicChannelsClick: function () { + this.do_action({ + name: _t('Public Channels'), + type: 'ir.actions.act_window', + res_model: "mail.channel", + views: [[false, 'kanban'], [false, 'form']], + domain: [['public', '!=', 'private']], + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb, + }); }, - on_update_message: function (message, type) { - // To identify 'update_button_status' method is called from from 'Mark as read / Mark all read' - // button, we are passing type to this method so that rainbowman will only appear on marking all - // messages as read (because the same method is also called from 'fetch_and_render_thread' when - // clicking @inbox, and we don't want rainbowman to appear everytime on empty inbox) - var self = this; - var current_channel_id = this.channel.id; - if ((current_channel_id === "channel_starred" && !message.is_starred) || - (current_channel_id === "channel_inbox" && !message.is_needaction)) { - chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) { - var options = self.get_thread_rendering_options(messages); - self.thread.remove_message_and_render(message.id, messages, options).then(function () { - self.update_button_status(messages.length === 0, type); - }); - }); - } else if (_.contains(message.channel_ids, current_channel_id)) { - this.fetch_and_render_thread(); - } + /** + * @private + */ + _onCloseNotificationBar: function () { + this.$(".o_mail_annoying_notification_bar").slideUp(); }, - on_new_channel: function (channel) { - this.renderSidebar(); - if (channel.autoswitch) { - this.set_channel(channel); - } + /** + * @private + */ + _onInviteButtonClicked: function () { + var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); + new PartnerInviteDialog(this, title, this.channel.id).open(); }, - on_channel_unsubscribed: function (channel_id) { - if (this.channel.id === channel_id) { - this.set_channel(chat_manager.get_channel("channel_inbox")); + /** + * @private + * @param {KeyEvent} event + */ + _onKeydown: function (event) { + if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) { + this._unselectMessage(); } - this.renderSidebar(); - delete this.channels_scrolltop[channel_id]; - }, - on_composer_input_focused: function () { - var composer = this.channel.mass_mailing ? this.extended_composer : this.basic_composer; - var commands = chat_manager.get_commands(this.channel); - var partners = chat_manager.get_mention_partner_suggestions(this.channel); - composer.mention_set_enabled_commands(commands); - composer.mention_set_prefetched_partners(partners); }, - - on_click_button_invite: function () { - var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); - new PartnerInviteDialog(this, title, this.channel.id).open(); + /** + * @private + * @param {MouseEvent} event + */ + _onRequestNotificationPermission: function (event) { + var self = this; + event.preventDefault(); + this.$(".o_mail_annoying_notification_bar").slideUp(); + var def = window.Notification && window.Notification.requestPermission(); + if (def) { + def.then(function (value) { + if (value === 'denied') { + utils.send_notification(self, _t('Permission denied'), + _t('Odoo will not have the permission to send native notifications on this device.')); + } else { + utils.send_notification(self, _t('Permission granted'), + _t('Odoo has now the permission to send you native notifications on this device.')); + } + }); + } }, - - on_click_button_unsubscribe: function () { - chat_manager.unsubscribe(this.channel); + /** + * @private + * @param {OdooEvent} + */ + _onSearch: function (event) { + var session = this.getSession(); + var result = pyeval.eval_domains_and_contexts({ + domains: event.data.domains, + contexts: [session.user_context], + }); + this.domain = result.domain; + this._fetchAndRenderThread(); }, - on_click_button_settings: function() { + /** + * @private + */ + _onSettingsButtonClicked: function () { this.do_action({ type: 'ir.actions.act_window', res_model: "mail.channel", @@ -692,31 +920,31 @@ var ChatAction = Widget.extend(ControlPanelMixin, { target: 'current' }); }, - destroy: function() { - this.$buttons.off().destroy(); - this._super.apply(this, arguments); + /** + * @private + * @param {MouseEvent} event + */ + _onUnpinChannel: function (event) { + event.stopPropagation(); + var channel_id = $(event.target).data("channel-id"); + chat_manager.unsubscribe(chat_manager.get_channel(channel_id)); }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - /** * @private - * @param {OdooEvent} */ - _onSearch: function (event) { - var session = this.getSession(); - var result = pyeval.eval_domains_and_contexts({ - domains: event.data.domains, - contexts: [session.user_context], - }); - this.domain = result.domain; - this.fetch_and_render_thread(); + _onUnstarAllClicked: function () { + chat_manager.unstar_all(); + }, + /** + * @private + */ + _onUnsubscribeButtonClicked: function () { + chat_manager.unsubscribe(this.channel); }, }); - core.action_registry.add('mail.chat.instant_messaging', ChatAction); +return ChatAction; + }); 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 = $('<button/>', {type: 'button'}) + .addClass('o_enable_searchview btn fa fa-search') + .on('click', this._toggleSearchView.bind(this)); + $enable_searchview.insertAfter(this.searchview.$el); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAddChannel: function () { + this.$('.o_mail_add_channel').show().find('input').focus(); + }, + /** + * Switches to the clicked channel in the Inbox page (Inbox or Starred). + * + * @private + * @param {MouseEvent} + */ + _onMobileInboxButtonClicked: function (event) { + this._setChannel(chat_manager.get_channel($(event.currentTarget).data('type'))); + this._updateContent(this.channel.id); + }, + /** + * Switches to another tab. + * + * @private + * @param {MouseEvent} + */ + _onMobileTabClicked: function (event) { + var type = $(event.currentTarget).data('type'); + if (type === 'channel_inbox') { + this._setChannel(chat_manager.get_channel('channel_inbox')); + } + this._updateContent(type); + }, + /** + * Opens a channel in a chat windown (full screen in mobile). + * + * @private + * @param {MouseEvent} + */ + _onMobileChannelClicked: function (event) { + var channelId = $(event.currentTarget).data("channel_id"); + chat_manager.detach_channel(chat_manager.get_channel(channelId)); + }, +}); + +}); diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 2782ff11442..e46225a8f65 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -5,6 +5,7 @@ var chat_mixin = require('mail.chat_mixin'); var DocumentViewer = require('mail.DocumentViewer'); var utils = require('mail.utils'); +var config = require('web.config'); var core = require('web.core'); var data = require('web.data'); var dom = require('web.dom'); @@ -374,6 +375,7 @@ var BasicComposer = Widget.extend(chat_mixin, { send_text: _t('Send'), default_body: '', default_mention_selections: {}, + isMobile: config.isMobile }); this.context = this.options.context; diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index 5995c635f33..5964248d4fd 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -1,6 +1,7 @@ odoo.define('mail.systray', function (require) { "use strict"; +var config = require('web.config'); var core = require('web.core'); var framework = require('web.framework'); var session = require('web.session'); @@ -27,6 +28,10 @@ var MessagingMenu = Widget.extend({ "click .o_new_message": "on_click_new_message", "click .o_mail_channel_preview": "_onClickChannel", }, + init: function () { + this._super.apply(this, arguments); + this.isMobile = config.isMobile; // used by the template + }, start: function () { this.$filter_buttons = this.$('.o_filter_button'); this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels'); @@ -83,23 +88,6 @@ var MessagingMenu = Widget.extend({ }); }, _render_channels_preview: 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 = 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'); - } - }); - this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', { channels: channels_preview, })); @@ -111,9 +99,9 @@ var MessagingMenu = Widget.extend({ }, on_click_filter_button: function (event) { event.stopPropagation(); - this.$filter_buttons.removeClass('o_selected'); + this.$filter_buttons.removeClass('active'); var $target = $(event.currentTarget); - $target.addClass('o_selected'); + $target.addClass('active'); this.filter = $target.data('filter'); this.update_channels_preview(); }, @@ -125,14 +113,14 @@ var MessagingMenu = Widget.extend({ /** * When a channel is clicked on, we want to open chat/channel window - * If channel is inbox then redirect to that record view - * If record not linked redirect to Inbox + * * @private * @param {MouseEvent} event */ _onClickChannel: function (event) { + var self = this; var channelID = $(event.currentTarget).data('channel_id'); - if (channelID == 'channel_inbox') { + if (channelID === 'channel_inbox') { var resID = $(event.currentTarget).data('res_id'); var resModel = $(event.currentTarget).data('res_model'); if (resModel && resID) { @@ -143,8 +131,11 @@ var MessagingMenu = Widget.extend({ res_id: resID }); } else { - // if no model linked redirect to inbox - framework.redirect('mail/view?message_id=channel_inbox'); + this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}) + .then(function () { + self.trigger_up('hide_app_switcher'); + core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id()); + }); } } else { var channel = chat_manager.get_channel(channelID); diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js index 1e3ad352dd3..a08b4f5052b 100644 --- a/addons/mail/static/src/js/window_manager.js +++ b/addons/mail/static/src/js/window_manager.js @@ -272,6 +272,7 @@ function render_hidden_sessions_dropdown () { sessions: display_state.hidden_sessions, open: display_state.windows_dropdown_is_open, unread_counter: display_state.hidden_unread_counter, + widget: {isMobile: config.isMobile}, })); return $dropdown; } diff --git a/addons/mail/static/src/less/chat_window.less b/addons/mail/static/src/less/chat_window.less index 3e93945d194..0c7c48ef685 100644 --- a/addons/mail/static/src/less/chat_window.less +++ b/addons/mail/static/src/less/chat_window.less @@ -1,7 +1,8 @@ -@o-chat-window-bg: white; +@o-chat-window-bg: #FAFAFA; @o-chat-window-width: 325px; @o-mail-chatter-gap: 10px; // has to be defined here as this file is both in // backend and other assets (livechat) +@o-chat-header-height: 46px; .o_chat_window { .o-flex-display(); @@ -13,13 +14,14 @@ max-height: 100%; font-size: 12px; background-color: @o-chat-window-bg; - border-radius: 3px 3px 0 0; + border-radius: 6px 6px 0 0; z-index: 3; - box-shadow: -5px -5px 20px fade(black, 28%); + box-shadow: -5px -5px 10px fade(black, 18%); @media (max-width: @screen-xs-max) { width: 100%; height: 100%!important; + box-shadow: none; &.o_folded { display: none; } @@ -37,11 +39,20 @@ border-radius: 3px 3px 0 0; border-bottom: 1px solid @odoo-color-silver-dark; // cannot use gray-lighter-dark as this file is also frontend background-color: @odoo-brand-primary; + padding: 8px; @media (max-width: @screen-xs-max) { - height: @navbar-height; - .o_chat_title, .o_chat_window_close { - font-size: 22px; + height: @o-chat-header-height; + padding-top: 12px; + border-radius: 0px; + .o_chat_title { + font-size: 16px; + margin-left: 10px; + } + + .o_chat_window_close { + font-size: 17px; + color: white; } } @@ -66,24 +77,23 @@ .o_mail_thread { .o-flex(1, 1, auto); overflow: auto; - border: 1px solid @odoo-color-silver-dark; // cannot use gray-lighter-dark as this file is also frontend - border-width: 0 1px; - .o_thread_date_separator { - margin-top: 0px; - margin-bottom: 15px; - border-color: lightgray; + margin: 0px 0px 15px 0px; .o_thread_date { background-color: @o-chat-window-bg; } } .o_thread_message { + padding: 4px 5px; .o_thread_message_sidebar { margin-right: 5px; } - padding: 4px 5px; } } + + .o_chat_composer input { + width: 100%; + } } .o_chat_window_dropdown { diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less index 78a4f27f21b..9a13db80152 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -6,10 +6,7 @@ .o_mail_chat { .o-position-absolute(0, 0, 0, 0); height: 100%; - - @media (max-width: @screen-xs-max) { - height: auto; - } + overflow: hidden; .o_mail_annoying_notification_bar { height: 40px; @@ -47,12 +44,6 @@ overflow: auto; padding: @odoo-horizontal-padding 0; - @media (max-width: @screen-xs-max) { - width: 100%; - float: none; - display: none; - } - @media (min-width: @screen-lg-min) { width: @mail-chat-sidebar-width + 50px; } @@ -130,10 +121,6 @@ overflow: auto; } - @media (max-width: @screen-xs-max) { - margin-left: 0; - } - @media (min-width: @screen-lg-min) { margin-left: @mail-chat-sidebar-width + 50px; } @@ -267,11 +254,222 @@ .o_mail_user_status { font-size: 1em; position: relative; - &.o_user_online { - color: @brand-success; + color: @odoo-brand-optional; } &.o_user_idle { color: @brand-warning; } } + + +// ------------------------------------------------------------------ +// Mobile +// ------------------------------------------------------------------ +@media (max-width: @screen-xs-max) { + .o_mail_chat { + .o-flex-display(); + .o-flex-flow(column, nowrap); + background: white; + + .o_mail_chat_mobile_control_panel { + .o-flex(0, 0, auto); + padding: 5px; + min-height: 44px; + border-bottom: 1px solid #ebebeb; + box-shadow: 0px 0px 10px #afafaf; + z-index: 1; // so that box shaddow is above thread's images + .o_searchview { + border: none; + padding: 5px 5px 0px 5px; + .o_searchview_input { + font-size: 15px; + } + .o_searchview_more { + display: none; + } + } + .o_enable_searchview, .o_enable_searchview:focus, + .o_enable_searchview:hover, .o_enable_searchview:active { + .o-position-absolute(@right: 5px, @top: 5px); + color: gray; + background: none; + font-size: 15px; + &:active { + box-shadow: none; + } + } + } + + .o_mail_chat_mobile_inbox_buttons { + .o-flex(0, 0, auto); + padding: 15px 15px 10px 15px; + .o_channel_inbox_item { + width: 50%; + border: 1px solid @odoo-brand-optional; + &:first-child { + border-radius: 4px 0px 0px 4px; + } + &:last-child { + border-radius: 0px 4px 4px 0px; + } + } + } + + .o_mail_chat_content { + .o-flex(1, 0, 0); + margin-left: 0; + background-color: transparent; + .o_mail_no_content { + .o-position-absolute(@top: 30%); + text-align: center; + } + } + + + .o_mail_chat_tab_pane { + overflow-x: scroll; + .o_mail_add_channel { + display: none; + position: relative; + padding: 5px 0px; + margin: 10px; + border: 1px solid #afafaf; + border-radius: 4px; + > span { + .o-position-absolute(@top: 10px, @left: 10px); + } + > input { + border: none; + padding: 5px 8px; + padding-left: 20px; + } + } + } + .o_mail_mobile_tabs { + .o-flex(0, 0, auto); + } + } +} + +// ------------------------------------------------------------------ +// Channel preview: shared between client action (mobile) and systray +// ------------------------------------------------------------------ + +.o_mail_channel_preview { + .o-flex-display(); + border-bottom: 1px solid #ebebeb; + overflow: hidden; + position: relative; + .o_mail_channel_image { + .o-flex(0, 0, auto); + position: relative; + text-align: center; + > img { + border-radius: 50%; + } + .o_mail_user_status { + .o-position-absolute(@bottom: 0px, @right: 0px); + } + } + .o_channel_info { + .o-flex(1, 1, 100%); + overflow: hidden; + .o_channel_title { + .o-flex-display(); + .o_channel_name { + .o-flex(0, 1, auto); + .o-text-overflow(); + } + .o_channel_counter { + .o-flex(1, 1, auto); + } + .o_channel_name, .o_channel_counter { + color: @odoo-main-text-color; + } + .o_last_message_date { + .o-flex(0, 0, auto); + color: @odoo-main-color-muted; + } + } + .o_last_message_preview { + width: 100%; + max-height: 20px; + color: @odoo-main-color-muted; + .o-text-overflow(); + } + } + &.o_channel_unread { + background-color: #f5f5f5; + .o_channel_info { + .o_channel_title { + .o_channel_name, .o_channel_counter { + font-weight: 700; + } + .o_last_message_date { + color: @odoo-brand-optional; + } + } + } + } +} + +@media (max-width: @screen-xs-max) { + .o_mail_channel_preview { + height: 85px; + padding: 15px; + .o_channel_info { + margin-left: 15px; + padding-top: 5px; + .o_channel_title { + .o_channel_name, .o_channel_counter { + font-size: 15px; + font-weight: 500; + } + .o_last_message_date { + padding-top: 2px; + margin-left: 10px; + } + } + } + &.o_channel_unread { + .o_channel_info .o_last_message_preview { + font-weight: 500; + } + } + } + .o_mail_channel_image { + width: 55px; + > img { + max-height: 55px; + } + } + .o_mail_mobile_tabs { + .o-flex-display(); + .o-flex-flow(row, nowrap); + border-top: 1px solid #ebebeb; + box-shadow: 0px 0px 10px #afafaf; + z-index: 1; // so that box shaddow is above thread's images + .o_mail_mobile_tab { + .o-flex(1, 1, 0); + padding: 8px 4px; + text-align: center; + &:not(:last-child) { + border-right: 1px solid #d7d7d7; + } + > span { + display: block; + &.fa { + font-size: 16px; + } + &.o_tab_title{ + font-size: 8px; + padding-top: 8px; + } + } + &.active > span { + color: @odoo-brand-optional; + } + } + } +} diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index e32c1167ac2..cc95a0ddd21 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -54,6 +54,11 @@ .o_composer_button_full_composer { .o-position-absolute(0, 0); } + @media (max-width: @screen-xs-max) { + .o_composer_button_send { + color: @odoo-brand-optional; + } + } } // Both inline and not-inline design. Mini-composer disabled. @@ -171,19 +176,21 @@ .o_composer_input { .o-flex-display(); .o-flex-flow(row, nowrap); - padding: @o-mail-chatter-gap; textarea.o_composer_text_field { .o-flex(1, 1, auto); - padding-top: @o-mail-chatter-gap*0.5; } .o_chatter_composer_tools { .o-flex(0, 0, auto); } + @media (max-width: @screen-xs-max) { + .o_composer_button_send { + display: inline-block; + } + } } - - .o_composer_button_send, .o_chatter_avatar, .o_composer_button_add_attachment, .o_composer_attachments_list { + .o_composer_button_send, .o_chatter_avatar { display: none; } } @@ -197,8 +204,8 @@ // Emoji .o_mail_emoji { display: inline-block; - width: 24px; - height: 24px; + height: 20px; + width: 20px; > img { width: 100%; diff --git a/addons/mail/static/src/less/extended_chat_window.less b/addons/mail/static/src/less/extended_chat_window.less index 028befebdf9..3ae73dee861 100644 --- a/addons/mail/static/src/less/extended_chat_window.less +++ b/addons/mail/static/src/less/extended_chat_window.less @@ -24,4 +24,37 @@ display: none; } } + + .o_chat_composer { + .o_composer { + .o_composer_input { + width: 80%; + > textarea { + padding: 10px; + font-size: 13px; + } + @media (max-width: @screen-xs-max) { + width: 70%; + > textarea { + padding: 15px 10px; + } + } + } + .o_chatter_composer_tools { + .o-position-absolute(@right: 0px, @top: 0px); + button { + padding: 10px; + @media (max-width: @screen-xs-max) { + padding: 13px; + } + &.btn-icon { + font-size: 1.3em; + } + } + } + } + .o_composer_attachments_list { + margin: 0px; + } + } } diff --git a/addons/mail/static/src/less/systray.less b/addons/mail/static/src/less/systray.less index 1ecb50989aa..82e122b3a99 100644 --- a/addons/mail/static/src/less/systray.less +++ b/addons/mail/static/src/less/systray.less @@ -20,13 +20,11 @@ .o-flex-flow(column, nowrap); } .o_notification_counter { + .o-position-absolute(@top: 20%, @right: 1px); background: @odoo-brand-optional; color: white; padding: 0em 0.3em; font-size: 0.7em; - top: 19%; - position: absolute; - margin: 0px 0px 0px 6px; } .o_mail_navbar_dropdown { width: 350px; @@ -45,17 +43,16 @@ .o-flex(0, 0, auto); justify-content: space-between; border-bottom: 1px solid lightgray; - .o_filter_button, .o_new_message { .btn-link; padding: 5px; } .o_filter_button { color: @odoo-main-color-muted; - &:hover, &.o_selected { + &:hover, &.active { color: @odoo-brand-optional; } - &.o_selected { + &.active { cursor: default; font-weight: bold; } @@ -68,56 +65,28 @@ 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(); - color: @odoo-main-text-color; - } - .o_last_message_date { - .o-flex(0, 0, auto); - padding-top: 3px; - font-size: xx-small; - color: @odoo-main-color-muted; + @media (min-width: @screen-sm-min) { + .o_mail_channel_preview { + height: 50px; + padding: 5px; + .o_mail_channel_image { + width: 40px; + > img { + max-height: 40px; } } - .o_last_message_preview { - width: 100%; - max-height: 20px; - color: @odoo-main-color-muted; - .o-text-overflow(); + .o_channel_info { + margin-left: 10px; + .o_channel_title { + .o_channel_name, .o_channel_counter { + color: #666666; + } + .o_last_message_date { + padding-top: 2px; + font-size: x-small; + margin-left: 10px; + } + } } } } @@ -139,3 +108,36 @@ .o_no_chat_window .o_mail_navbar_dropdown .o_new_message { display: none; // hide 'new message' button if chat windows are disabled } + +// Mobile rules +// Goal: mock the design of Discuss in mobile +@media (max-width: @screen-xs-max) { + .o_mail_navbar_item { + .o_notification_counter { + top: 10%; + } + .o_mail_navbar_dropdown { + position: relative; + .o_mail_navbar_dropdown_top { + padding: 5px; + } + .o_mail_navbar_mobile_header { + padding: 5px; + height: 44px; + border-bottom: 1px solid #ebebeb; + } + .o_mail_navbar_dropdown_channels { + max-height: none; + padding-bottom: 52px; // leave space for tabs + } + .o_mail_mobile_tabs { + position: fixed; + bottom: 0px; + left: 0px; + right: 0px; + background-color: white; + color: @odoo-main-text-color; + } + } + } +} diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index 16e58a8e285..03bb984599a 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -11,6 +11,10 @@ .o_thread_date_separator { margin-top: 15px; margin-bottom: 30px; + @media (max-width: @screen-xs-max) { + margin-top: 0px; + margin-bottom: 15px; + } border-bottom: 1px solid @odoo-color-silver-darker; // cannot use gray-lighter-darker as this file is also frontend text-align: center; @@ -69,14 +73,12 @@ opacity: 0; } } - .o_thread_icons { + .o_thread_icon { cursor: pointer; - > .o_thread_icon { - opacity: 0; - &.fa-star { - opacity: @mail-thread-icon-opacity; - color: gold; - } + opacity: 0; + &.fa-star { + opacity: @mail-thread-icon-opacity; + color: gold; } } @@ -84,7 +86,7 @@ .o_thread_message_side_date { opacity: @mail-thread-side-date-opacity; } - .o_thread_icons > * { + .o_thread_icon { opacity: @mail-thread-icon-opacity; &:hover { opacity: 1; @@ -207,9 +209,7 @@ } .o_web_client.o_touch_device { - .o_thread_icons { - > .o_thread_icon { - opacity: @mail-thread-icon-opacity; - } + .o_mail_thread .o_thread_icon { + opacity: @mail-thread-icon-opacity; } } diff --git a/addons/mail/static/src/xml/chat_window.xml b/addons/mail/static/src/xml/chat_window.xml index a9117c76bd6..525898dabd8 100644 --- a/addons/mail/static/src/xml/chat_window.xml +++ b/addons/mail/static/src/xml/chat_window.xml @@ -41,12 +41,15 @@ </t> <t t-name="mail.ChatWindowHeaderContent"> + <span t-if="widget.isMobile"> + <a href="#" class="o_chat_window_close fa fa-1x fa-arrow-left mr4"/> + </span> <span class="o_chat_title"> <t t-if="status" t-call="mail.chat.UserStatus"/> <t t-esc="title"/> <span t-if="unread_counter"> (<t t-esc="unread_counter"/>)</span> </span> - <span class="o_chat_window_buttons"> + <span t-if="!widget.isMobile" class="o_chat_window_buttons"> <a href="#" class="o_chat_window_close fa fa-close"/> </span> </t> diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml index b4539dd7bb7..e27b70e40e5 100644 --- a/addons/mail/static/src/xml/client_action.xml +++ b/addons/mail/static/src/xml/client_action.xml @@ -17,13 +17,13 @@ </t> <t t-name="mail.chat.Sidebar"> <div class="o_mail_chat_sidebar"> - <div t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id == 'channel_inbox') ? 'o_active': ''}" + <div t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id === 'channel_inbox') ? 'o_active': ''}" data-channel-id="channel_inbox"> <span class="o_channel_name"><i class="fa fa-inbox mr8"/>Inbox</span> <t t-set="counter" t-value="needaction_counter"/> <t t-call="mail.chat.SidebarNeedaction"/> </div> - <div t-attf-class="o_mail_chat_title_main o_mail_chat_title_starred o_mail_chat_channel_item #{(active_channel_id == 'channel_starred') ? 'o_active': ''}" + <div t-attf-class="o_mail_chat_title_main o_mail_chat_title_starred o_mail_chat_channel_item #{(active_channel_id === 'channel_starred') ? 'o_active': ''}" data-channel-id="channel_starred"> <span class="o_channel_name"><i class="fa fa-star-o mr8"/>Starred</span> <t t-set="counter" t-value="starred_counter"/> @@ -130,12 +130,13 @@ <!-- 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" 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-default btn-sm o_mail_chat_button_mark_read hidden-xs" 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_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> + <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite hidden-xs" title="Invite people">Invite</button> + <button type="button" class="btn btn-default btn-sm o_mail_chat_button_mark_read" title="Mark all as read">Mark all read</button> + <button type="button" class="btn btn-default 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_unsubscribe hidden-xs" title="Unsubscribe from channel">Unsubscribe</button> + <button type="button" class="btn btn-default btn-sm o_mail_chat_button_dm visible-xs" title="New Message">New Message</button> + <button t-if="!disable_add_channel" type="button" class="btn btn-default btn-sm o_mail_chat_button_public o_mail_chat_button_private visible-xs" title="New Channel">New Channel</button> + <button t-if="debug" type="button" class="btn btn-default btn-sm o_mail_chat_button_settings" title="Open channel settings">Settings</button> </div> </t> @@ -143,4 +144,85 @@ <div t-name="mail.PartnerInviteDialog"> <input type="text" class="o_input o_mail_chat_partner_invite_input" id="mail_search_partners"/> </div> + + + <!-- Mobile templates --> + <t t-name="mail.client_action_mobile"> + <div class="o_mail_chat"> + <div class="o_mail_chat_mobile_control_panel"/> + <div class="o_mail_chat_mobile_inbox_buttons"> + <button type="button" class="btn btn-primary btn-sm visible-xs-inline o_channel_inbox_item" title="Inbox" data-type="channel_inbox"> + Inbox + </button><button type="button" class="btn btn-default btn-sm visible-xs-inline o_channel_inbox_item" title="Starred" data-type="channel_starred"> + Starred + </button> + </div> + <div class="o_mail_chat_content"/> + <div class="o_mail_mobile_tabs"> + <div class="o_mail_mobile_tab" data-type="channel_inbox"> + <span class="fa fa-inbox"/> + <span class="o_tab_title">Inbox</span> + </div> + <div class="o_mail_mobile_tab" data-type="dm"> + <span class="fa fa-user"/> + <span class="o_tab_title">Chat</span> + </div> + <div class="o_mail_mobile_tab" data-type="public"> + <span class="fa fa-users"/> + <span class="o_tab_title">Channels</span> + </div> + <div class="o_mail_mobile_tab" data-type="private"> + <span class="fa fa-eye-slash"/> + <span class="o_tab_title">Private Channels</span> + </div> + </div> + </div> + </t> + + <t t-name="mail.chat.MobileTabPane"> + <div class="o_mail_chat_tab_pane" t-att-data-type="type"> + <div t-if="!disable_add_channel" class="o_mail_add_channel" t-att-data-type="type"> + <span t-if="type == 'private' || type == 'public'">#</span> + <t t-set="input_placeholder" t-if="type == 'private' || type == 'public'">Add a channel</t> + <t t-set="input_placeholder" t-if="type == 'dm'">Open chat</t> + <input type="text" t-attf-placeholder="#{input_placeholder}"/> + </div> + <t t-foreach="channels" t-as="channel"> + <t t-call="mail.chat.ChannelPreview"/> + </t> + </div> + </t> + + <!-- The ChannelPreview template is used by the client action in mobile, and by the systray menu --> + <t t-name="mail.chat.ChannelPreview"> + <div t-attf-class="o_mail_channel_preview #{channel.unread_counter ? 'o_channel_unread' : ''}" + t-att-data-channel_id="channel.id" t-att-data-res_id="channel.res_id" t-att-data-res_model="channel.model"> + <div class="o_mail_channel_image"> + <img class="o_mail_channel_image" t-att-src="channel.image_src"/> + <i t-if="channel.status === 'online'" class="o_mail_user_status o_user_online fa fa-circle" title="Online"/> + <i t-if="channel.status === 'away'" class="o_mail_user_status o_user_idle fa fa-circle" title="Idle"/> + </div> + <div class="o_channel_info"> + <div class="o_channel_title"> + <span class="o_channel_name"> + <t t-esc="channel.name"/> + </span> + <span class="o_channel_counter"> + <t t-if="channel.unread_counter">&nbsp;(<t t-esc="channel.unread_counter"/>)</t> + </span> + <span class="o_last_message_date"> <t t-esc="channel.last_message_date"/> </span> + </div> + <div t-if="channel.last_message" class="o_last_message_preview"> + <t t-if="channel.last_message.is_author"> + <span class="fa fa-mail-reply"/> You: + </t> + <t t-else=""> + <t t-esc="channel.last_message.displayed_author"/>: + </t> + <t t-raw="channel.last_message_preview"/> + </div> + </div> + </div> + </t> + </templates> diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 68c2730707e..a7b2e7cb02f 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -13,13 +13,14 @@ <div class="o_chatter_composer_tools"> <button tabindex="4" class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/> <button tabindex="5" class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/> + <button t-if="widget.options.isMobile" tabindex="3" class="btn btn-sm btn-icon fa fa-paper-plane-o o_composer_button_send" type="button"/> </div> </div> </div> <div class="o_composer_attachments_list"/> </div> <div class="o_composer_send"> - <button tabindex="3" class="btn btn-sm btn-primary o_composer_button_send" type="button"><t t-esc="widget.options.send_text"/></button> + <button tabindex="3" class="btn btn-sm btn-primary o_composer_button_send hidden-xs" type="button"><t t-esc="widget.options.send_text"/></button> </div> <span class="hide"> <t t-call="HiddenInputFile"> diff --git a/addons/mail/static/src/xml/extended_chat_window.xml b/addons/mail/static/src/xml/extended_chat_window.xml index 5548688d462..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 @@ </t> <t t-extend="mail.ChatWindowHeaderContent"> - <t t-jquery=".o_chat_window_close" t-operation="before"> - <a href="#" class="o_chat_window_expand fa fa-expand" title=" - Open in Discuss"/> + <t t-jquery=".o_chat_window_buttons" t-operation="prepend"> + <a href="#" class="o_chat_window_expand fa fa-expand" title="Open in Discuss"/> </t> </t> diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml index 8e7e9b161d4..653faa4dc6d 100644 --- a/addons/mail/static/src/xml/systray.xml +++ b/addons/mail/static/src/xml/systray.xml @@ -7,15 +7,36 @@ <i class="fa fa-comments"/> <span class="o_notification_counter badge"/> </a> <ul class="o_mail_navbar_dropdown dropdown-menu" role="menu"> - <li class="o_mail_navbar_dropdown_top"> + <li t-if="widget.isMobile"> + <div class="o_mail_navbar_mobile_header"> + <button type="button" class="btn btn-default btn-sm o_new_message"> New message </button> + </div> + </li> + <li t-if="!widget.isMobile" 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 active"> 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 t-if="widget.isMobile"> + <div class="o_mail_mobile_tabs"> + <div class="o_mail_mobile_tab o_filter_button active"> + <span class="fa fa-envelope"/> + <span class="o_tab_title">All</span> + </div> + <div class="o_mail_mobile_tab o_filter_button" data-filter='chat'> + <span class="fa fa-user"/> + <span class="o_tab_title">Chat</span> + </div> + <div class="o_mail_mobile_tab o_filter_button" data-filter='channels'> + <span class="fa fa-users"/> + <span class="o_tab_title">Channels</span> + </div> + </div> + </li> </ul> </li> </t> @@ -27,26 +48,7 @@ </li> </t> <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" t-att-data-res_id="channel.res_id" t-att-data-res_model="channel.model"> - <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-esc="channel.last_message.displayed_author"/>: - <t t-raw="channel.last_message_preview"/> - </div> - </div> - </div> + <t t-call="mail.chat.ChannelPreview"/> </t> </t> @@ -58,7 +60,9 @@ </t> <t t-foreach="activities" t-as="activity"> <div class="o_mail_channel_preview" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='my'> - <img class="o_channel_image" t-att-src="activity.icon"/> + <div class="o_mail_channel_image"> + <img t-att-src="activity.icon"/> + </div> <div class="o_channel_info"> <div class="o_channel_title"> <span class="o_channel_name"> @@ -70,8 +74,8 @@ <span t-if="!activity.overdue_count" class="o_no_activity mr16">0 Late </span> <button t-if="activity.today_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='today'> <t t-esc="activity.today_count"/> Today </button> <span t-if="!activity.today_count" class="o_no_activity mr16">0 Today </span> - <button t-if="activity.planned_count" type="button" class="btn btn-link o_activity_filter_button pull-right mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='upcoming_all'> <t t-esc="activity.planned_count"/> Future </button> - <span t-if="!activity.planned_count" class="o_no_activity pull-right mr16">0 Future</span> + <button t-if="activity.planned_count" type="button" class="btn btn-link o_activity_filter_button pull-right" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='upcoming_all'> <t t-esc="activity.planned_count"/> Future </button> + <span t-if="!activity.planned_count" class="o_no_activity pull-right">0 Future</span> </div> </div> </div> diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 8237658e99e..89f0921a6cc 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -3,10 +3,10 @@ <t t-name="mail.ChatThread"> <t t-if="messages.length"> - <t t-if="options.display_load_more && options.display_order == ORDER.ASC" + <t t-if="options.display_load_more and options.display_order == ORDER.ASC" t-call="mail.ChatThread.LoadMore"/> <t t-call="mail.ChatThread.Content"/> - <t t-if="options.display_load_more && options.display_order == ORDER.DESC" + <t t-if="options.display_load_more and options.display_order == ORDER.DESC" t-call="mail.ChatThread.LoadMore"/> </t> <t t-if="options.display_empty_channel"> @@ -133,23 +133,23 @@ <t t-name="mail.ChatThread.Message"> <div t-att-class="'o_thread_message' + (message.expanded ? ' o_message_expanded ' : ' ') + (message.is_selected ? 'o_thread_selected_message' : '')" t-att-data-message-id="message.id"> <div t-if="options.display_avatar" class="o_thread_message_sidebar"> - <t t-if="!message.mailto && message.author_id[0]"> - <img t-if="message.avatar_src && message.display_author" + <t t-if="!message.mailto and message.author_id[0]"> + <img t-if="message.avatar_src and message.display_author" t-att-src="message.avatar_src" data-oe-model="res.partner" t-att-data-oe-id="message.author_redirect ? message.author_id[0] : ''" t-attf-class="o_thread_message_avatar img-circle #{message.author_redirect ? 'o_mail_redirect' : ''}"/> </t> <t t-if="message.mailto || !message.author_id[0]"> - <img t-if="message.avatar_src && message.display_author" + <img t-if="message.avatar_src and message.display_author" t-att-src="message.avatar_src" class="o_thread_message_avatar img-circle"/> </t> <span t-if="!message.display_author" t-att-title="message.date" class="o_thread_message_side_date"> <t t-esc="message.date.format('hh:mm')"/> </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')" + <i t-if="!message.display_author and options.display_stars and message.message_type != 'notification'" + t-att-class="'fa o_thread_message_star o_thread_icon ' + (message.is_starred ? 'fa-star' : 'fa-star-o')" 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' : '')"> @@ -163,23 +163,23 @@ <t t-esc="message.mailto"/> </a> </strong> - <strong t-if="!message.mailto && message.author_id[0]" + <strong t-if="!message.mailto and message.author_id[0]" data-oe-model="res.partner" t-att-data-oe-id="message.author_redirect ? message.author_id[0] : ''" - t-attf-class="#{message.author_redirect ? 'o_mail_redirect' : ''}"> + t-attf-class="o_thread_author #{message.author_redirect ? 'o_mail_redirect' : ''}"> <t t-esc="message.displayed_author"/> </strong> - <strong t-if="!message.mailto && !message.author_id[0]"> + <strong t-if="!message.mailto and !message.author_id[0]" class="o_thread_author"> <t t-esc="message.displayed_author"/> </strong> - <small class="o_mail_timestamp" t-att-title="message.date"><t t-esc="message.hour"/></small> - <t t-if="message.model && (message.model != 'mail.channel') && options.display_document_link"> - on <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id"><t t-esc="message.record_name"/></a> + <t t-if="message.model and (message.model != 'mail.channel') and options.display_document_link"> + <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link">on <t t-esc="message.record_name"/></a> </t> - <t t-if="message.origin_id && (message.origin_id !== options.channel_id)"> + <t t-if="message.origin_id and (message.origin_id !== options.channel_id)"> (from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>) </t> - <span t-if="options.display_email_icon && message.customer_email_data && message.customer_email_data.length" class="o_thread_tooltip_container"> + <span t-if="options.display_email_icon and message.customer_email_data and message.customer_email_data.length" class="o_thread_tooltip_container"> <i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope-o'"/> <span class="o_thread_tooltip_content"> <t t-foreach="message.customer_email_data" t-as="customer"> @@ -206,19 +206,21 @@ 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"> - <t t-if="message.subtype_description"> - <p t-esc="message.subtype_description"/> + <div class="o_thread_message_content"> + <t t-if="message.tracking_value_ids and message.tracking_value_ids.length > 0"> + <t t-if="message.subtype_description"> + <p><t t-esc="message.subtype_description"/></p> + </t> + <t t-call="mail.ChatThread.MessageTracking"/> </t> - <t t-call="mail.ChatThread.MessageTracking"/> - </t> - <p t-if="options.display_subject && message.display_subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p> - <t t-if="!(message.tracking_value_ids && message.tracking_value_ids.length > 0)"> - <t t-raw="message.body"/> - </t> - <t t-foreach="message.attachment_ids" t-as="attachment"> - <t t-call="mail.Attachment"/> - </t> + <p t-if="options.display_subject and message.display_subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p> + <t t-if="!(message.tracking_value_ids and message.tracking_value_ids.length > 0)"> + <t t-raw="message.body"/> + </t> + <t t-foreach="message.attachment_ids" t-as="attachment"> + <t t-call="mail.Attachment"/> + </t> + </div> </div> </div> <t t-if="options.messages_separator_position == message.id"> @@ -329,5 +331,4 @@ <button class="btn btn-sm btn-link">-------- Show older messages --------</button> </div> </t> - </templates> diff --git a/addons/mail/static/tests/client_action_tests.js b/addons/mail/static/tests/client_action_tests.js new file mode 100644 index 00000000000..84bcc590328 --- /dev/null +++ b/addons/mail/static/tests/client_action_tests.js @@ -0,0 +1,92 @@ +odoo.define('mail.client_action_test', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); +var Widget = require('web.Widget'); + +var ChatAction = require("mail.chat_client_action"); + +QUnit.module('mail', {}, function () { + +QUnit.module('Discuss client action', { + beforeEach: function () { + this.data = { + 'mail.message': { + fields: {}, + }, + }; + this.createChatAction = function (params) { + var parent = new Widget(); + testUtils.addMockEnvironment(parent, { + data: this.data, + archs: { + 'mail.message,false,search': '<search/>', + }, + config: { + isMobile: true + }, + }); + var chatAction = new ChatAction(parent, params); + chatAction.set_cp_bus(new Widget()); + chatAction.appendTo($('#qunit-fixture')); + + return chatAction; + }; + + }, +}); + +QUnit.skip('mobile basic rendering', function (assert) { + // Unfortunately, this test is skipped for now because there is no way to + // execute the whole test suite in mobile (it only works test by test), so + // as the client action include for mobile is rejected when we are not in + // mobile, it isn't possible to test it + // Moreover, RPCs done by the chat_manager (e.g. message_fetch) should be + // properly mocked. + assert.expect(11); + + var parent = new Widget(); + testUtils.addMockEnvironment(parent, { + data: this.data, + archs: { + 'mail.message,false,search': '<search/>', + }, + config: { + isMobile: true, + }, + }); + + var params = { + id: 1, + context: {}, + params: {}, + }; + var chatAction = this.createChatAction(params); + + // test basic rendering in mobile + assert.equal(chatAction.$(".o_mail_chat_mobile_control_panel").length, 1, "Mobile control panel created"); + assert.equal(chatAction.$(".o_mail_mobile_tab").length, 4, "Four mobile tabs created"); + assert.equal(chatAction.$('.o_mail_chat_content').length, 1, "One default chat content pane created"); + assert.equal(chatAction.$(".o_mail_chat_tab_pane").length, 3, "Three mobile tab panes created"); + + // Inbox + assert.equal(chatAction.activeMobileTab, "channel_inbox", "'channel_inbox' is default active tab"); + assert.ok(chatAction.$(".o_channel_inbox_item:nth(0)").hasClass("btn-primary"), "Showing 'Inbox'"); + + // Starred + chatAction.$(".o_channel_inbox_item[data-type='channel_starred']").click(); + assert.ok(chatAction.$(".o_channel_inbox_item:nth(1)").hasClass("btn-primary"), "Clicked on 'Starred'"); + + assert.ok(chatAction.$(".o_mail_chat_content").is(":visible"), "Default main content pane visible"); + + chatAction.$(".o_mail_mobile_tab[data-type='dm']").click(); + assert.equal(chatAction.activeMobileTab, "dm", "After click on 'Conversation', is now active tab"); + + assert.ok(!chatAction.$(".o_mail_chat_content").is(":visible"), "none", "'Main' content pane is invisible"); + assert.ok(chatAction.$(".o_mail_chat_tab_pane:nth(0)").is(":visible"), "'Conversation' pane is visible"); + + chatAction.destroy(); +}); + +}); +}); diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 2fe052975c3..78e1d4433b1 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -6,6 +6,7 @@ <script type="text/javascript" src="/mail/static/src/js/many2many_tags_email.js"></script> <script type="text/javascript" src="/mail/static/src/js/client_action.js"></script> + <script type="text/javascript" src="/mail/static/src/js/client_action_mobile.js"></script> <script type="text/javascript" src="/mail/static/src/js/chat_window.js"></script> <script type="text/javascript" src="/mail/static/src/js/extended_chat_window.js"></script> <script type="text/javascript" src="/mail/static/src/js/document_viewer.js"></script> @@ -40,6 +41,7 @@ <template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite"> <xpath expr="//t[@t-set='head']" position="inside"> + <script type="text/javascript" src="/mail/static/tests/client_action_tests.js"></script> <script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script> <script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script> <script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script> |
