708c9cd25f09f8c7d36b359a642b8e1915ed1b9b
[odoo/odoo.git] / addons / im / static / src / js / im_common.js
1
2 /*
3     This file must compile in EcmaScript 3 and work in IE7.
4
5     Prerequisites to use this module:
6     - load the im_common.xml qweb template into openerp.qweb
7     - implement all the stuff defined later
8 */
9
10 (function() {
11
12 function declare($, _, openerp) {
13     /* jshint es3: true */
14     "use strict";
15
16     var im_common = {};
17
18     /*
19         All of this must be defined to use this module
20     */
21     _.extend(im_common, {
22         notification: function(message) {
23             throw new Error("Not implemented");
24         },
25         connection: null
26     });
27
28     var _t = openerp._t;
29
30     var ERROR_DELAY = 5000;
31
32     im_common.ImUser = openerp.Class.extend(openerp.PropertiesMixin, {
33         init: function(parent, user_rec) {
34             openerp.PropertiesMixin.init.call(this, parent);
35             
36             user_rec.image_url = im_common.connection.url('/web/binary/image', {model:'im.user', field: 'image', id: user_rec.id});
37
38             this.set(user_rec);
39             this.set("watcher_count", 0);
40             this.on("change:watcher_count", this, function() {
41                 if (this.get("watcher_count") === 0)
42                     this.destroy();
43             });
44         },
45         destroy: function() {
46             this.trigger("destroyed");
47             openerp.PropertiesMixin.destroy.call(this);
48         },
49         add_watcher: function() {
50             this.set("watcher_count", this.get("watcher_count") + 1);
51         },
52         remove_watcher: function() {
53             this.set("watcher_count", this.get("watcher_count") - 1);
54         }
55     });
56
57     im_common.ConversationManager = openerp.Class.extend(openerp.PropertiesMixin, {
58         init: function(parent, options) {
59             openerp.PropertiesMixin.init.call(this, parent);
60             this.options = _.clone(options) || {};
61             _.defaults(this.options, {
62                 inputPlaceholder: _t("Say something..."),
63                 defaultMessage: null,
64                 userName: _t("Anonymous"),
65                 anonymous_mode: false
66             });
67             this.set("right_offset", 0);
68             this.set("bottom_offset", 0);
69             this.conversations = [];
70             this.on("change:right_offset", this, this.calc_positions);
71             this.on("change:bottom_offset", this, this.calc_positions);
72             this.set("window_focus", true);
73             this.set("waiting_messages", 0);
74             this.focus_hdl = _.bind(function() {
75                 this.set("window_focus", true);
76             }, this);
77             $(window).bind("focus", this.focus_hdl);
78             this.blur_hdl = _.bind(function() {
79                 this.set("window_focus", false);
80             }, this);
81             $(window).bind("blur", this.blur_hdl);
82             this.on("change:window_focus", this, this.window_focus_change);
83             this.window_focus_change();
84             this.on("change:waiting_messages", this, this.messages_change);
85             this.messages_change();
86             this.create_ting();
87             this.activated = false;
88             this.users_cache = {};
89             this.last = null;
90             this.unload_event_handler = _.bind(this.unload, this);
91         },
92         start_polling: function() {
93             var self = this;
94             var def = $.when();
95             var uuid = false;
96
97             if (this.options.anonymous_mode) {
98                 uuid = localStorage["oe_livesupport_uuid"] || false;
99
100                 if (! uuid) {
101                     def = im_common.connection.rpc("/longpolling/im/gen_uuid", {}).then(function(my_uuid) {
102                         uuid = my_uuid;
103                         localStorage["oe_livesupport_uuid"] = uuid;
104                     });
105                 }
106                 def = def.then(function() {
107                     return im_common.connection.model("im.user").call("assign_name", [uuid, self.options.userName]);
108                 });
109             }
110
111             return def.then(function() {
112                 return im_common.connection.model("im.user").call("get_my_id", [uuid]);
113             }).then(function(my_user_id) {
114                 self.my_id = my_user_id;
115                 return self.ensure_users([self.my_id]);
116             }).then(function() {
117                 var me = self.users_cache[self.my_id];
118                 delete self.users_cache[self.my_id];
119                 self.me = me;
120                 me.set("name", _t("You"));
121                 return im_common.connection.rpc("/longpolling/im/activated", {}, {shadow: true});
122             }).then(function(activated) {
123                 if (activated) {
124                     self.activated = true;
125                     $(window).on("unload", self.unload_event_handler);
126                     self.poll();
127                 } else {
128                     return $.Deferred().reject();
129                 }
130             }, function(a, e) {
131                 e.preventDefault();
132             });
133         },
134         unload: function() {
135             return im_common.connection.model("im.user").call("im_disconnect", [], {uuid: this.me.get("uuid"), context: {}});
136         },
137         ensure_users: function(user_ids) {
138             var no_cache = {};
139             _.each(user_ids, function(el) {
140                 if (! this.users_cache[el])
141                     no_cache[el] = el;
142             }, this);
143             var self = this;
144             if (_.size(no_cache) === 0)
145                 return $.when();
146             else
147                 return im_common.connection.model("im.user").call("read", [_.values(no_cache), []]).then(function(users) {
148                     self.add_to_user_cache(users);
149                 });
150         },
151         add_to_user_cache: function(user_recs) {
152             _.each(user_recs, function(user_rec) {
153                 if (! this.users_cache[user_rec.id]) {
154                     var user = new im_common.ImUser(this, user_rec);
155                     this.users_cache[user_rec.id] = user;
156                     user.on("destroyed", this, function() {
157                         delete this.users_cache[user_rec.id];
158                     });
159                 }
160             }, this);
161         },
162         get_user: function(user_id) {
163             return this.users_cache[user_id];
164         },
165         poll: function() {
166             var self = this;
167             var user_ids = _.map(this.users_cache, function(el) {
168                 return el.get("id");
169             });
170             im_common.connection.rpc("/longpolling/im/poll", {
171                 last: this.last,
172                 users_watch: user_ids,
173                 uuid: self.me.get("uuid")
174             }, {shadow: true}).then(function(result) {
175                 _.each(result.users_status, function(el) {
176                     if (self.get_user(el.id))
177                         self.get_user(el.id).set(el);
178                 });
179                 self.last = result.last;
180                 self.received_messages(result.res).then(function() {
181                     self.poll();
182                 });
183             }, function(unused, e) {
184                 e.preventDefault();
185                 setTimeout(_.bind(self.poll, self), ERROR_DELAY);
186             });
187         },
188         get_activated: function() {
189             return this.activated;
190         },
191         create_ting: function() {
192             if (typeof(Audio) === "undefined") {
193                 this.ting = {play: function() {}};
194                 return;
195             }
196             var kitten = jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
197             this.ting = new Audio(im_common.connection.url(
198                 "/im/static/src/audio/" +
199                 (kitten ? "purr" : "Ting") +
200                 (new Audio().canPlayType("audio/ogg; codecs=vorbis") ? ".ogg": ".mp3")
201             ));
202         },
203         window_focus_change: function() {
204             if (this.get("window_focus")) {
205                 this.set("waiting_messages", 0);
206             }
207         },
208         messages_change: function() {
209             if (! openerp.webclient || !openerp.webclient.set_title_part)
210                 return;
211             openerp.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
212                 _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));
213         },
214         activate_session: function(session_id, focus) {
215             var conv = _.find(this.conversations, function(conv) {return conv.session_id == session_id;});
216             var def = $.when();
217             if (! conv) {
218                 conv = new im_common.Conversation(this, this, session_id, this.options);
219                 def = conv.appendTo($("body")).then(_.bind(function() {
220                     conv.on("destroyed", this, function() {
221                         this.conversations = _.without(this.conversations, conv);
222                         this.calc_positions();
223                     });
224                     this.conversations.push(conv);
225                     this.calc_positions();
226                     this.trigger("new_conversation", conv);
227                 }, this));
228             }
229             if (focus) {
230                 def = def.then(function() {
231                     conv.focus();
232                 });
233             }
234             return def.then(function() {return conv});
235         },
236         received_messages: function(messages) {
237             var self = this;
238             if (! this.get("window_focus") && messages.length >= 1) {
239                 this.set("waiting_messages", this.get("waiting_messages") + messages.length);
240                 this.ting.play();
241                 this.create_ting();
242             }
243             var defs = [];
244             _.each(messages, function(message) {
245                 defs.push(self.activate_session(message.session_id[0]).then(function(conv) {
246                     return conv.received_message(message);
247                 }));
248             });
249             return $.when.apply($, defs);
250         },
251         calc_positions: function() {
252             var current = this.get("right_offset");
253             _.each(_.range(this.conversations.length), function(i) {
254                 this.conversations[i].set("bottom_position", this.get("bottom_offset"));
255                 this.conversations[i].set("right_position", current);
256                 current += this.conversations[i].$().outerWidth(true);
257             }, this);
258         },
259         destroy: function() {
260             $(window).off("unload", this.unload_event_handler);
261             $(window).unbind("blur", this.blur_hdl);
262             $(window).unbind("focus", this.focus_hdl);
263             openerp.PropertiesMixin.destroy.call(this);
264         }
265     });
266
267     im_common.Conversation = openerp.Widget.extend({
268         className: "openerp_style oe_im_chatview",
269         events: {
270             "keydown input": "send_message",
271             "click .oe_im_chatview_close": "destroy",
272             "click .oe_im_chatview_header": "show_hide"
273         },
274         init: function(parent, c_manager, session_id, options) {
275             this._super(parent);
276             this.c_manager = c_manager;
277             this.options = options || {};
278             this.session_id = session_id;
279             this.set("right_position", 0);
280             this.set("bottom_position", 0);
281             this.shown = true;
282             this.set("pending", 0);
283             this.inputPlaceholder = this.options.defaultInputPlaceholder;
284             this.set("users", []);
285             this.set("disconnected", false);
286             this.others = [];
287         },
288         start: function() {
289             var self = this;
290
291             self.$().append(openerp.qweb.render("im_common.conversation", {widget: self}));
292
293             var change_status = function() {
294                 var disconnected = _.every(this.get("users"), function(u) { return u.get("im_status") === false; });
295                 self.set("disconnected", disconnected);
296                 this.$(".oe_im_chatview_users").html(openerp.qweb.render("im_common.conversation.header",
297                     {widget: self, to_url: _.bind(im_common.connection.url, im_common.connection)}));
298             };
299             this.on("change:users", this, function(unused, ev) {
300                 _.each(ev.oldValue, function(user) {
301                     user.off("change:im_status", self, change_status);
302                 });
303                 _.each(ev.newValue, function(user) {
304                     user.on("change:im_status", self, change_status);
305                 });
306                 change_status.call(self);
307             });
308             this.on("change:disconnected", this, function() {
309                 self.$().toggleClass("oe_im_chatview_disconnected_status", this.get("disconnected"));
310                 self._go_bottom();
311             });
312
313             var user_ids;
314             return im_common.connection.model("im.session").call("read", [self.session_id]).then(function(session) {
315                 user_ids = _.without(session.user_ids, self.c_manager.me.get("id"));
316                 return self.c_manager.ensure_users(session.user_ids);
317             }).then(function() {
318                 var users = _.map(user_ids, function(id) {return self.c_manager.get_user(id);});
319                 _.each(users, function(user) {
320                     user.add_watcher();
321                 });
322                 self.set("users", users);
323
324                 self.on("change:right_position", self, self.calc_pos);
325                 self.on("change:bottom_position", self, self.calc_pos);
326                 self.full_height = self.$().height();
327                 self.calc_pos();
328                 self.on("change:pending", self, _.bind(function() {
329                     if (self.get("pending") === 0) {
330                         self.$(".oe_im_chatview_nbr_messages").text("");
331                     } else {
332                         self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
333                     }
334                 }, self));
335             });
336         },
337         show_hide: function() {
338             if (this.shown) {
339                 this.$().animate({
340                     height: this.$(".oe_im_chatview_header").outerHeight()
341                 });
342             } else {
343                 this.$().animate({
344                     height: this.full_height
345                 });
346             }
347             this.shown = ! this.shown;
348             if (this.shown) {
349                 this.set("pending", 0);
350             }
351         },
352         calc_pos: function() {
353             this.$().css("right", this.get("right_position"));
354             this.$().css("bottom", this.get("bottom_position"));
355         },
356         received_message: function(message) {
357             if (this.shown) {
358                 this.set("pending", 0);
359             } else {
360                 this.set("pending", this.get("pending") + 1);
361             }
362             this.c_manager.ensure_users([message.from_id[0]]).then(_.bind(function() {
363                 var user = this.c_manager.get_user(message.from_id[0]);
364                 if (! _.contains(this.get("users"), user) && ! _.contains(this.others, user)) {
365                     this.others.push(user);
366                     user.add_watcher();
367                 }
368                 this._add_bubble(user, message.message, openerp.str_to_datetime(message.date));
369             }, this));
370         },
371         send_message: function(e) {
372             if(e && e.which !== 13) {
373                 return;
374             }
375             var mes = this.$("input").val();
376             if (! mes.trim()) {
377                 return;
378             }
379             this.$("input").val("");
380             var send_it = _.bind(function() {
381                 var model = im_common.connection.model("im.message");
382                 return model.call("post", [mes, this.session_id], {uuid: this.c_manager.me.get("uuid"), context: {}});
383             }, this);
384             var tries = 0;
385             send_it().then(_.bind(function() {}, function(error, e) {
386                 e.preventDefault();
387                 tries += 1;
388                 if (tries < 3)
389                     return send_it();
390             }));
391         },
392         _add_bubble: function(user, item, date) {
393             var items = [item];
394             if (user === this.last_user) {
395                 this.last_bubble.remove();
396                 items = this.last_items.concat(items);
397             }
398             this.last_user = user;
399             this.last_items = items;
400             var zpad = function(str, size) {
401                 str = "" + str;
402                 return new Array(size - str.length + 1).join('0') + str;
403             };
404             date = "" + zpad(date.getHours(), 2) + ":" + zpad(date.getMinutes(), 2);
405             
406             this.last_bubble = $(openerp.qweb.render("im_common.conversation_bubble", {"items": items, "user": user, "time": date}));
407             $(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble);
408             this._go_bottom();
409         },
410         _go_bottom: function() {
411             this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
412         },
413         add_user: function(user) {
414             if (user === this.me || _.contains(this.get("users"), user))
415                 return;
416             im_common.connection.model("im.session").call("add_to_session",
417                     [this.session_id, user.get("id"), this.c_manager.me.get("uuid")]).then(_.bind(function() {
418                 if (_.contains(this.others, user)) {
419                     this.others = _.without(this.others, user);
420                 } else {
421                     user.add_watcher();
422                 }
423                 this.set("users", this.get("users").concat([user]));
424             }, this));
425         },
426         focus: function() {
427             this.$(".oe_im_chatview_input").focus();
428             if (! this.shown)
429                 this.show_hide();
430         },
431         destroy: function() {
432             _.each(this.get("users"), function(user) {
433                 user.remove_watcher();
434             })
435             _.each(this.others, function(user) {
436                 user.remove_watcher();
437             })
438             this.trigger("destroyed");
439             return this._super();
440         }
441     });
442
443     return im_common;
444 }
445
446 if (typeof(define) !== "undefined") {
447     define(["jquery", "underscore", "openerp"], declare);
448 } else {
449     window.im_common = declare($, _, openerp);
450 }
451
452 })();