summaryrefslogtreecommitdiff
path: root/addons/mail/static/src
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2015-09-30 14:12:19 +0200
committerGéry Debongnie <ged@odoo.com>2015-10-01 14:00:18 +0200
commit62be162877f30a6ca0f61b38f0b070d56f7ab419 (patch)
tree1d2f750c3ce7f29bd0547acdc98afa160a827f04 /addons/mail/static/src
parent512287cac1fb7d4da632a4be0922679b203aff66 (diff)
[REF] mail: second part of the chat refactoring (client side)
Joint work with aab
Diffstat (limited to 'addons/mail/static/src')
-rw-r--r--addons/mail/static/src/js/chat_window.js77
-rw-r--r--addons/mail/static/src/js/client_action.js461
-rw-r--r--addons/mail/static/src/js/systray.js97
-rw-r--r--addons/mail/static/src/js/window_manager.js115
-rw-r--r--addons/mail/static/src/less/chat_window.less50
-rw-r--r--addons/mail/static/src/less/client_action.less188
-rw-r--r--addons/mail/static/src/xml/chat_window.xml24
-rw-r--r--addons/mail/static/src/xml/client_action.xml97
-rw-r--r--addons/mail/static/src/xml/systray.xml20
9 files changed, 1129 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js
new file mode 100644
index 00000000000..c9ca3675054
--- /dev/null
+++ b/addons/mail/static/src/js/chat_window.js
@@ -0,0 +1,77 @@
+odoo.define('mail.ChatWindow', function (require) {
+"use strict";
+
+var ChatThread = require('mail.ChatThread');
+
+var core = require('web.core');
+var Widget = require('web.Widget');
+
+var _t = core._t;
+
+return Widget.extend({
+ template: "mail.ChatWindow",
+ events: {
+ "keydown .o_chat_input": "on_keydown",
+ "click .o_chat_window_close": "on_click_close",
+ "click .o_chat_title": "on_click_fold",
+ },
+
+ init: function (parent, channel_id, title, is_folded) {
+ this._super(parent);
+ this.title = title;
+ this.channel_id = channel_id;
+ this.placeholder = _t("Say something");
+ this.folded = is_folded;
+ },
+ start: function () {
+ this.$content = this.$('.o_chat_content');
+ this.$input = this.$('.o_chat_input input');
+
+ this.thread = new ChatThread(this, {display_avatar: false});
+ this.thread.on('toggle_star_status', this, function (message_id) {
+ this.trigger('toggle_star_status', message_id);
+ });
+
+ this.fold();
+ var def = this.thread.appendTo(this.$content);
+ return $.when(this._super(), def);
+ },
+ render: function (messages) {
+ this.thread.render(messages, {display_load_more: false});
+ },
+ scrollBottom: function () {
+ this.$content.scrollTo('max');
+ },
+ fold: function () {
+ this.$el.animate({
+ height: this.folded ? "28px" : "333px"
+ });
+ },
+ toggle_fold: function () {
+ this.folded = !this.folded;
+ this.fold();
+ },
+ on_keydown: function (event) {
+ if (event.which === $.ui.keyCode.ENTER) {
+ var message = {
+ content: this.$input.val(),
+ attachment_ids: [],
+ partner_ids: [],
+ channel_id: this.channel_id,
+ };
+ this.$input.val('');
+ this.trigger('post_message', message);
+ }
+ },
+ on_click_close: function (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.trigger("close_chat_session");
+ },
+ on_click_fold: function () {
+ this.trigger("fold_channel", this.channel_id);
+ this.toggle_fold();
+ },
+});
+
+});
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
new file mode 100644
index 00000000000..a5797b8f58d
--- /dev/null
+++ b/addons/mail/static/src/js/client_action.js
@@ -0,0 +1,461 @@
+odoo.define('mail.chat_client_action', function (require) {
+"use strict";
+
+var chat_manager = require('mail.chat_manager');
+var ChatComposer = require('mail.ChatComposer');
+var ChatThread = require('mail.ChatThread');
+
+var ControlPanelMixin = require('web.ControlPanelMixin');
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var Model = require('web.Model');
+
+var pyeval = require('web.pyeval');
+var SearchView = require('web.SearchView');
+var Sidebar = require('web.Sidebar');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+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.
+ **/
+var PartnerInviteDialog = Dialog.extend({
+ dialog_title: _t('Invite people'),
+ template: "mail.PartnerInviteDialog",
+ init: function(parent, title, channel_id){
+ this.channel_id = channel_id;
+
+ this._super(parent, {
+ title: title,
+ size: "medium",
+ buttons: [{
+ text: _t("Invite"),
+ close: true,
+ classes: "btn-primary",
+ click: _.bind(this.on_click_add, this),
+ }],
+ });
+ this.PartnersModel = new Model('res.partner');
+ },
+ start: function(){
+ var self = this;
+ this.$input = this.$('.o_mail_chat_partner_invite_input');
+ this.$input.select2({
+ width: '100%',
+ allowClear: true,
+ multiple: true,
+ formatResult: function(item){
+ var css_class = "fa-circle" + (item.im_status === 'online' ? "" : "-o");
+ return $('<span class="fa">').addClass(css_class).text(item.text);
+ },
+ query: function (query) {
+ self.PartnersModel.call('im_search', [query.term, 20]).then(function(result){
+ var data = [];
+ _.each(result, function(partner){
+ partner.text = partner.name;
+ data.push(partner);
+ });
+ query.callback({results: data});
+ });
+ }
+ });
+ return this._super.apply(this, arguments);
+ },
+ on_click_add: function(){
+ var self = this;
+ var data = this.$input.select2('data');
+ if(data.length >= 1){
+ var ChannelModel = new Model('mail.channel');
+ return ChannelModel.call('channel_invite', [], {ids : [this.channel_id], partner_ids: _.pluck(data, 'id')})
+ .then(function(){
+ var names = _.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);
+ });
+ }
+ },
+});
+
+var ChatAction = Widget.extend(ControlPanelMixin, {
+ template: 'mail.client_action',
+
+ events: {
+ "click .o_mail_chat_channel_item": function (event) {
+ event.preventDefault();
+ var channel_id = this.$(event.currentTarget).data('channel-id');
+ this.set_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 self = this;
+ var channel_id = $(event.target).data("channel-id");
+ chat_manager.unsubscribe(channel_id).then(function () {
+ self.render_sidebar();
+ self.set_channel("channel_inbox");
+ });
+ },
+ },
+
+ on_attach_callback: function () {
+ this.thread.scroll_to({offset: this.channels_scrolltop[this.channel_id]});
+ },
+ on_detach_callback: function () {
+ this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop();
+ },
+
+ init: function(parent, action) {
+ this._super.apply(this, arguments);
+ this.action_manager = parent;
+ this.domain = [];
+ this.action = action;
+ this.channels_scrolltop = {};
+ },
+
+ start: function() {
+ var self = this;
+
+ // create searchview
+ var options = {
+ $buttons: $("<div>"),
+ action: this.action,
+ disable_groupby: true,
+ };
+ var dataset = new data.DataSetSearch(this, 'mail.message');
+ var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
+ var default_channel = this.action.context.active_id || this.action.params.default_active_id || 'channel_inbox';
+ this.searchview = new SearchView(this, dataset, view_id, {}, options);
+ this.searchview.on('search_data', this, this.on_search);
+
+ this.sidebar = new Sidebar(this, {
+ sections: [
+ {name: 'action', label: _t('Action')},
+ ],
+ items: {
+ action: [
+ {label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe},
+ {label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings},
+ ],
+ },
+ });
+
+ this.composer = new ChatComposer(this);
+ this.thread = new ChatThread(this, {
+ no_content_helper: this.action.help,
+ });
+
+ 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_detach', this.on_click_button_detach);
+
+ this.thread.on('redirect', this, this.on_redirect);
+ 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.composer.on('post_message', this, this.on_post_message);
+
+ var def1 = this.thread.prependTo(this.$('.o_mail_chat_content'));
+ var def2 = this.composer.appendTo(this.$('.o_mail_chat_content'));
+ var def3 = this.searchview.appendTo($("<div>"));
+ var def4 = this.sidebar.appendTo($("<div>"));
+
+ this.render_sidebar();
+
+ return $.when(def1, def2, def3, def4)
+ .then(this.set_channel.bind(this, default_channel))
+ .then(function () {
+ 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;
+ });
+ });
+ },
+
+ render_sidebar: function () {
+ var self = this;
+ var $sidebar = QWeb.render("mail.chat.Sidebar", {
+ channels: chat_manager.get_channels(),
+ needaction_counter: chat_manager.get_needaction_counter()
+ });
+ this.$(".o_mail_chat_sidebar").replaceWith($sidebar);
+
+ this.$('.o_mail_add_channel[data-type=public]').find("input").autocomplete({
+ source: function(request, response) {
+ self.last_search_val = request.term;
+ self.do_search_channel(request.term).done(function(result){
+ result.push({
+ 'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"'+self.last_search_val+'"</em>'),
+ 'value': '_create',
+ });
+ response(result);
+ });
+ },
+ select: function(event, ui) {
+ if (self.last_search_val) {
+ if (ui.item.value === '_create') {
+ chat_manager.create_channel(self.last_search_val, "public");
+ } else {
+ chat_manager.join_channel(ui.item.id);
+ }
+ }
+ },
+ focus: function(event) {
+ event.preventDefault();
+ },
+ html: true,
+ });
+
+ this.$('.o_mail_add_channel[data-type=dm]').find("input").autocomplete({
+ source: function(request, response) {
+ self.last_search_val = request.term;
+ self.do_search_partner(request.term).done(function(result){
+ response(result);
+ });
+ },
+ select: function(event, ui) {
+ var partner_id = ui.item.id;
+ chat_manager.create_channel(partner_id, "dm");
+ },
+ focus: function(event) {
+ event.preventDefault();
+ },
+ html: true,
+ });
+
+ this.$('.o_mail_add_channel[data-type=private]').find("input").on('keyup', this, function (event) {
+ var name = $(event.target).val();
+ if(event.which === $.ui.keyCode.ENTER && name) {
+ chat_manager.create_channel(name, "private");
+ }
+ });
+ },
+
+ do_search_channel: function(search_val){
+ var Channel = new Model("mail.channel");
+ return Channel.call('channel_search_to_join', [search_val]).then(function(result){
+ var values = [];
+ _.each(result, function(channel){
+ values.push(_.extend(channel, {
+ 'value': channel.name,
+ 'label': channel.name,
+ }));
+ });
+ return values;
+ });
+ },
+
+ do_search_partner: function (search_val) {
+ var Partner = new Model("res.partner");
+ return Partner.call('im_search', [search_val, 20]).then(function(result){
+ var values = [];
+ _.each(result, function(user){
+ values.push(_.extend(user, {
+ 'value': user.name,
+ 'label': user.name,
+ }));
+ });
+ return values;
+ });
+ },
+
+ set_channel: function (channel_id) {
+ var self = this;
+ // Store scroll position of previous channel
+ if (this.channel_id) {
+ this.channels_scrolltop[this.channel_id] = this.thread.get_scrolltop();
+ }
+ var new_channel_scrolltop = this.channels_scrolltop[channel_id];
+ var channel = chat_manager.get_channel(channel_id);
+ channel_id = channel.id;
+ this.channel_id = channel_id;
+ this.set("title", channel.name);
+ this.$buttons.toggle(channel.type !== "static");
+ this.$buttons
+ .find('.o_mail_chat_button_invite')
+ .toggle(channel.type !== "dm");
+
+ this.update_cp();
+ this.action.context.active_id = channel_id;
+ this.action.context.active_ids = [channel_id];
+
+ this.$('.o_mail_chat_channel_item')
+ .removeClass('o_active')
+ .filter('[data-channel-id=' + channel_id + ']')
+ .addClass('o_active');
+
+ this.$('.o_chat_composer').toggle(channel.type !== 'static');
+ this.sidebar.$el.toggle(channel.type !== 'static');
+
+ return this.fetch_and_render_thread().then(function () {
+ self.thread.scroll_to({offset: new_channel_scrolltop});
+ self.action_manager.do_push_state({
+ action: self.action.id,
+ active_id: self.channel_id,
+ });
+ });
+ },
+
+ fetch_and_render_thread: function () {
+ var self = this;
+ return chat_manager.fetch(this.channel_id, this.domain).then(function(result) {
+ self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)});
+ });
+ },
+
+ 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 = -framework.getPosition(document.querySelector(oldest_msg_selector)).top;
+ return chat_manager
+ .fetch_more(this.channel_id, this.domain)
+ .then(function(result) {
+ self.thread.render(result, {display_load_more: !chat_manager.all_history_loaded(self.channel_id)});
+ offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top;
+ self.thread.scroll_to({offset: offset});
+ });
+ },
+
+ update_cp: function () {
+ this.update_control_panel({
+ breadcrumbs: this.action_manager.get_breadcrumbs(),
+ cp_content: {
+ $buttons: this.$buttons,
+ $searchview: this.searchview.$el,
+ $searchview_buttons: this.searchview.$buttons.contents(),
+ $sidebar: this.sidebar.$el,
+ },
+ searchview: this.searchview,
+ });
+ },
+
+ on_search: function (domains) {
+ var result = pyeval.sync_eval_domains_and_contexts({
+ domains: domains
+ });
+
+ this.domain = result.domain;
+ this.fetch_and_render_thread();
+ },
+
+ on_redirect: function (res_model, res_id) {
+ this.action_manager.do_push_state({
+ model: res_model,
+ id: res_id,
+ });
+ this.do_action({
+ type:'ir.actions.act_window',
+ view_type: 'form',
+ view_mode: 'form',
+ res_model: res_model,
+ views: [[false, 'form']],
+ res_id: res_id,
+ }, {
+ on_reverse_breadcrumb: this.on_reverse_breadcrumb,
+ });
+ },
+ on_reverse_breadcrumb: function () {
+ this.update_cp(); // do not reload the client action, just display it, but a refresh of the control panel is needed.
+ this.action_manager.do_push_state({
+ action: this.action.id,
+ active_id: this.channel_id,
+ });
+ },
+ on_post_message: function (message) {
+ message.channel_id = this.channel_id;
+ chat_manager
+ .post_message(message)
+ .fail(function () {
+ // todo: display notification
+ });
+ },
+ on_new_message: function (message) {
+ var self = this;
+ if (_.contains(message.channel_ids, this.channel_id)) {
+ this.fetch_and_render_thread().then(function () {
+ self.thread.scroll_to({id: message.id});
+ });
+ }
+ // Dump scroll position of channels in which the new message arrived
+ this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids);
+ },
+ on_update_message: function (message) {
+ var self = this;
+ if ((this.channel_id === "channel_starred" && !message.is_starred) ||
+ (this.channel_id === "channel_inbox" && !message.is_needaction)) {
+ chat_manager.fetch(this.channel_id, this.domain).then(function (messages) {
+ self.thread.remove_message_and_render(message.id, messages);
+ });
+ } else if (_.contains(message.channel_ids, this.channel_id)) {
+ this.fetch_and_render_thread();
+ }
+ },
+ on_new_channel: function (channel) {
+ this.render_sidebar();
+ if (channel.autoswitch) {
+ this.set_channel(channel.id);
+ }
+ },
+
+ on_click_button_invite: function () {
+ var channel = chat_manager.get_channel(this.channel_id);
+ var title = _.str.sprintf(_t('Invite people to %s'), channel.name);
+ new PartnerInviteDialog(this, title, this.channel_id).open();
+ },
+ on_click_button_detach: function () {
+ chat_manager.detach_channel(this.channel_id);
+ },
+
+ on_click_button_unsubscribe: function () {
+ var self = this;
+ var channel = chat_manager.get_channel(this.channel_id);
+ chat_manager
+ .unsubscribe(channel.id)
+ .then(this.set_channel.bind(this, "channel_inbox"))
+ .then(this.render_sidebar.bind(this))
+ .then(function () {
+ var msg = _.str.sprintf(_t('You unsubscribed from <b>%s</b>.'), channel.name);
+ self.do_notify(_t("Unsubscribed"), msg);
+ });
+ },
+ on_click_button_settings: function() {
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: "mail.channel",
+ res_id: this.channel_id,
+ views: [[false, 'form']],
+ target: 'current'
+ }, {
+ on_reverse_breadcrumb: this.on_reverse_breadcrumb,
+ });
+ },
+});
+
+
+core.action_registry.add('mail.chat.instant_messaging', ChatAction);
+
+});
diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js
new file mode 100644
index 00000000000..f60ab76e04c
--- /dev/null
+++ b/addons/mail/static/src/js/systray.js
@@ -0,0 +1,97 @@
+odoo.define('mail.systray', function (require) {
+"use strict";
+
+var core = require('web.core');
+var Model = require('web.Model');
+var session = require('web.session');
+var SystrayMenu = require('web.SystrayMenu');
+var web_client = require('web.web_client');
+var Widget = require('web.Widget');
+
+var internal_bus = core.bus;
+
+/**
+ * Widget Top Menu Notification Counter
+ *
+ * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to
+ * increment (or not) the counter. On click, should redirect to the client action.
+ **/
+var NotificationTopButton = Widget.extend({
+ template:'mail.chat.NotificationTopButton',
+ events: {
+ "click": "on_click",
+ },
+ init: function () {
+ this._super.apply(this, arguments);
+ this.set('counter', 0);
+ },
+ willStart: function () {
+ var self = this;
+ var ir_model = new Model("ir.model.data");
+ var def1 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]);
+ var def2 = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]);
+ var def3 = session.rpc('/mail/needaction');
+ var def4 = this._super();
+ return $.when(def1, def2, def3, def4).then(function (menu_id, action_id, needaction_count) {
+ self.discuss_menu_id = menu_id;
+ self.client_action_id = action_id;
+ self.set('counter', needaction_count);
+ });
+ },
+ start: function () {
+ this.on("change:counter", this, this.on_change_counter);
+ // events
+ internal_bus.on('mail_needaction_new', this, this.counter_increment);
+ internal_bus.on('mail_needaction_done', this, this.counter_decrement);
+ return this._super();
+ },
+ counter_increment: function (inc) {
+ this.set('counter', this.get('counter')+inc);
+ },
+ counter_decrement: function (dec) {
+ this.set('counter', this.get('counter')-dec);
+ },
+ on_change_counter: function () {
+ this.$('.fa-comment').html(this.get('counter') || '');
+ },
+ on_click: function (event) {
+ var self = this;
+ event.preventDefault();
+ this.do_action(this.client_action_id, {clear_breadcrumbs: true}).then(function () {
+ core.bus.trigger('change_menu_section', self.discuss_menu_id);
+ });
+ },
+});
+
+SystrayMenu.Items.push(NotificationTopButton);
+
+
+
+/**
+ * * Global ComposeMessage Top Button
+ * *
+ * * Add a link on the top user bar to write a full mail. It opens the form view
+ * * of the mail.compose.message (in a modal).
+ * */
+var ComposeMessageTopButton = Widget.extend({
+ template:'mail.ComposeMessageTopButton',
+ events: {
+ "click": "on_compose_message",
+ },
+ on_compose_message: function (ev) {
+ ev.preventDefault();
+ web_client.action_manager.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ });
+ },
+});
+
+// Put the ComposeMessageTopButton widget in the systray menu
+SystrayMenu.Items.push(ComposeMessageTopButton);
+
+});
diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js
new file mode 100644
index 00000000000..5f0f559ba19
--- /dev/null
+++ b/addons/mail/static/src/js/window_manager.js
@@ -0,0 +1,115 @@
+odoo.define('mail.window_manager', function (require) {
+"use strict";
+
+var chat_manager = require('mail.chat_manager');
+var ChatWindow = require('mail.ChatWindow');
+
+var core = require('web.core');
+var web_client = require('web.web_client');
+
+
+// chat window management
+//----------------------------------------------------------------
+var chat_sessions = [];
+
+function open_chat (session) {
+ if (!_.findWhere(chat_sessions, {id: session.id})) {
+ var chat_session = {
+ id: session.id,
+ uuid: session.uuid,
+ name: session.name,
+ window: new ChatWindow(web_client, session.id, session.name, session.is_folded),
+ };
+ chat_session.window.on("close_chat_session", null, function () {
+ close_chat(chat_session);
+ chat_manager.close_chat_session(chat_session.id);
+ });
+ chat_session.window.on("toggle_star_status", null, function (message_id) {
+ chat_manager.toggle_star_status(message_id);
+ });
+
+ chat_session.window.on("fold_channel", null, function (channel_id) {
+ chat_manager.fold_channel(channel_id);
+ });
+
+ chat_session.window.on("post_message", null, function (message) {
+ chat_manager.post_message(message);
+ });
+
+ chat_sessions.push(chat_session);
+ chat_session.window.appendTo($('body'))
+ .then(reposition_windows)
+ .then(function () {
+ return chat_manager.get_messages({channel_id: chat_session.id});
+ }).then(function (messages) {
+ chat_session.window.render(messages);
+ chat_session.window.scrollBottom();
+ });
+ }
+}
+
+function close_chat (chat_session) {
+ var session = _.find(chat_sessions, {id: chat_session.id});
+ if (session) {
+ chat_sessions = _.without(chat_sessions, session);
+ session.window.destroy();
+ reposition_windows();
+ }
+}
+
+function toggle_fold_chat (channel) {
+ var session = _.find(chat_sessions, {id: channel.id});
+ if (session) {
+ session.window.toggle_fold();
+ }
+}
+
+function reposition_windows () {
+ _.each(chat_sessions, function (session, index) {
+ session.window.$el.css({right: 245 * index, bottom: 0});
+ });
+}
+
+function update_sessions (message, scrollBottom) {
+ _.each(chat_sessions, function (session) {
+ if (_.contains(message.channel_ids, session.id)) {
+ chat_manager.get_messages({channel_id: session.id}).then(function (messages) {
+ session.window.render(messages);
+ if (scrollBottom) {
+ session.window.scrollBottom();
+ }
+ });
+ }
+ });
+}
+
+
+core.bus.on('web_client_ready', null, function () {
+ chat_manager.bus.on('open_chat', null, open_chat);
+ chat_manager.bus.on('close_chat', null, close_chat);
+ chat_manager.bus.on('channel_toggle_fold', null, toggle_fold_chat);
+
+ chat_manager.bus.on('new_message', null, function (message) {
+ update_sessions(message, true);
+ });
+
+ chat_manager.bus.on('update_message', null, function (message) {
+ update_sessions(message, false);
+ });
+
+ chat_manager.bus.on('anyone_listening', null, function (channel, query) {
+ _.each(chat_sessions, function (session) {
+ if (_.contains(channel.id, session.id)) {
+ query.is_displayed = true;
+ }
+ });
+ });
+
+ _.each(chat_manager.get_channels(), function (channel) {
+ if (channel.is_detached) {
+ open_chat(channel);
+ }
+ });
+});
+
+});
diff --git a/addons/mail/static/src/less/chat_window.less b/addons/mail/static/src/less/chat_window.less
new file mode 100644
index 00000000000..43fb5c7f6f1
--- /dev/null
+++ b/addons/mail/static/src/less/chat_window.less
@@ -0,0 +1,50 @@
+@o-chat-window-bg: lighten(lightgray, 13%);
+
+.o_chat_window {
+ .o-flex-display();
+ .o-flex-flow(column, nowrap);
+ position: fixed;
+ width: 240px;
+ height: 333px;
+ background-color: @o-chat-window-bg;
+ border: 1px solid gray;
+
+ .o_chat_header {
+ .o-flex-display();
+ .o-flex(0, 0, auto);
+ color: white;
+ background-color: @odoo-brand-primary;
+ padding: 5px;
+
+ .o_chat_title {
+ cursor: pointer;
+ .o-flex(1, 1, auto);
+ }
+
+ .o_chat_window_close {
+ color: white;
+ padding: 3px;
+ &:hover {
+ background-color: darken(@odoo-brand-primary, 10%);
+ }
+ }
+ }
+
+ .o_chat_content {
+ .o-flex(1, 1, auto);
+ overflow: auto;
+ padding: 5px;
+
+ .o_mail_thread .o_thread_date_separator {
+ border-color: lightgray;
+ .o_thread_date {
+ background-color: @o-chat-window-bg;
+ }
+ }
+ }
+
+ .o_chat_input > input {
+ padding: 5px;
+ width: 100%;
+ }
+}
diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less
new file mode 100644
index 00000000000..ddb074f6923
--- /dev/null
+++ b/addons/mail/static/src/less/client_action.less
@@ -0,0 +1,188 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+.o_mail_chat {
+ .o-position-absolute(0, 0, 0, 0);
+ height: 100%;
+
+ @media (max-width: @screen-xs-max) {
+ position: static;
+ height: auto;
+ }
+
+ .o-flex-display();
+ .o-flex-flow(row, nowrap);
+
+ .o_mail_chat_sidebar {
+ .o-flex(0, 0, 200px);
+ overflow: auto;
+ padding: 8px 0;
+
+ .o_mail_sidebar_title {
+ .o-webclient-padding(@top: 5px, @bottom: 5px);
+ .o-flex-display();
+ align-items: center;
+ justify-content: space-between;
+ h4 {
+ text-transform: uppercase;
+ }
+ .o_add {
+ padding: 8px;
+ }
+ }
+ .o_mail_chat_channel_item {
+ width: 100%;
+ .o-webclient-padding(@top: 5px, @bottom: 5px);
+ padding-left: 2*@odoo-horizontal-padding; // larger padding than titles
+ > .badge {
+ margin: 0;
+ }
+ .o_mail_partner_unpin {
+ padding: 8px;
+ padding-top: 2px;
+ }
+ }
+ .o_mail_add_channel {
+ width: 100%;
+ .o-webclient-padding(@top: 5px, @bottom: 5px);
+ padding-left: 2*@odoo-horizontal-padding; // larger padding than titles
+ span {
+ margin-right: 5px;
+ }
+ }
+ }
+
+ .o_mail_chat_content {
+ .o-flex(1, 1, 100%);
+ height: 100%;
+ .o-flex-display();
+ .o-flex-flow(column, nowrap);
+ @media (max-width: @screen-xs-max) {
+ height: auto;
+ }
+
+ .o_mail_thread {
+ .o-flex(1, 0, 0);
+ overflow: auto;
+ .o-webclient-padding();
+
+ .o_chat_help_message {
+ padding: 25px 0;
+ text-align: justify;
+ }
+ }
+ .o_chat_composer {
+ .o-flex(0, 0, auto);
+ .o-webclient-padding(@top: 10px, @bottom: 5px);
+
+ .o_composer {
+ .o-flex-display();
+
+ .o_composer_input {
+ .o-flex(1, 1, auto);
+ padding: 5px;
+ margin-right: 5px;
+ border: none;
+ }
+
+ .o_composer_buttons {
+ .o-flex(0, 0, auto);
+ }
+ }
+ }
+ }
+}
+
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+.o_mail_chat {
+ .o_mail_chat_sidebar {
+ background-color: @gray;
+ color: @gray-lighter;
+
+ .o_mail_chat_channel_item {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+ &:hover {
+ background-color: @gray-dark;
+ }
+
+ > .badge {
+ color: white;
+ background-color: @odoo-brand-optional;
+ }
+
+ &.o_active {
+ background-color: @gray-dark;
+ }
+ }
+
+ .o_mail_add_channel {
+ display: none;
+ input {
+ color: black;
+ }
+ }
+
+ .o_mail_chat_channel_unread {
+ font-weight: bold;
+ }
+
+ .o_mail_sidebar_title {
+ h4 {
+ font-size: 13px;
+ text-transform: uppercase;
+ }
+ .o_add {
+ cursor: pointer;
+ &:hover {
+ background-color: @gray-dark;
+ }
+ }
+ }
+
+ // .o_mail_chat_add_more_search {
+ // padding: @padding-base-vertical @padding-base-horizontal;
+ // .o_mail_chat_add_more_text {
+ // color: @gray-lighter;
+ // display: inline-block;
+ // font-style: italic;
+ // cursor: pointer;
+ // font-size: @font-size-base;
+ // }
+ // .o_mail_chat_add_more_text:hover {
+ // text-decoration: underline;
+ // color: white;
+ // cursor: pointer;
+ // }
+ // }
+
+ }
+
+ .o_mail_chat_content {
+ background-color: white;
+
+ .o_chat_composer {
+ border-top: 1px solid @odoo-brand-optional;
+ background-color: @odoo-brand-optional;
+
+ .o_attachment {
+ .o_attachment_name, .o_attachment_delete {
+ color: white;
+ }
+ }
+
+ .o_composer_mentioned_partners {
+ color: white;
+
+ > .badge {
+ color: white;
+ border: 1px solid white;
+ background-color: transparent;
+ }
+ }
+ }
+ }
+}
diff --git a/addons/mail/static/src/xml/chat_window.xml b/addons/mail/static/src/xml/chat_window.xml
new file mode 100644
index 00000000000..e94c51767c2
--- /dev/null
+++ b/addons/mail/static/src/xml/chat_window.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+
+ <t t-name="mail.ChatWindow">
+ <div class="o_chat_window">
+ <div class="o_chat_header">
+ <span class="o_chat_title">
+ <t t-raw="widget.title"/>
+ </span>
+ <span>
+ <a href="#" class="o_chat_window_close fa fa-close"></a>
+ </span>
+ </div>
+ <div class="o_chat_content">
+ <!-- <t t-call="mail.chat.im.Conversation.messages"/> -->
+ </div>
+ <div class="o_chat_input">
+ <input t-att-placeholder="widget.placeholder"/>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml
new file mode 100644
index 00000000000..8a23b33333e
--- /dev/null
+++ b/addons/mail/static/src/xml/client_action.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <!--
+ ChatMailThread
+ Layout of the client action
+ param: widget: the widget (ChatMailThread)
+ -->
+ <t t-name="mail.client_action">
+ <div class="o_mail_chat">
+ <div class="o_mail_chat_sidebar"/>
+ <div class="o_mail_chat_content"/>
+ </div>
+ </t>
+
+ <div t-name="mail.chat.Sidebar" class="o_mail_chat_sidebar">
+ <div class="o_mail_chat_channel_item" data-channel-id="channel_inbox">
+ @ Inbox
+ <span t-attf-class="badge pull-right #{(!needaction_counter ? 'hide' : '')}">
+ <t t-esc="needaction_counter"/>
+ </span>
+ </div>
+ <div class="o_mail_chat_channel_item" data-channel-id="channel_starred">
+ <span class="fa fa-star-o"/> Starred
+ </div>
+
+ <div class="o_mail_sidebar_title">
+ <h4>Channels</h4>
+ <span class="fa fa-plus o_add" data-type="public"/>
+ </div>
+ <t t-foreach="channels" t-as="channel">
+ <t t-if="channel.type === 'public'">
+ <div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
+ <t t-esc="channel.name"/>
+ </div>
+ </t>
+ </t>
+ <div class="o_mail_add_channel" data-type="public">
+ <input type="text" placeholder="Add a channel"/>
+ </div>
+
+ <div class="o_mail_sidebar_title">
+ <h4>Direct Messages</h4>
+ <span class="fa fa-plus o_add" data-type="dm"/>
+ </div>
+ <t t-foreach="channels" t-as="channel">
+ <t t-if="channel.type === 'dm'">
+ <div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
+ <i t-att-class="'fa ' + (channel.status == 'online' ? 'fa-circle' : 'fa-circle-o')"/>
+ <t t-esc="channel.name"/>
+ <span class="fa fa-times pull-right o_mail_partner_unpin" t-att-title="_t('Close')" t-att-data-channel-id="channel.id"/>
+ </div>
+ </t>
+ </t>
+ <div class="o_mail_add_channel" data-type="dm">
+ <input type="text" placeholder="Open chat"/>
+ </div>
+
+ <div class="o_mail_sidebar_title">
+ <h4>Private Channels</h4>
+ <span class="fa fa-plus o_add" data-type="private"/>
+ </div>
+ <t t-foreach="channels" t-as="channel">
+ <t t-if="channel.type === 'private'">
+ <div class="o_mail_chat_channel_item" t-att-data-channel-id="channel.id">
+ <t t-esc="channel.name"/>
+ </div>
+ </t>
+ </t>
+ <div class="o_mail_add_channel" data-type="private">
+ <input type="text" placeholder="Add a channel"/>
+ </div>
+
+
+ <!-- <a href="#" class="o_mail_chat_add_more_text"><span class="fa fa-plus"/>Add</a>
+ <div class="o_mail_chat_add_more_search_bar" style="display: none;">
+ <input type="text"/>
+ </div> -->
+ <!-- <t t-call="mail.chat.ChatMailThread.channels">
+ <t t-set="channel_slot" t-value="'channel_private_group'"/>
+ </t> -->
+ </div>
+
+ <!--
+ Buttons of the Control Panel
+ -->
+ <t t-name="mail.chat.ControlButtons">
+ <div>
+ <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button>
+ <button type="button" class="btn btn-default btn-sm o_mail_chat_button_detach" t-att-title='_t("Detach the conversation")'>Detach</button>
+ </div>
+ </t>
+
+ <div t-name="mail.PartnerInviteDialog">
+ <input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
+ </div>
+</templates>
diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml
new file mode 100644
index 00000000000..5f355d88969
--- /dev/null
+++ b/addons/mail/static/src/xml/systray.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+ <t t-name="mail.ComposeMessageTopButton">
+ <li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email">
+ <a href="#">
+ <i class="fa fa-envelope-o fa-fw"></i>
+ </a>
+ </li>
+ </t>
+
+ <t t-name="mail.chat.NotificationTopButton">
+ <li t-att-title='_t("Notifications")'>
+ <a href="#">
+ <i class="fa fa-comment"/> <t t-esc="widget.get('counter') || ''"/>
+ </a>
+ </li>
+ </t>
+
+</templates>
+