[MERGE] forward port of branch 8.0 up to e883193
[odoo/odoo.git] / addons / im_chat / static / src / js / im_chat.js
1 (function(){
2
3     "use strict";
4
5     var _t = openerp._t;
6     var _lt = openerp._lt;
7     var QWeb = openerp.qweb;
8     var NBR_LIMIT_HISTORY = 20;
9     var USERS_LIMIT = 20;
10     var im_chat = openerp.im_chat = {};
11
12     im_chat.ConversationManager = openerp.Widget.extend({
13         init: function(parent, options) {
14             var self = this;
15             this._super(parent);
16             this.options = _.clone(options) || {};
17             _.defaults(this.options, {
18                 inputPlaceholder: _t("Say something..."),
19                 defaultMessage: null,
20                 defaultUsername: _t("Visitor"),
21             });
22             // business
23             this.sessions = {};
24             this.bus = openerp.bus.bus;
25             this.bus.on("notification", this, this.on_notification);
26             this.bus.options["im_presence"] = true;
27
28             // ui
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);
33
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");
37             });
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();
43         },
44         on_notification: function(notification) {
45             var self = this;
46             var channel = notification[0];
47             var message = notification[1];
48             var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
49
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);
55                 }
56                 // activate the received session
57                 if(message.uuid){
58                     this.apply_session(message);
59                 }
60                 // user status notification
61                 if(message.im_status){
62                     self.trigger("im_new_user_status", [message]);
63                 }
64             }
65         },
66
67         // window focus unfocus beep and title
68         window_focus: function() {
69             this.set("window_focus", true);
70             this.set("waiting_messages", 0);
71         },
72         window_blur: function() {
73             this.set("window_focus", false);
74         },
75         window_beep: function() {
76             if (typeof(Audio) === "undefined") {
77                 return;
78             }
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);
83             audio.play();
84         },
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"))
89                 this.window_beep();
90             }
91             if (! openerp.webclient || !openerp.webclient.set_title_part)
92                 return;
93             openerp.webclient.set_title_part("im_messages", title);
94         },
95
96         apply_session: function(session, focus){
97             var self = this;
98             var conv = this.sessions[session.uuid];
99             if (! conv) {
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();
106                 }
107             }else{
108                 conv.set("session", session);
109             }
110             conv && this.trigger("im_session_activated", conv);
111             if (focus)
112                 conv.focus();
113             return conv;
114         },
115         activate_session: function(session, focus) {
116             var self = this;
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');
122             }
123             return conv;
124         },
125         delete_session: function(uuid){
126             delete this.sessions[uuid];
127             this.calc_positions();
128         },
129         received_message: function(message) {
130             var self = this;
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);
135             }
136             var conv = this.sessions[uuid];
137             if(!conv){
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);
142                 });
143             }else{
144                 conv.received_message(message);
145             }
146         },
147         calc_positions: function() {
148             var self = this;
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);
154             });
155         },
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();
161         }
162     });
163
164     im_chat.Conversation = openerp.Widget.extend({
165         className: "openerp_style oe_im_chatview",
166         events: {
167             "keydown input": "keydown",
168             "click .oe_im_chatview_close": "click_close",
169             "click .oe_im_chatview_header": "click_header"
170         },
171         init: function(parent, c_manager, session, options) {
172             this._super(parent);
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;
182         },
183         start: function() {
184             var self = this;
185             self.$().append(openerp.qweb.render("im_chat.Conversation", {widget: self}));
186             self.$().hide();
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();
191             self.calc_pos();
192             self.on("change:pending", self, _.bind(function() {
193                 if (self.get("pending") === 0) {
194                     self.$(".oe_im_chatview_nbr_messages").text("");
195                 } else {
196                     self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
197                 }
198             }, self));
199             // messages business
200             self.on("change:messages", this, this.render_messages);
201             self.$('.oe_im_chatview_content').on('scroll',function(){
202                 if($(this).scrollTop() === 0){
203                     self.load_history();
204                 }
205             });
206             self.load_history();
207             self.$().show();
208             // prepare the header and the correct state
209             self.update_session();
210         },
211         show: function(){
212             this.$().animate({
213                 height: this.full_height
214             });
215             this.set("pending", 0);
216         },
217         hide: function(){
218             this.$().animate({
219                 height: this.$(".oe_im_chatview_header").outerHeight()
220             });
221         },
222         calc_pos: function() {
223             this.$().css("right", this.get("right_position"));
224             this.$().css("bottom", this.get("bottom_position"));
225         },
226         update_fold_state: function(state){
227             return new openerp.Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state});
228         },
229         update_session: function(){
230             // built the name
231             var names = [];
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);
235                 }
236             });
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'){
242                     this.destroy();
243                 }else{
244                     if(this.get("session").state === 'open'){
245                         this.show();
246                     }else{
247                         this.hide();
248                     }
249                 }
250             }
251         },
252         load_history: function(){
253             var self = this;
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;
257                 if(lastid){
258                     data["last_id"] = lastid;
259                 }
260                 openerp.session.rpc("/im_chat/history", data).then(function(messages){
261                     if(messages){
262                         self.insert_messages(messages);
263                                         if(messages.length != NBR_LIMIT_HISTORY){
264                             self.loading_history = false;
265                         }
266                     }else{
267                         self.loading_history = false;
268                     }
269                 });
270             }
271         },
272         received_message: function(message) {
273             if (this.get('session').state === 'open') {
274                 this.set("pending", 0);
275             } else {
276                 this.set("pending", this.get("pending") + 1);
277             }
278             this.insert_messages([message]);
279         },
280         send_message: function(message, type) {
281             var self = this;
282             var send_it = function() {
283                 return openerp.session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
284             };
285             var tries = 0;
286             send_it().fail(function(error, e) {
287                 e.preventDefault();
288                 tries += 1;
289                 if (tries < 3)
290                     return send_it();
291             });
292         },
293         insert_messages: function(messages){
294                 var self = this;
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){
299                 if(!m.from_id){
300                     m.from_id = [false, self.options["defaultUsername"]];
301                 }
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");
305                 return m;
306             });
307                 this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
308         },
309         render_messages: function(){
310             var self = this;
311             var res = {};
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){
318                     res[date_day] = [];
319                     last_user_id = -1;
320                 }
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);
325                     }else{
326                         res[date_day].push([current]);
327                     }
328                     last_user_id = current.from_id[0];
329                 }else{ // meta message
330                     res[date_day].push([current]);
331                     last_user_id = -1;
332                 }
333             });
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})));
336             this._go_bottom();
337         },
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();
342                 }else{
343                     this.$el.next().find('.oe_im_chatview_input').focus();
344                 }
345                 e.stopPropagation();
346                 this.update_fold_state('closed');
347             }
348             if(e && e.which !== 13) {
349                 return;
350             }
351             var mes = this.$("input").val();
352             if (! mes.trim()) {
353                 return;
354             }
355             this.$("input").val("");
356             this.send_message(mes, "message");
357         },
358         get_smiley_list: function(){
359             var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
360             var smileys = {
361                 ":'(": "&#128546;",
362                 ":O" : "&#128561;",
363                 "3:)": "&#128520;",
364                 ":)" : "&#128522;",
365                 ":D" : "&#128517;",
366                 ";)" : "&#128521;",
367                 ":p" : "&#128523;",
368                 ":(" : "&#9785;",
369                 ":|" : "&#128528;",
370                 ":/" : "&#128527;",
371                 "8)" : "&#128563;",
372                 ":s" : "&#128534;",
373                 ":pinky" : "<img src='/im_chat/static/src/img/pinky.png'/>",
374                 ":musti" : "<img src='/im_chat/static/src/img/musti.png'/>",
375             };
376             if(kitten){
377                 _.extend(smileys, {
378                     ":)" : "&#128570;",
379                     ":D" : "&#128569;",
380                     ";)" : "&#128572;",
381                     ":p" : "&#128573;",
382                     ":(" : "&#128576;",
383                     ":|" : "&#128575;",
384                 });
385             }
386             return smileys;
387         },
388         smiley: function(str){
389             var re_escape = function(str){
390                 return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
391              };
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> ');
395             });
396             return str;
397         },
398         escape_keep_url: function(str){
399             var url_regex = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/gi;
400             var last = 0;
401             var txt = "";
402             while (true) {
403                 var result = url_regex.exec(str);
404                 if (! result)
405                     break;
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>';
410             }
411             txt += _.escape(str.slice(last, str.length));
412             return txt;
413         },
414         _go_bottom: function() {
415             this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
416         },
417         add_user: function(user){
418             return new openerp.Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]);
419         },
420         focus: function() {
421             this.$(".oe_im_chatview_input").focus();
422         },
423         click_header: function(){
424             this.update_fold_state();
425         },
426         click_close: function(event) {
427             event.stopPropagation();
428             this.update_fold_state('closed');
429         },
430         destroy: function() {
431             this.trigger("destroyed", this.get('session').uuid);
432             return this._super();
433         }
434     });
435
436     im_chat.UserWidget = openerp.Widget.extend({
437         "template": "im_chat.UserWidget",
438         events: {
439             "click": "activate_user",
440         },
441         init: function(parent, user) {
442             this._super(parent);
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);
447         },
448         start: function() {
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();
453         },
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);
458         },
459         activate_user: function() {
460             this.trigger("activate_user", this.get("id"));
461         },
462     });
463
464     im_chat.InstantMessaging = openerp.Widget.extend({
465         template: "im_chat.InstantMessaging",
466         events: {
467             "keydown .oe_im_searchbox": "input_change",
468             "keyup .oe_im_searchbox": "input_change",
469             "change .oe_im_searchbox": "input_change",
470         },
471         init: function(parent) {
472             this._super(parent);
473             this.shown = false;
474             this.set("right_offset", 0);
475             this.set("current_search", "");
476             this.users = [];
477             this.widgets = {};
478
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"));
482             }, this));
483             this.user_search_dm = new openerp.web.DropMisordered();
484         },
485         start: function() {
486             var self = this;
487             this.$el.css("right", -this.$el.outerWidth());
488             $(window).scroll(_.bind(this.calc_box, this));
489             $(window).resize(_.bind(this.calc_box, this));
490             this.calc_box();
491
492             this.on("change:current_search", this, this.search_users_status);
493
494             // add a drag & drop listener
495             self.c_manager.on("im_session_activated", self, function(conv) {
496                 conv.$el.droppable({
497                     drop: function(event, ui) {
498                         conv.add_user(ui.draggable.data("user"));
499                     }
500                 });
501             });
502             // add a listener for the update of users status
503             this.c_manager.on("im_new_user_status", this, this.update_users_status);
504
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);
509                 });
510                 // start polling
511                 openerp.bus.bus.start_polling();
512             });
513             return;
514         },
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);
521         },
522         input_change: function() {
523             this.set("current_search", this.$(".oe_im_searchbox").val());
524         },
525         search_users_status: function(e) {
526             var user_model = new openerp.web.Model("res.users");
527             var self = this;
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;
532                 self.widgets = {};
533                 self.users = [];
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);
541                 });
542                 _.each(old_widgets, function(w) {
543                     w.destroy();
544                 });
545             });
546         },
547         switch_display: function() {
548             this.calc_box();
549             var fct =  _.bind(function(place) {
550                 this.set("right_offset", place + this.$el.outerWidth());
551                 this.$(".oe_im_searchbox").focus();
552             }, this);
553             var opt = {
554                 step: fct,
555             };
556             if (this.shown) {
557                 this.$el.animate({
558                     right: -this.$el.outerWidth(),
559                 }, opt);
560             } else {
561                 if (! openerp.bus.bus.activated) {
562                     this.do_warn("Instant Messaging is not activated on this server. Try later.", "");
563                     return;
564                 }
565                 // update the list of user status when show the IM
566                 this.search_users_status();
567                 this.$el.animate({
568                     right: 0,
569                 }, opt);
570             }
571             this.shown = ! this.shown;
572         },
573         activate_user: function(user_id) {
574             var self = this;
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);
578             });
579         },
580         update_users_status: function(users_list){
581             var self = this;
582             _.each(users_list, function(el) {
583                 self.widgets[el.id] && self.widgets[el.id].set("im_status", el.im_status);
584             });
585         }
586     });
587
588     im_chat.ImTopButton = openerp.Widget.extend({
589         template:'im_chat.ImTopButton',
590         events: {
591             "click": "clicked",
592         },
593         clicked: function(ev) {
594             ev.preventDefault();
595             this.trigger("clicked");
596         },
597     });
598
599     if(openerp.web && openerp.web.UserMenu) {
600         openerp.web.UserMenu.include({
601             do_update: function(){
602                 var self = this;
603                 var Users = new openerp.web.Model('res.users');
604                 Users.call('has_group', ['base.group_user']).done(function(is_employee) {
605                     if (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'));
613                         });
614                     }
615                 });
616                 return this._super.apply(this, arguments);
617             },
618         });
619     }
620
621     return im_chat;
622 })();