diff --git a/addons/im_livechat/static/src/xml/im_livechat_backend.xml b/addons/im_livechat/static/src/xml/im_livechat_backend.xml index e686a55c08e..3240bc63447 100644 --- a/addons/im_livechat/static/src/xml/im_livechat_backend.xml +++ b/addons/im_livechat/static/src/xml/im_livechat_backend.xml @@ -7,9 +7,20 @@ Livechat + fa-comments + + + +
+ + Livechat +
+
+
+ diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 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 = _('
created #%s
') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") + channel_info = new_channel.channel_info('creation')[0] self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 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 $('').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 %s 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, - }); - }, + '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', }, - 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(); - }, - - 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: $("
"), - 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.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); - 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(); - }); + 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()); - 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($("
")).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)) + 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,226 +186,62 @@ 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(); - } - this.extended_composer.do_show(); - - this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true}); - this.extended_composer.focus('body'); - }, - - 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); - - 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; - }, - - renderSidebar: 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(''+_t("Create %s")+'', '"#'+self.last_search_val+'"'), - 'value': '_create', - }); - 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); - } - } - }, - 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"); - } + /** + * @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, }); }, - - _renderSidebar: function (options) { - return $(QWeb.render("mail.chat.Sidebar", options)); + /** + * @override + */ + destroy: function () { + this.$buttons.off().destroy(); + this._super.apply(this, arguments); }, - - render_snackbar: function (template, context, timeout) { - if (this.$snackbar) { - this.$snackbar.remove(); - } - timeout = timeout || 20000; - this.$snackbar = $(QWeb.render(template, context)); - 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); - }, - - do_search_channel: function(search_val){ - return this._rpc({ - model: 'mail.channel', - method: 'channel_search_to_join', - args: [search_val] - }) - .then(function(result){ - var values = []; - _.each(result, function(channel){ - var escaped_name = _.escape(channel.name); - values.push(_.extend(channel, { - 'value': escaped_name, - 'label': escaped_name, - })); - }); - return values; - }); - }, - - 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); - } - }, - - set_channel: function (channel) { - var self = this; - // Store scroll position of previous channel + /** + * @override + */ + on_attach_callback: function () { + chat_manager.bus.trigger('client_action_open', true); if (this.channel) { - this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); - this.storeComposerState(this.channel); + this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]}); } - var new_channel_scrolltop = this.channels_scrolltop[channel.id]; - - 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; - if (this.$snackbar) { - this.$snackbar.remove(); - } - - this.action.context.active_id = channel.id; - this.action.context.active_ids = [channel.id]; - - return this.fetch_and_render_thread().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}); - } - - // 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(); - } - - // Display and focus the adequate composer, and unselect possibly selected message - // to prevent sending messages as reply to that message - self.unselect_message(); - - self.action_manager.do_push_state({ - action: self.action.id, - active_id: self.channel.id, - }); - }); + }, + /** + * @override + */ + on_detach_callback: function () { + chat_manager.bus.trigger('client_action_open', false); + this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop(); }, - get_thread_rendering_options: function (messages) { + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @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)) { @@ -522,16 +265,375 @@ var ChatAction = Widget.extend(ControlPanelMixin, { display_reply_icon: true, }; }, - - fetch_and_render_thread: function () { + /** + * @private + * @returns {Deferred} + */ + _loadMoreMessages: 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 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(''+_t("Create %s")+'', '"#'+self.last_search_val+'"'), + 'value': '_create', + }); + 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); + } + } + }, + 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"); + } + }); + } 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: $("
"), + action: this.action, + disable_groupby: true, + }; + this.searchview = new SearchView(this, this.dataset, this.fields_view, options); + return this.searchview.appendTo($("
")).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)); + }, + /** + * @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(); + } + timeout = timeout || 20000; + this.$snackbar = $(QWeb.render(template, context)); + 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); + }, + /** + * Renders, binds events and appends a thread widget. + * + * @private + * @returns {Deferred} + */ + _renderThread: function () { + this.thread = new ChatThread(this, {display_help: true}); - update_button_status: function (disabled, type) { + 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); + + 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} + */ + _searchChannel: function (search_val){ + return this._rpc({ + model: 'mail.channel', + method: 'channel_search_to_join', + args: [search_val] + }) + .then(function (result){ + var values = []; + _.each(result, function (channel){ + var escaped_name = _.escape(channel.name); + values.push(_.extend(channel, { + 'value': escaped_name, + 'label': escaped_name, + })); + }); + 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); + + if (this.channel.type !== 'static') { + this.basic_composer.do_hide(); + } + this.extended_composer.do_show(); + + 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 and composer state of the previous channel + this._storeChannelState(); + + this.channel = 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; + if (this.$snackbar) { + this.$snackbar.remove(); + } + + this.action.context.active_id = channel.id; + this.action.context.active_ids = [channel.id]; + + 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); + } + + // 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.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._unselectMessage(); + + self.action_manager.do_push_state({ + action: self.action.id, + active_id: self.channel.id, + }); + }); + }, + /** + * 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); + } + }, + /** + * @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); + + 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; + 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); + }); + }, + /** + * @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"); - 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, - }); + this.$('.o_mail_chat_channel_item') + .removeClass('o_active') + .filter('[data-channel-id=' + channel.id + ']') + .removeClass('o_unread_message') + .addClass('o_active'); }, - on_post_message: function (message) { + //-------------------------------------------------------------------------- + // 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); + } + + 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,91 +838,63 @@ 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); - } - }, - on_channel_unsubscribed: function (channel_id) { - if (this.channel.id === channel_id) { - this.set_channel(chat_manager.get_channel("channel_inbox")); - } - 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 () { + /** + * @private + */ + _onInviteButtonClicked: function () { var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name); new PartnerInviteDialog(this, title, this.channel.id).open(); }, - - on_click_button_unsubscribe: function () { - chat_manager.unsubscribe(this.channel); + /** + * @private + * @param {KeyEvent} event + */ + _onKeydown: function (event) { + if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) { + this._unselectMessage(); + } }, - on_click_button_settings: function() { - this.do_action({ - type: 'ir.actions.act_window', - res_model: "mail.channel", - res_id: this.channel.id, - views: [[false, 'form']], - target: 'current' - }); + /** + * @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.')); + } + }); + } }, - destroy: function() { - this.$buttons.off().destroy(); - this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - /** * @private * @param {OdooEvent} @@ -712,11 +906,45 @@ var ChatAction = Widget.extend(ControlPanelMixin, { contexts: [session.user_context], }); this.domain = result.domain; - this.fetch_and_render_thread(); + this._fetchAndRenderThread(); + }, + /** + * @private + */ + _onSettingsButtonClicked: function () { + this.do_action({ + type: 'ir.actions.act_window', + res_model: "mail.channel", + res_id: this.channel.id, + views: [[false, 'form']], + target: 'current' + }); + }, + /** + * @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)); + }, + /** + * @private + */ + _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 = $(' - - - - - + + + + + + +
@@ -143,4 +144,85 @@
+ + + + +
+
+
+ +
+
+
+
+ + Inbox +
+
+ + Chat +
+
+ + Channels +
+
+ + Private Channels +
+
+
+ + + +
+
+ # + Add a channel + Open chat + +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + + +  () + + +
+
+ + You: + + + : + + +
+
+
+
+ diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 68c2730707e..a7b2e7cb02f 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -13,13 +13,14 @@
- +
diff --git a/addons/mail/static/src/xml/extended_chat_window.xml b/addons/mail/static/src/xml/extended_chat_window.xml index 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 @@ - -
+ + 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 @@ @@ -27,26 +48,7 @@ -
- -
-
- - - - - - () - - -
-
- : - -
-
-
+
@@ -58,7 +60,9 @@
- +
+ +
@@ -70,8 +74,8 @@ 0 Late 0 Today - - 0 Future + + 0 Future
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 @@ - - @@ -133,23 +133,23 @@
- - + - -
@@ -163,23 +163,23 @@ - + t-attf-class="o_thread_author #{message.author_redirect ? 'o_mail_redirect' : ''}"> - + - - - on + + on - + (from #) - + @@ -206,19 +206,21 @@ t-att-data-message-id="message.id" title="Mark as Read"/>

- - -

+

+ + +

+
+ - - -

Subject:

- - - - - - +

Subject:

+ + + + + + +
@@ -329,5 +331,4 @@
- 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': '', + }, + 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': '', + }, + 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 @@ + @@ -40,6 +41,7 @@