summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2017-09-15 15:25:16 +0200
committerAaron Bohy <aab@odoo.com>2017-09-15 15:25:16 +0200
commit13a44d33bda1d7ff21e3de5de8b3c84f6a482a0d (patch)
treebc30e753ecc419f93a7f156d10b557ffb9e4c6c1
parenta8e7d096b85709ccf52411c450d0574f9238fd6f (diff)
parent7a91d87614f9a72df1d81ee28a5f425517e9b218 (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
-rw-r--r--addons/im_livechat/static/src/xml/im_livechat_backend.xml11
-rw-r--r--addons/mail/models/mail_channel.py9
-rw-r--r--addons/mail/static/src/js/chat_manager.js40
-rw-r--r--addons/mail/static/src/js/chat_window.js6
-rw-r--r--addons/mail/static/src/js/client_action.js1118
-rw-r--r--addons/mail/static/src/js/client_action_mobile.js283
-rw-r--r--addons/mail/static/src/js/composer.js2
-rw-r--r--addons/mail/static/src/js/systray.js39
-rw-r--r--addons/mail/static/src/js/window_manager.js1
-rw-r--r--addons/mail/static/src/less/chat_window.less36
-rw-r--r--addons/mail/static/src/less/client_action.less230
-rw-r--r--addons/mail/static/src/less/composer.less19
-rw-r--r--addons/mail/static/src/less/extended_chat_window.less33
-rw-r--r--addons/mail/static/src/less/systray.less110
-rw-r--r--addons/mail/static/src/less/thread.less24
-rw-r--r--addons/mail/static/src/xml/chat_window.xml5
-rw-r--r--addons/mail/static/src/xml/client_action.xml98
-rw-r--r--addons/mail/static/src/xml/composer.xml3
-rw-r--r--addons/mail/static/src/xml/extended_chat_window.xml5
-rw-r--r--addons/mail/static/src/xml/systray.xml54
-rw-r--r--addons/mail/static/src/xml/thread.xml55
-rw-r--r--addons/mail/static/tests/client_action_tests.js92
-rw-r--r--addons/mail/views/mail_templates.xml2
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">&amp;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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; message.author_id[0]">
- <img t-if="message.avatar_src &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; options.display_stars &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; (message.model != 'mail.channel') &amp;&amp; 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 &amp;&amp; (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 &amp;&amp; message.customer_email_data &amp;&amp; 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 &amp;&amp; message.tracking_value_ids.length &gt; 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 &gt; 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 &amp;&amp; message.display_subject" class="o_mail_subject">Subject: <t t-esc="message.subject"/></p>
- <t t-if="!(message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 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 &gt; 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>