7 var QWeb = openerp.qweb;
8 var NBR_LIMIT_HISTORY = 20;
10 var im_chat = openerp.im_chat = {};
12 im_chat.ConversationManager = openerp.Widget.extend({
13 init: function(parent, options) {
16 this.options = _.clone(options) || {};
17 _.defaults(this.options, {
18 inputPlaceholder: _t("Say something..."),
20 defaultUsername: _t("Visitor"),
24 this.bus = openerp.bus.bus;
25 this.bus.on("notification", this, this.on_notification);
26 this.bus.options["im_presence"] = true;
29 this.set("right_offset", 0);
30 this.set("bottom_offset", 0);
31 this.on("change:right_offset", this, this.calc_positions);
32 this.on("change:bottom_offset", this, this.calc_positions);
34 this.set("window_focus", true);
35 this.on("change:window_focus", self, function(e) {
36 self.bus.options["im_presence"] = self.get("window_focus");
38 this.set("waiting_messages", 0);
39 this.on("change:waiting_messages", this, this.window_title_change);
40 $(window).on("focus", _.bind(this.window_focus, this));
41 $(window).on("blur", _.bind(this.window_blur, this));
42 this.window_title_change();
44 on_notification: function(notification) {
46 var channel = notification[0];
47 var message = notification[1];
48 var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
50 // 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)
51 if((Array.isArray(channel) && (channel[1] === 'im_chat.session' || channel[1] === 'im_chat.presence')) || (regex_uuid.test(channel))){
52 // message to display in the chatview
53 if (message.type === "message" || message.type === "meta") {
54 self.received_message(message);
56 // activate the received session
58 this.apply_session(message);
60 // user status notification
61 if(message.im_status){
62 self.trigger("im_new_user_status", [message]);
67 // window focus unfocus beep and title
68 window_focus: function() {
69 this.set("window_focus", true);
70 this.set("waiting_messages", 0);
72 window_blur: function() {
73 this.set("window_focus", false);
75 window_beep: function() {
76 if (typeof(Audio) === "undefined") {
79 var audio = new Audio();
80 var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
81 var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
82 audio.src = openerp.session.url("/im_chat/static/src/audio/" + (kitten ? "purr" : "ting") + ext);
85 window_title_change: function() {
86 var title = undefined;
87 if (this.get("waiting_messages") !== 0) {
88 title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))
91 if (! openerp.webclient || !openerp.webclient.set_title_part)
93 openerp.webclient.set_title_part("im_messages", title);
96 apply_session: function(session, focus){
98 var conv = this.sessions[session.uuid];
100 if(session.state !== 'closed'){
101 conv = new im_chat.Conversation(this, this, session, this.options);
102 conv.appendTo($("body"));
103 conv.on("destroyed", this, _.bind(this.delete_session, this));
104 this.sessions[session.uuid] = conv;
105 this.calc_positions();
108 conv.set("session", session);
110 conv && this.trigger("im_session_activated", conv);
115 activate_session: function(session, focus) {
117 var active_session = _.clone(session);
118 active_session.state = 'open';
119 var conv = this.apply_session(active_session, focus);
120 if(session.state !== 'open'){
121 conv.update_fold_state('open');
125 delete_session: function(uuid){
126 delete this.sessions[uuid];
127 this.calc_positions();
129 received_message: function(message) {
131 var session_id = message.to_id[0];
132 var uuid = message.to_id[1];
133 if (! this.get("window_focus")) {
134 this.set("waiting_messages", this.get("waiting_messages") + 1);
136 var conv = this.sessions[uuid];
138 // fetch the session, and init it with the message
139 var def_session = new openerp.Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){
140 conv = self.activate_session(session, false);
141 conv.received_message(message);
144 conv.received_message(message);
147 calc_positions: function() {
149 var current = this.get("right_offset");
150 _.each(this.sessions, function(s) {
151 s.set("bottom_position", self.get("bottom_offset"));
152 s.set("right_position", current);
153 current += s.$().outerWidth(true);
156 destroy: function() {
157 $(window).off("unload", this.unload);
158 $(window).off("focus", this.window_focus);
159 $(window).off("blur", this.window_blur);
160 return this._super();
164 im_chat.Conversation = openerp.Widget.extend({
165 className: "openerp_style oe_im_chatview",
167 "keydown input": "keydown",
168 "click .oe_im_chatview_close": "click_close",
169 "click .oe_im_chatview_header": "click_header"
171 init: function(parent, c_manager, session, options) {
173 this.c_manager = c_manager;
174 this.options = options || {};
175 this.loading_history = true;
176 this.set("messages", []);
177 this.set("session", session);
178 this.set("right_position", 0);
179 this.set("bottom_position", 0);
180 this.set("pending", 0);
181 this.inputPlaceholder = this.options.defaultInputPlaceholder;
185 self.$().append(openerp.qweb.render("im_chat.Conversation", {widget: self}));
187 self.on("change:session", self, self.update_session);
188 self.on("change:right_position", self, self.calc_pos);
189 self.on("change:bottom_position", self, self.calc_pos);
190 self.full_height = self.$().height();
192 self.on("change:pending", self, _.bind(function() {
193 if (self.get("pending") === 0) {
194 self.$(".oe_im_chatview_nbr_messages").text("");
196 self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
200 self.on("change:messages", this, this.render_messages);
201 self.$('.oe_im_chatview_content').on('scroll',function(){
202 if($(this).scrollTop() === 0){
208 // prepare the header and the correct state
209 self.update_session();
213 height: this.full_height
215 this.set("pending", 0);
219 height: this.$(".oe_im_chatview_header").outerHeight()
222 calc_pos: function() {
223 this.$().css("right", this.get("right_position"));
224 this.$().css("bottom", this.get("bottom_position"));
226 update_fold_state: function(state){
227 return new openerp.Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state});
229 update_session: function(){
232 _.each(this.get("session").users, function(user){
233 if( (openerp.session.uid !== user.id) && !(_.isUndefined(openerp.session.uid) && !user.id) ){
234 names.push(user.name);
237 this.$(".oe_im_chatview_header_name").text(names.join(", "));
238 this.$(".oe_im_chatview_header_name").attr('title', names.join(", "));
239 // update the fold state
240 if(this.get("session").state){
241 if(this.get("session").state === 'closed'){
244 if(this.get("session").state === 'open'){
252 load_history: function(){
254 if(this.loading_history){
255 var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY};
256 var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false;
258 data["last_id"] = lastid;
260 openerp.session.rpc("/im_chat/history", data).then(function(messages){
262 self.insert_messages(messages);
263 if(messages.length != NBR_LIMIT_HISTORY){
264 self.loading_history = false;
267 self.loading_history = false;
272 received_message: function(message) {
273 if (this.get('session').state === 'open') {
274 this.set("pending", 0);
276 this.set("pending", this.get("pending") + 1);
278 this.insert_messages([message]);
280 send_message: function(message, type) {
282 var send_it = function() {
283 return openerp.session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
286 send_it().fail(function(error, e) {
293 insert_messages: function(messages){
295 // avoid duplicated messages
296 messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; });
297 // escape the message content and set the timezone
298 _.map(messages, function(m){
300 m.from_id = [false, self.options["defaultUsername"]];
302 m.message = self.escape_keep_url(m.message);
303 m.message = self.smiley(m.message);
304 m.create_date = Date.parse(m.create_date).setTimezone("UTC").toString("yyyy-MM-dd HH:mm:ss");
307 this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
309 render_messages: function(){
312 var last_date_day, last_user_id = -1;
313 _.each(this.get("messages"), function(current){
314 // add the url of the avatar for all users in the conversation
315 current.from_id[2] = openerp.session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0]));
316 var date_day = current.create_date.split(" ")[0];
317 if(date_day !== last_date_day){
321 last_date_day = date_day;
322 if(current.type == "message"){ // traditionnal message
323 if(last_user_id === current.from_id[0]){
324 _.last(res[date_day]).push(current);
326 res[date_day].push([current]);
328 last_user_id = current.from_id[0];
329 }else{ // meta message
330 res[date_day].push([current]);
334 // render and set the content of the chatview
335 this.$('.oe_im_chatview_content_bubbles').html($(openerp.qweb.render("im_chat.Conversation_content", {"list": res})));
338 keydown: function(e) {
339 if(e && e.which == 27) {
340 if(this.$el.prev().find('.oe_im_chatview_input').length > 0){
341 this.$el.prev().find('.oe_im_chatview_input').focus();
343 this.$el.next().find('.oe_im_chatview_input').focus();
346 this.update_fold_state('closed');
348 if(e && e.which !== 13) {
351 var mes = this.$("input").val();
355 this.$("input").val("");
356 this.send_message(mes, "message");
358 get_smiley_list: function(){
359 var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
373 ":pinky" : "<img src='/im_chat/static/src/img/pinky.png'/>",
374 ":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
388 smiley: function(str){
389 var re_escape = function(str){
390 return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
392 var smileys = this.get_smiley_list();
393 _.each(_.keys(smileys), function(key){
394 str = str.replace( new RegExp("(?:^|\\s)(" + re_escape(key) + ")(?:\\s|$)"), ' <span class="smiley">'+smileys[key]+'</span> ');
398 escape_keep_url: function(str){
399 var url_regex = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi;
403 var result = url_regex.exec(str);
406 txt += _.escape(str.slice(last, result.index));
407 last = url_regex.lastIndex;
408 var url = _.escape(result[0]);
409 txt += '<a href="' + url + '" target="_blank">' + url + '</a>';
411 txt += _.escape(str.slice(last, str.length));
414 _go_bottom: function() {
415 this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
417 add_user: function(user){
418 return new openerp.Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]);
421 this.$(".oe_im_chatview_input").focus();
423 click_header: function(){
424 this.update_fold_state();
426 click_close: function(event) {
427 event.stopPropagation();
428 this.update_fold_state('closed');
430 destroy: function() {
431 this.trigger("destroyed", this.get('session').uuid);
432 return this._super();
436 im_chat.UserWidget = openerp.Widget.extend({
437 "template": "im_chat.UserWidget",
439 "click": "activate_user",
441 init: function(parent, user) {
443 this.set("id", user.id);
444 this.set("name", user.name);
445 this.set("im_status", user.im_status);
446 this.set("image_url", user.image_url);
449 this.$el.data("user", {id:this.get("id"), name:this.get("name")});
450 this.$el.draggable({helper: "clone"});
451 this.on("change:im_status", this, this.update_status);
452 this.update_status();
454 update_status: function(){
455 this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline');
456 var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png');
457 this.$(".oe_im_user_online").attr('src', img_src);
459 activate_user: function() {
460 this.trigger("activate_user", this.get("id"));
464 im_chat.InstantMessaging = openerp.Widget.extend({
465 template: "im_chat.InstantMessaging",
467 "keydown .oe_im_searchbox": "input_change",
468 "keyup .oe_im_searchbox": "input_change",
469 "change .oe_im_searchbox": "input_change",
471 init: function(parent) {
474 this.set("right_offset", 0);
475 this.set("current_search", "");
479 this.c_manager = new openerp.im_chat.ConversationManager(this);
480 this.on("change:right_offset", this.c_manager, _.bind(function() {
481 this.c_manager.set("right_offset", this.get("right_offset"));
483 this.user_search_dm = new openerp.web.DropMisordered();
487 this.$el.css("right", -this.$el.outerWidth());
488 $(window).scroll(_.bind(this.calc_box, this));
489 $(window).resize(_.bind(this.calc_box, this));
492 this.on("change:current_search", this, this.search_users_status);
494 // add a drag & drop listener
495 self.c_manager.on("im_session_activated", self, function(conv) {
497 drop: function(event, ui) {
498 conv.add_user(ui.draggable.data("user"));
502 // add a listener for the update of users status
503 this.c_manager.on("im_new_user_status", this, this.update_users_status);
505 // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page)
506 openerp.session.rpc("/im_chat/init",{}).then(function(notifications) {
507 _.each(notifications, function(notif){
508 self.c_manager.on_notification(notif);
511 openerp.bus.bus.start_polling();
515 calc_box: function() {
516 var $topbar = window.$('#oe_main_menu_navbar'); // .oe_topbar is replaced with .navbar of bootstrap3
517 var top = $topbar.offset().top + $topbar.height();
518 top = Math.max(top - $(window).scrollTop(), 0);
519 this.$el.css("top", top);
520 this.$el.css("bottom", 0);
522 input_change: function() {
523 this.set("current_search", this.$(".oe_im_searchbox").val());
525 search_users_status: function(e) {
526 var user_model = new openerp.web.Model("res.users");
528 return this.user_search_dm.add(user_model.call("im_search", [this.get("current_search"),
529 USERS_LIMIT], {context:new openerp.web.CompoundContext()})).then(function(result) {
530 self.$(".oe_im_input").val("");
531 var old_widgets = self.widgets;
534 _.each(result, function(user) {
535 user.image_url = openerp.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user.id});
536 var widget = new openerp.im_chat.UserWidget(self, user);
537 widget.appendTo(self.$(".oe_im_users"));
538 widget.on("activate_user", self, self.activate_user);
539 self.widgets[user.id] = widget;
540 self.users.push(user);
542 _.each(old_widgets, function(w) {
547 switch_display: function() {
549 var fct = _.bind(function(place) {
550 this.set("right_offset", place + this.$el.outerWidth());
551 this.$(".oe_im_searchbox").focus();
558 right: -this.$el.outerWidth(),
561 if (! openerp.bus.bus.activated) {
562 this.do_warn("Instant Messaging is not activated on this server. Try later.", "");
565 // update the list of user status when show the IM
566 this.search_users_status();
571 this.shown = ! this.shown;
573 activate_user: function(user_id) {
575 var sessions = new openerp.web.Model("im_chat.session");
576 return sessions.call("session_get", [user_id]).then(function(session) {
577 self.c_manager.activate_session(session, true);
580 update_users_status: function(users_list){
582 _.each(users_list, function(el) {
583 self.widgets[el.id] && self.widgets[el.id].set("im_status", el.im_status);
588 im_chat.ImTopButton = openerp.Widget.extend({
589 template:'im_chat.ImTopButton',
593 clicked: function(ev) {
595 this.trigger("clicked");
599 if(openerp.web && openerp.web.UserMenu) {
600 openerp.web.UserMenu.include({
601 do_update: function(){
603 var Users = new openerp.web.Model('res.users');
604 Users.call('has_group', ['base.group_user']).done(function(is_employee) {
606 self.update_promise.then(function() {
607 var im = new openerp.im_chat.InstantMessaging(self);
608 openerp.im_chat.single = im;
609 im.appendTo(openerp.client.$el);
610 var button = new openerp.im_chat.ImTopButton(this);
611 button.on("clicked", im, im.switch_display);
612 button.appendTo(window.$('.oe_systray'));
616 return this._super.apply(this, arguments);