diff --git a/addons/im_chat/static/src/css/im_common.css b/addons/im_chat/static/src/css/im_common.css index 11cbe5de19f..bb3c4645fd8 100644 --- a/addons/im_chat/static/src/css/im_common.css +++ b/addons/im_chat/static/src/css/im_common.css @@ -64,67 +64,63 @@ .oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected { display: block; } -.oe_im_chatview .oe_im_chatview_header { + + + +/* ####### Chat View : Header ####### */ +.oe_im_chatview_header { padding: 3px 6px 2px; background: #DEDEDE; background: -moz-linear-gradient(#FCFCFC, #DEDEDE); background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE)); + border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; -webkit-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; border-bottom: 1px solid #AEB9BD; cursor: pointer; } -.oe_im_chatview .oe_im_chatview_header_name{ +.oe_im_chatview_header .header_name{ max-width: 75%; word-wrap: break-word; display: inline-block; height: 15px; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.oe_im_chatview .oe_im_chatview_right { - padding: 0; - background: transparent; - border: 0; + +.oe_im_chatview_header .right_buttons { float: right; +} +.oe_im_chatview_header .right_buttons > * { + display: inline-block; + border: 0; + cursor: pointer; color: gray ; + background: transparent; + font-size: 16px; + line-height: 16px; + margin-left: 5px; + -webkit-appearance: none; } -/* options */ -.oe_im_chatview .oe_im_chatview_option_group{ - z-index: 10; -} -.oe_im_chatview .oe_im_chatview_option_list{ +.oe_im_chatview_header .right_buttons .dropdown-menu{ right: 0; left: auto; } -.oe_im_chatview .oe_im_chatview_option_list li{ +.oe_im_chatview_header .right_buttons .dropdown-menu > li{ display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.428571429; - color: #333; + color: #4c4c4c; white-space: nowrap; } -.oe_im_chatview .oe_im_chatview_right div, .oe_im_chatview .oe_im_chatview_right button{ - cursor: pointer; - background: transparent; - border: 0; - margin-left: 5px; - -webkit-appearance: none; - font-size: 18px; - line-height: 16px; - font-weight: bold; - text-shadow: 0 1px 0 white; - opacity: 0.9; - display: inline-block; -} + + .oe_im_chatview .oe_im_chatview_content { overflow: auto; height: 287px; diff --git a/addons/im_chat/static/src/js/im_chat.js b/addons/im_chat/static/src/js/im_chat.js index 2017f5c3582..135782a382c 100644 --- a/addons/im_chat/static/src/js/im_chat.js +++ b/addons/im_chat/static/src/js/im_chat.js @@ -9,423 +9,47 @@ var data = require('web.data'); var Model = require('web.Model'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); -var time = require('web.time'); var utils = require('web.utils'); var web_client = require('web.web_client'); var Widget = require('web.Widget'); +var im_chat_common = require('im_chat.im_chat_common'); var _t = core._t; var QWeb = core.qweb; -var NBR_LIMIT_HISTORY = 20; var USERS_LIMIT = 20; -var ConversationManager = Widget.extend({ - init: function(parent, options) { - var self = this; - this._super(parent); - this.options = _.clone(options) || {}; - _.defaults(this.options, { - inputPlaceholder: _t("Say something..."), - defaultMessage: null, - defaultUsername: _t("Visitor"), - }); - // business - this.sessions = {}; - this.bus = bus.bus; - this.bus.on("notification", this, this.on_notification); - this.bus.options.im_presence = true; - // ui - this.set("right_offset", 0); - this.set("bottom_offset", 0); - this.on("change:right_offset", this, this.calc_positions); - this.on("change:bottom_offset", this, this.calc_positions); +// ########## CONVERSATION extentions ############### - this.set("window_focus", true); - this.on("change:window_focus", self, function(e) { - self.bus.options.im_presence = self.get("window_focus"); - }); - this.set("waiting_messages", 0); - this.on("change:waiting_messages", this, this.window_title_change); - $(window).on("focus", _.bind(this.window_focus, this)); - $(window).on("blur", _.bind(this.window_blur, this)); - this.window_title_change(); - }, - on_notification: function(notification) { - var self = this; - var channel = notification[0]; - var message = notification[1]; - var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); - - // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session) - if((Array.isArray(channel) && (channel[1] === 'im_chat.session' || channel[1] === 'im_chat.presence')) || (regex_uuid.test(channel))){ - // message to display in the chatview - if (message.type === "message" || message.type === "meta") { - self.received_message(message); - } - // activate the received session - if(message.uuid){ - this.apply_session(message); - } - // user status notification - if(message.im_status){ - self.trigger("im_new_user_status", [message]); - } - } - }, - - // window focus unfocus beep and title - window_focus: function() { - this.set("window_focus", true); - this.set("waiting_messages", 0); - }, - window_blur: function() { - this.set("window_focus", false); - }, - window_beep: function() { - if (typeof(Audio) === "undefined") { - return; - } - var audio = new Audio(); - var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; - audio.src = session.url("/im_chat/static/src/audio/ting") + ext; - audio.play(); - }, - window_title_change: function() { - var title = undefined; - if (this.get("waiting_messages") !== 0) { - title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); - this.window_beep(); - } - if (! web_client || !web_client.set_title_part) - return; - web_client.set_title_part("im_messages", title); - }, - - apply_session: function(session, focus){ - var self = this; - var conv = this.sessions[session.uuid]; - if (! conv) { - if(session.state !== 'closed'){ - conv = new Conversation(this, this, session, this.options); - conv.appendTo($("body")); - conv.on("destroyed", this, _.bind(this.delete_session, this)); - this.sessions[session.uuid] = conv; - this.calc_positions(); - } - }else{ - conv.set("session", session); - } - conv && this.trigger("im_session_activated", conv); - if (focus) - conv.focus(); - return conv; - }, - activate_session: function(session, focus) { - var self = this; - var active_session = _.clone(session); - active_session.state = 'open'; - var conv = this.apply_session(active_session, focus); - if(session.state !== 'open'){ - conv.update_fold_state('open'); - } - return conv; - }, - delete_session: function(uuid){ - delete this.sessions[uuid]; - this.calc_positions(); - }, - received_message: function(message) { +im_chat_common.ConversationManager.include({ + _message_receive: function(message){ var self = this; var session_id = message.to_id[0]; - var uuid = message.to_id[1]; - var from_id = message.from_id ? message.from_id[0] : false; - var current_user = session ? session.uid : false; - if (! this.get("window_focus") && from_id != current_user) { - this.set("waiting_messages", this.get("waiting_messages") + 1); - } - var conv = this.sessions[uuid]; + var conv = this.sessions[message.to_id[1]]; if(!conv){ // fetch the session, and init it with the message var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){ - conv = self.activate_session(session, false); - conv.received_message(message); + conv = self.session_apply(session, {'force_open': true}); + conv.message_receive(message); }); }else{ - conv.received_message(message); + this._super(message); } }, - calc_positions: function() { - var self = this; - var current = this.get("right_offset"); - _.each(this.sessions, function(s) { - s.set("bottom_position", self.get("bottom_offset")); - s.set("right_position", current); - current += s.$().outerWidth(true); - }); - }, - destroy: function() { - $(window).off("unload", this.unload); - $(window).off("focus", this.window_focus); - $(window).off("blur", this.window_blur); - return this._super(); - } }); -var Conversation = Widget.extend({ - className: "openerp_style oe_im_chatview", - events: { - "keydown input": "keydown", - "click .oe_im_chatview_options" : "click_options", - "click .oe_im_chatview_close": "click_close", - "click .oe_im_chatview_header": "click_header" +im_chat_common.Conversation.include({ + // user actions + prepare_action_menu: function(){ + this._super(); + this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode); + this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session); }, - init: function(parent, c_manager, session, options) { - this._super(parent); - this.c_manager = c_manager; - this.options = options || {}; - this.loading_history = true; - this.set("messages", []); - this.set("session", session); - this.set("right_position", 0); - this.set("bottom_position", 0); - this.set("pending", 0); - this.inputPlaceholder = this.options.defaultInputPlaceholder; - }, - start: function() { - var self = this; - self.$().append(QWeb.render("im_chat.Conversation", {widget: self})); - self.$().hide(); - self.on("change:session", self, self.update_session); - self.on("change:right_position", self, self.calc_pos); - self.on("change:bottom_position", self, self.calc_pos); - self.full_height = self.$().height(); - self.calc_pos(); - self.on("change:pending", self, _.bind(function() { - if (self.get("pending") === 0) { - self.$(".oe_im_chatview_nbr_messages").text(""); - } else { - self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")"); - } - }, self)); - // messages business - self.on("change:messages", this, this.render_messages); - self.$('.oe_im_chatview_content').on('scroll',function(){ - if($(this).scrollTop() === 0){ - self.load_history(); - } - }); - self.load_history(); - self.$().show(); - // prepare the header and the correct state - self.update_session(); - - self.init_options(); - }, - init_options: function(){ - this.define_options(); - if(this.$('.oe_im_chatview_option_list li').length === 0){ - this.$('.oe_im_chatview_option_group').hide(); - } - }, - define_options: function(){ - // add here the option to put in the dropdown menu - this._add_option("Shortcuts", "option_shortcut", "fa fa-info-circle"); - this.$('.oe_im_chatview_option_list .option_shortcut').on('click', _.bind(this.click_option_shortcut, this)); - // quit the conversation - this._add_option('Quit discussion', 'im_chat_option_quit', 'fa fa-minus-square'); - this.$('.oe_im_chatview_option_list .im_chat_option_quit').on('click', this, _.bind(this.action_quit_conversation, this)); - }, - _add_option: function(label, style_class, icon_fa_class){ - if(icon_fa_class){ - label = ' ' + label; - } - this.$('.oe_im_chatview_option_list').append('
  • '+label+'
  • '); - }, - action_quit_conversation: function(){ - var self = this; - var Session = new Model("im_chat.session"); - return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { - if(! res){ - core.bus.trigger('display_notification_warning', _t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); - } - }); - }, - show: function(){ - this.$().animate({ - height: this.full_height - }); - this.set("pending", 0); - }, - hide: function(){ - this.$().animate({ - height: this.$(".oe_im_chatview_header").outerHeight() - }); - }, - calc_pos: function() { - this.$().css("right", this.get("right_position")); - this.$().css("bottom", this.get("bottom_position")); - }, - update_fold_state: function(state){ - return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}); - }, - update_session: function(){ - // built the name - var names = []; - _.each(this.get("session").users, function(user){ - if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){ - names.push(user.name); - } - }); - this.$(".oe_im_chatview_header_name").text(names.join(", ")); - this.$(".oe_im_chatview_header_name").attr('title', names.join(", ")); - // update the fold state - if(this.get("session").state){ - if(this.get("session").state === 'closed'){ - this.destroy(); - }else{ - if(this.get("session").state === 'open'){ - this.show(); - }else{ - this.hide(); - } - } - } - }, - load_history: function(){ - var self = this; - if(this.loading_history){ - var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY}; - var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false; - if(lastid){ - data.last_id = lastid; - } - session.rpc("/im_chat/history", data).then(function(messages){ - if(messages){ - self.insert_messages(messages); - if(messages.length != NBR_LIMIT_HISTORY){ - self.loading_history = false; - } - }else{ - self.loading_history = false; - } - }); - } - }, - received_message: function(message) { - if (this.get('session').state === 'open') { - this.set("pending", 0); - } else { - this.set("pending", this.get("pending") + 1); - } - this.insert_messages([message]); - this._go_bottom(); - }, - send_message: function(message, type) { - var self = this; - var send_it = function() { - return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message}); - }; - var tries = 0; - send_it().fail(function(error, e) { - e.preventDefault(); - tries += 1; - if (tries < 3) - return send_it(); - }); - }, - insert_messages: function(messages){ - var self = this; - var get_anonymous_name = function(){ - var name = self.options.defaultUsername; - _.each(self.get('session').users, function(u){ - if(!u.id){ - name = u.name; - } - }); - return name; - }; - // avoid duplicated messages - messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); - // escape the message content and set the timezone - _.map(messages, function(m){ - if(!m.from_id){ - m.from_id = [false, get_anonymous_name()]; - } - m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss'); - return m; - }); - this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); - }, - render_messages: function(){ - var self = this; - var res = {}; - var last_date_day, last_user_id = -1; - _.each(this.get("messages"), function(current){ - // add the url of the avatar for all users in the conversation - current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0])); - var date_day = current.create_date.split(" ")[0]; - if(date_day !== last_date_day){ - res[date_day] = []; - last_user_id = -1; - } - last_date_day = date_day; - if(current.type == "message"){ // traditionnal message - if(last_user_id === current.from_id[0]){ - _.last(res[date_day]).push(current); - }else{ - res[date_day].push([current]); - } - last_user_id = current.from_id[0]; - }else{ // meta message - res[date_day].push([current]); - last_user_id = -1; - } - }); - // render and set the content of the chatview - // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template - // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be - // imported in the frontend. - this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res}))); - this._go_bottom(); - }, - keydown: function(e) { - if(e && e.which == 27) { - if(this.$el.prev().find('.oe_im_chatview_input').length > 0){ - this.$el.prev().find('.oe_im_chatview_input').focus(); - }else{ - this.$el.next().find('.oe_im_chatview_input').focus(); - } - e.stopPropagation(); - this.update_fold_state('closed'); - } - if(e && e.which !== 13) { - return; - } - var mes = this.$("input").val(); - if (! mes.trim()) { - return; - } - this.$("input").val(""); - this.send_message(mes, "message"); - }, - _go_bottom: function() { - this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); - }, - add_user: function(user){ - return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]); - }, - focus: function() { - this.$(".oe_im_chatview_input").focus(); - }, - click_options: function(e){ - this.$('.oe_im_chatview_options').dropdown(); - }, - click_option_shortcut: function(){ - web_client.action_manager.do_action({ + action_shorcode: function(e){ + return web_client.action_manager.do_action({ type: 'ir.actions.act_window', - name : 'Shortcode', + name : _t('Shortcode'), res_model: 'im_chat.shortcode', view_mode: 'tree,form', view_type: 'tree', @@ -438,23 +62,46 @@ var Conversation = Widget.extend({ } }); }, - click_header: function(event){ - var classes = event.target.className.split(' '); - if(_.contains(classes, 'oe_im_chatview_header_name') || _.contains(classes, 'oe_im_chatview_header')){ - this.update_fold_state(); + action_quit_session: function(e){ + var self = this; + var Session = new Model("im_chat.session"); + return Session.call("quit_user", [this.get("session").uuid]).then(function(res) { + if(! res){ + self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave.")); + } + }); + }, + // session + session_update_state: function(state){ + var self = this; + var args = arguments; + var super_call = this._super; + // broadcast the state changing + return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ + super_call.apply(self, args); + }); + }, + // window title + window_title_change: function() { + this.super(); + var title = undefined; + if (this.get("waiting_messages") !== 0) { + title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")) } + web_client.set_title_part("im_messages", title); }, - click_close: function(event) { - this.update_fold_state('closed'); + // TODO : change this way + add_user: function(user){ + return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]); }, - destroy: function() { - this.trigger("destroyed", this.get('session').uuid); - return this._super(); - } }); + + +// ###### BACKEND : contact panel, top menu button ######### + var UserWidget = Widget.extend({ - "template": "im_chat.UserWidget", + template: "im_chat.UserWidget", events: { "click": "activate_user", }, @@ -477,7 +124,7 @@ var UserWidget = Widget.extend({ this.$(".oe_im_user_online").attr('src', img_src); }, activate_user: function() { - this.trigger("activate_user", this.get("id")); + this.trigger("user_clicked", this.get("id")); }, }); @@ -496,43 +143,55 @@ var InstantMessaging = Widget.extend({ this.users = []; this.widgets = {}; - this.c_manager = new ConversationManager(this); - this.on("change:right_offset", this.c_manager, _.bind(function() { - this.c_manager.set("right_offset", this.get("right_offset")); - }, this)); - this.user_search_dm = new utils.DropMisordered(); + // listen bus + this.bus = bus.bus; + this.bus.on("notification", this, this.on_notification); }, start: function() { var self = this; + // ui + this.conv_manager = new im_chat_common.ConversationManager(this); + this.on("change:right_offset", this.conv_manager, _.bind(function() { + this.conv_manager.set("right_offset", this.get("right_offset")); + }, this)); + $(window).scroll(_.bind(this.position_compute, this)); + $(window).resize(_.bind(this.position_compute, this)); this.$el.css("right", -this.$el.outerWidth()); - $(window).scroll(_.bind(this.calc_box, this)); - $(window).resize(_.bind(this.calc_box, this)); - this.calc_box(); + this.position_compute(); - this.on("change:current_search", this, this.search_users_status); + // business + this.on("change:current_search", this, this.user_search); // add a drag & drop listener - self.c_manager.on("im_session_activated", self, function(conv) { + self.conv_manager.on("im_session_activated", self, function(conv) { conv.$el.droppable({ drop: function(event, ui) { conv.add_user(ui.draggable.data("user")); } }); }); - // add a listener for the update of users status - this.c_manager.on("im_new_user_status", this, this.update_users_status); // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page) - session.rpc("/im_chat/init",{}).then(function(notifications) { + return session.rpc("/im_chat/init", {}).then(function(notifications) { _.each(notifications, function(notif){ - self.c_manager.on_notification(notif); + self.conv_manager.on_notification(notif, {'load_history': true}); }); // start polling - bus.bus.start_polling(); + self.bus.start_polling(); }); - return; }, - calc_box: function() { + on_notification: function(notification){ + var channel = notification[0]; + var message = notification[1]; + // user status notification + if(channel[1] === 'im_chat.presence'){ + if(message.im_status){ + this.user_update_status([message]); + } + } + }, + // ui + position_compute: function() { var $topbar = window.$('#oe_main_menu_navbar'); // .oe_topbar is replaced with .navbar of bootstrap3 var top = $topbar.offset().top + $topbar.height(); top = Math.max(top - $(window).scrollTop(), 0); @@ -542,30 +201,8 @@ var InstantMessaging = Widget.extend({ input_change: function() { this.set("current_search", this.$(".oe_im_searchbox").val()); }, - search_users_status: function(e) { - var user_model = new Model("res.users"); - var self = this; - return this.user_search_dm.add(user_model.call("im_search", [this.get("current_search"), - USERS_LIMIT], {context:new data.CompoundContext()})).then(function(result) { - self.$(".oe_im_input").val(""); - var old_widgets = self.widgets; - self.widgets = {}; - self.users = []; - _.each(result, function(user) { - user.image_url = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id}); - var widget = new UserWidget(self, user); - widget.appendTo(self.$(".oe_im_users")); - widget.on("activate_user", self, self.activate_user); - self.widgets[user.id] = widget; - self.users.push(user); - }); - _.each(old_widgets, function(w) { - w.destroy(); - }); - }); - }, switch_display: function() { - this.calc_box(); + this.position_compute(); var fct = _.bind(function(place) { this.set("right_offset", place + this.$el.outerWidth()); this.$(".oe_im_searchbox").focus(); @@ -583,21 +220,43 @@ var InstantMessaging = Widget.extend({ return; } // update the list of user status when show the IM - this.search_users_status(); + this.user_search(); this.$el.animate({ right: 0, }, opt); } this.shown = ! this.shown; }, - activate_user: function(user_id) { + // user methods + user_search: function(e) { + var self = this; + var user_model = new Model("res.users"); + return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){ + self.$(".oe_im_input").val(""); + var old_widgets = self.widgets; + self.widgets = {}; + self.users = []; + _.each(result, function(user) { + user.image_url = session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id}); + var widget = new UserWidget(self, user); + widget.appendTo(self.$(".oe_im_users")); + widget.on("user_clicked", self, self.user_clicked); + self.widgets[user.id] = widget; + self.users.push(user); + }); + _.each(old_widgets, function(w) { + w.destroy(); + }); + }); + }, + user_clicked: function(user_id) { var self = this; var sessions = new Model("im_chat.session"); return sessions.call("session_get", [user_id]).then(function(session) { - self.c_manager.activate_session(session, true); + self.conv_manager.session_apply(session, {'focus': true, 'force_open': true}); }); }, - update_users_status: function(users_list){ + user_update_status: function(users_list){ var self = this; _.each(users_list, function(el) { if (self.widgets[el.id]) { @@ -637,8 +296,6 @@ Users.call('has_group', ['base.group_user']).done(function(is_employee) { }); return { - Conversation: Conversation, - ConversationManager: ConversationManager, InstantMessaging: InstantMessaging, UserWidget: UserWidget, }; diff --git a/addons/im_chat/static/src/js/im_chat_common.js b/addons/im_chat/static/src/js/im_chat_common.js new file mode 100644 index 00000000000..0aaa0e68d16 --- /dev/null +++ b/addons/im_chat/static/src/js/im_chat_common.js @@ -0,0 +1,433 @@ +odoo.define('im_chat.im_chat_common', function (require) { +"use strict"; + +// to do: make this work in website + +var bus = require('bus.bus'); +var core = require('web.core'); +var session = require('web.session'); +var time = require('web.time'); +var Widget = require('web.Widget'); + +var _t = core._t; +var QWeb = core.qweb; + +var NBR_LIMIT_HISTORY = 20; + + +var ConversationManager = Widget.extend({ + init: function(parent, options) { + var self = this; + this._super(parent); + this.options = _.clone(options) || {}; + _.defaults(this.options, { + inputPlaceholder: _t("Say something..."), + defaultMessage: null, + defaultUsername: _t("Visitor"), + force_open: false, + load_history: true, + focus: false, + }); + // business + this.sessions = {}; + this.bus = bus.bus; + this.bus.on("notification", this, this.on_notification); + this.bus.options.im_presence = true; + + // ui + this.set("right_offset", 0); + this.set("bottom_offset", 0); + this.on("change:right_offset", this, this.calc_positions); + this.on("change:bottom_offset", this, this.calc_positions); + + this.set("window_focus", true); + this.on("change:window_focus", self, function(e) { + self.bus.options.im_presence = self.get("window_focus"); + }); + this.set("waiting_messages", 0); + this.on("change:waiting_messages", this, this.window_title_change); + $(window).on("focus", _.bind(this.window_focus, this)); + $(window).on("blur", _.bind(this.window_blur, this)); + this.window_title_change(); + }, + on_notification: function(notification, options) { + var self = this; + var channel = notification[0]; + var message = notification[1]; + var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g); + + // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session) + if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){ + // message to display in the chatview + if (message.type === "message" || message.type === "meta") { + self.message_receive(message); + } + // activate the received session + if(message.uuid){ + this.session_apply(message, options); + } + } + }, + + // window focus unfocus beep and title + window_focus: function() { + this.set("window_focus", true); + this.set("waiting_messages", 0); + }, + window_blur: function() { + this.set("window_focus", false); + }, + window_beep: function() { + if (typeof(Audio) === "undefined") { + return; + } + var audio = new Audio(); + var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"; + audio.src = session.url("/im_chat/static/src/audio/ting") + ext; + audio.play(); + }, + window_title_change: function() { + var title = undefined; + if (this.get("waiting_messages") !== 0) { + title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); + this.window_beep(); + } + }, + // sessions and messages + session_apply: function(active_session, options){ + // options used by this function : force_open and focus (load_history can be usefull) + var self = this; + options = _.extend(_.clone(this.options), options || {}); + // force open + var session = _.clone(active_session); + if(options['force_open']){ + session.state = 'open'; + } + // create/get the conversation widget + var conv = this.sessions[session.uuid]; + if (! conv) { + if(session.state !== 'closed'){ + conv = new Conversation(this, this, session, options); + conv.appendTo($("body")); + conv.on("destroyed", this, _.bind(this.session_delete, this)); + this.sessions[session.uuid] = conv; + this.calc_positions(); + } + }else{ + conv.set("session", session); + } + // if force open, broadcast it + if(options['force_open'] && active_session.state !== 'open'){ + conv.session_update_state('open'); + } + // apply the focus + if (options['focus']){ + conv.focus(); + } + return conv; + }, + session_delete: function(uuid){ + delete this.sessions[uuid]; + this.calc_positions(); + }, + message_receive: function(message) { + var self = this; + var session_id = message.to_id[0]; + var uuid = message.to_id[1]; + var from_id = message['from_id'] ? message['from_id'][0] : false; + if (! this.get("window_focus") && from_id != this.get_current_uid()) { + this.set("waiting_messages", this.get("waiting_messages") + 1); + } + this._message_receive(message); + }, + _message_receive: function(message){ + var uuid = message.to_id[1]; + var conv = this.sessions[uuid]; + conv.message_receive(message); + }, + // others + get_current_uid: function(){ + return session && !_.isUndefined(session.uid) ? session.uid : false; + }, + calc_positions: function() { + var self = this; + var current = this.get("right_offset"); + _.each(this.sessions, function(s) { + s.set("bottom_position", self.get("bottom_offset")); + s.set("right_position", current); + current += s.$().outerWidth(true); + }); + }, + destroy: function() { + $(window).off("unload", this.unload); + $(window).off("focus", this.window_focus); + $(window).off("blur", this.window_blur); + return this._super(); + } +}); + + +var Conversation = Widget.extend({ + className: "openerp_style oe_im_chatview", + events: { + "keydown input": "keydown", + "click .oe_im_chatview_close": "click_close", + "click .oe_im_chatview_header": "click_header" + }, + init: function(parent, c_manager, session, options) { + this._super(parent); + this.c_manager = c_manager; + this.options = options || {}; + this.loading_history = true; + this.set("messages", []); + this.set("session", session); + this.set("right_position", 0); + this.set("bottom_position", 0); + this.set("pending", 0); + this.inputPlaceholder = this.options.defaultInputPlaceholder; + }, + start: function() { + var self = this; + self._super.apply(this, arguments); + self.prepare_action_menu(); + // ui + self.$().append(QWeb.render("im_chat.Conversation", {widget: self})); + self.on("change:right_position", self, self.calc_pos); + self.on("change:bottom_position", self, self.calc_pos); + self.on("change:pending", self, _.bind(function() { + if (self.get("pending") === 0) { + self.$(".nbr_messages").text(""); + } else { + self.$(".nbr_messages").text("(" + self.get("pending") + ")"); + } + }, self)); + self.full_height = self.$().height(); + self.calc_pos(); + // business + self.bind_action_menu(); + self.on("change:session", self, self.session_update); + self.on("change:messages", self, self.render_messages); + self.$('.oe_im_chatview_content').on('scroll',function(){ + if($(this).scrollTop() === 0){ + self.message_history(); + } + }); + if(self.options['load_history']){ // load history if asked + self.message_history(); + } + // prepare the header and the correct state + self.session_update(); + }, + // action menu + _add_action: function(label, style_class, icon_fa_class, callback){ + this.actions.push({ + 'label': label, + 'class': style_class, + 'icon_class': icon_fa_class, + 'callback': callback, + }); + }, + prepare_action_menu: function(){ + // override this method to add action with _add_action() + this.actions = []; + }, + bind_action_menu: function(){ + var self = this; + _.each(this.actions, function(action){ + if(action.callback){ + self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self)); + } + }); + }, + // ui + show: function(){ + this.$().animate({ + height: this.full_height + }); + this.set("pending", 0); + }, + hide: function(){ + this.$().animate({ + height: this.$(".oe_im_chatview_header").outerHeight() + }); + }, + calc_pos: function() { + this.$().css("right", this.get("right_position")); + this.$().css("bottom", this.get("bottom_position")); + }, + // session + session_update_state: function(state){ + var session = this.get('session'); + session.state = state; + this.set('session', session); + }, + session_update: function(){ + // built the name + var names = []; + _.each(this.get("session").users, function(user){ + if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){ + names.push(user.name); + } + }); + this.$(".header_name").text(names.join(", ")); + this.$(".header_name").attr('title', names.join(", ")); + // update the fold state + if(this.get("session").state){ + if(this.get("session").state === 'closed'){ + this.destroy(); + }else{ + if(this.get("session").state === 'open'){ + this.show(); + }else{ + this.hide(); + } + } + } + }, + // messages + message_history: function(){ + var self = this; + if(this.loading_history){ + var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY}; + var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false; + if(lastid){ + data.last_id = lastid; + } + session.rpc("/im_chat/history", data).then(function(messages){ + if(messages){ + self.insert_messages(messages); + if(messages.length != NBR_LIMIT_HISTORY){ + self.loading_history = false; + } + }else{ + self.loading_history = false; + } + }); + } + }, + message_receive: function(message) { + if (this.get('session').state === 'open') { + this.set("pending", 0); + } else { + this.set("pending", this.get("pending") + 1); + } + this.insert_messages([message]); + this._go_bottom(); + }, + message_send: function(message, type) { + var self = this; + var send_it = function() { + return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message}); + }; + var tries = 0; + send_it().fail(function(error, e) { + e.preventDefault(); + tries += 1; + if (tries < 3) + return send_it(); + }); + }, + insert_messages: function(messages){ + var self = this; + // avoid duplicated messages + messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; }); + // escape the message content and set the timezone + _.map(messages, function(m){ + if(!m.from_id){ + m.from_id = [false,self. get_anonymous_name()]; + } + m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss'); + return m; + }); + this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; })); + }, + render_messages: function(){ + var self = this; + var res = {}; + var last_date_day, last_user_id = -1; + _.each(this.get("messages"), function(current){ + // add the url of the avatar for all users in the conversation + current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0])); + var date_day = current.create_date.split(" ")[0]; + if(date_day !== last_date_day){ + res[date_day] = []; + last_user_id = -1; + } + last_date_day = date_day; + if(current.type == "message"){ // traditionnal message + if(last_user_id === current.from_id[0]){ + _.last(res[date_day]).push(current); + }else{ + res[date_day].push([current]); + } + last_user_id = current.from_id[0]; + }else{ // meta message + res[date_day].push([current]); + last_user_id = -1; + } + }); + // render and set the content of the chatview + // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template + // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be + // imported in the frontend. + this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res}))); + this._go_bottom(); + }, + // utils and event + get_anonymous_name: function(){ + var name = this.options["defaultUsername"]; + _.each(this.get('session').users, function(u){ + if(!u.id){ + name = u.name; + } + }); + return name; + }, + keydown: function(e) { + if(e && e.which == 27) { + if(this.$el.prev().find('.oe_im_chatview_input').length > 0){ + this.$el.prev().find('.oe_im_chatview_input').focus(); + }else{ + this.$el.next().find('.oe_im_chatview_input').focus(); + } + e.stopPropagation(); + this.session_update_state('closed'); + } + if(e && e.which !== 13) { + return; + } + var mes = this.$("input").val(); + if (! mes.trim()) { + return; + } + this.$("input").val(""); + this.message_send(mes, "message"); + }, + _go_bottom: function() { + this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight); + }, + focus: function() { + this.$(".oe_im_chatview_input").focus(); + }, + click_header: function(event){ + var classes = event.target.className.split(' '); + if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){ + this.session_update_state(); + } + }, + click_close: function(event) { + this.session_update_state('closed'); + }, + destroy: function() { + this.trigger("destroyed", this.get('session').uuid); + return this._super(); + } +}); + + +return { + Conversation: Conversation, + ConversationManager: ConversationManager, +}; + +}); \ No newline at end of file diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/im_chat/static/src/xml/im_chat.xml index 290de6b8cc6..9adef41a24f 100644 --- a/addons/im_chat/static/src/xml/im_chat.xml +++ b/addons/im_chat/static/src/xml/im_chat.xml @@ -2,16 +2,21 @@
    - - - -
    - - - - + + + + +
    + + + +
    ×
    diff --git a/addons/im_chat/views/im_chat.xml b/addons/im_chat/views/im_chat.xml index c18e801beb2..59d67e6a2d1 100644 --- a/addons/im_chat/views/im_chat.xml +++ b/addons/im_chat/views/im_chat.xml @@ -6,6 +6,7 @@ + diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index c5fd6cc75bb..9d27e2b7969 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -2,12 +2,12 @@ odoo.define('im_livechat.im_livechat', function (require) { "use strict"; var bus = require('bus.bus'); -var im_chat = require('im_chat.im_chat'); var core = require('web.core'); -var session = require('web.session'); +var user_session = require('web.session'); var time = require('web.time'); var utils = require('web.utils'); var Widget = require('web.Widget'); +var im_chat_common = require('im_chat.im_chat_common'); var _t = core._t; var QWeb = core.qweb; @@ -19,16 +19,12 @@ var COOKIE_NAME = 'livechat_conversation'; The state of anonymous session is hold by the client and not the server. Override the method managing the state of normal conversation. */ -im_chat.Conversation.include({ +im_chat_common.Conversation.include({ init: function(){ this._super.apply(this, arguments); this.shown = true; - this.loading_history = true; // since the session is kept after a refresh, anonymous can reload their history this.feedback = false; }, - define_options: function(){ - // no options for anonymous user - }, show: function(){ this._super.apply(this, arguments); this.shown = true; @@ -37,7 +33,8 @@ im_chat.Conversation.include({ this._super.apply(this, arguments); this.shown = false; }, - update_fold_state: function(state){ + session_update_state: function(state){ + // set manually the new state if(state === 'closed'){ this.destroy(); }else{ @@ -53,10 +50,9 @@ im_chat.Conversation.include({ } } } - var session = this.get('session'); - session.state = state; - this.set('session', session); - session.set_cookie(COOKIE_NAME, JSON.stringify(session), 60*60); + // session and cookie update + this._super(state); + utils.set_cookie(COOKIE_NAME, JSON.stringify(this.get('session')), 60*60); }, click_close: function(event) { if(!this.feedback && (this.get('messages').length > 1)){ @@ -68,21 +64,22 @@ im_chat.Conversation.include({ this.feedback.on("feedback_sent", this, this.click_close); }else{ this._super.apply(this, arguments); - session.set_cookie(COOKIE_NAME, "", -1); + utils.set_cookie(COOKIE_NAME, "", -1); } }, }); // To avoid exeption when the anonymous has close his // conversation and when operator send him a message. -im_chat.ConversationManager.include({ - received_message: function(message) { +im_chat_common.ConversationManager.include({ + message_receive: function(message) { try{ this._super(message); }catch(e){} } }); + var LiveSupport = Widget.extend({ init: function(server_url, db, channel, options, rule) { this._super(); @@ -93,11 +90,11 @@ var LiveSupport = Widget.extend({ defaultMessage: _t("How may I help you?"), defaultUsername: _t("Visitor"), }); - session.setup(server_url, {use_cors: false}); + user_session.setup(server_url, {use_cors: false}); this.load_template(db, channel, options, rule); }, _get_template_list: function(){ - return ['/im_live_chat/static/src/xml/im_live_chat.xml', '/im_chat/static/src/xml/im_chat.xml']; + return ['/im_livechat/static/src/xml/im_livechat.xml', '/im_chat/static/src/xml/im_chat.xml']; }, load_template: function(db, channel, options, rule){ var self = this; @@ -105,7 +102,7 @@ var LiveSupport = Widget.extend({ var defs = []; var templates = this._get_template_list(); _.each(templates, function(tmpl){ - defs.push(session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) { + defs.push(user_session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) { QWeb.add_template(xml); })); }); @@ -119,7 +116,7 @@ var LiveSupport = Widget.extend({ if(cookie){ self.build_button(channel, options, JSON.parse(cookie), rule); }else{ - session.rpc("/im_livechat/available", {db: db, channel: channel}).then(function(activated) { + user_session.rpc("/im_livechat/available", {db: db, channel: channel}).then(function(activated) { if(activated){ self.build_button(channel, options, false, rule); } @@ -154,11 +151,8 @@ var ChatButton = Widget.extend({ start: function() { this.$().append(QWeb.render("im_livechat.chatButton", {widget: this})); // set up the manager - this.manager = new im_chat.ConversationManager(this, this.options); + this.manager = new im_chat_common.ConversationManager(this, this.options); this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight()); - this.manager.notification = function(notif){ // override the notification default function - alert(notif); - }; if(this.session){ this.set_conversation(this.session); } @@ -166,17 +160,17 @@ var ChatButton = Widget.extend({ click: function() { var self = this; if (!this.conv){ - session.rpc("/im_livechat/get_session", { - "channel_id" : self.channel, + user_session.rpc("/im_livechat/get_session", { + "channel_id" : self.channel, "anonymous_name" : this.options.defaultUsername }, {shadow: true}).then(function(session) { if (! session) { - self.manager.notification(self.no_session_message); + alert(self.no_session_message); return; } session.state = 'open'; // save the session in a cookie - session.set_cookie(COOKIE_NAME, JSON.stringify(session), 60*60); + utils.set_cookie(COOKIE_NAME, JSON.stringify(session), 60*60); // create the conversation with the received session self.set_conversation(session, true); }); @@ -188,7 +182,7 @@ var ChatButton = Widget.extend({ if(session.state === 'closed'){ return; } - this.conv = this.manager.apply_session(session); + this.conv = this.manager.session_apply(session); this.conv.on("destroyed", this, function() { bus.bus.stop_polling(); delete self.conv; @@ -207,7 +201,7 @@ var ChatButton = Widget.extend({ if(this.session.users.length > 0){ if (self.options.defaultMessage) { setTimeout(function(){ - self.conv.received_message({ + self.conv.message_receive({ id : 1, type: "message", message: self.options.defaultMessage, @@ -229,7 +223,7 @@ var Feedback = Widget.extend({ this.conversation = parent; this.reason = false; this.rating = false; - this.server_origin = session.origin; + this.server_origin = user_session.origin; }, start: function(){ this._super.apply(this.arguments); @@ -263,7 +257,7 @@ var Feedback = Widget.extend({ _send_feedback: function(close){ var self = this; var uuid = this.conversation.get('session').uuid; - return session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) { + return user_session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) { if(close){ self.trigger("feedback_sent"); // will close the conversation } diff --git a/addons/im_livechat/views/im_livechat.xml b/addons/im_livechat/views/im_livechat.xml index 6f993cfef64..b13fe40772b 100644 --- a/addons/im_livechat/views/im_livechat.xml +++ b/addons/im_livechat/views/im_livechat.xml @@ -96,9 +96,36 @@ + + + + + + - + + + + + + + + + + + + + + + + @@ -109,7 +136,7 @@ - + @@ -118,23 +145,25 @@