PageRenderTime 30ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/jschat/js/jschat.js

https://github.com/sigmonky/LivingRoom
JavaScript | 476 lines | 383 code | 33 blank | 60 comment | 53 complexity | 947d268442e80cc56bdb64850e19d26e MD5 | raw file
  1. //
  2. // Declare namespace
  3. Jschat = {};
  4. //
  5. //Models
  6. //=======
  7. // This is tool from [JavascriptMVC](http://javascriptmvc.com/) framework.
  8. // It used to create binded to `this` callbacks, when _.bind() can not do this.
  9. Jschat.JsmvcCallback = {
  10. callback: function( funcs ) {
  11. var makeArray = $.makeArray,
  12. isFunction = $.isFunction,
  13. isArray = $.isArray,
  14. extend = $.extend,
  15. concatArgs = function(arr, args){
  16. return arr.concat(makeArray(args));
  17. };
  18. // args that should be curried
  19. var args = makeArray(arguments),
  20. self;
  21. funcs = args.shift();
  22. if (!$.isArray(funcs) ) {
  23. funcs = [funcs];
  24. }
  25. self = this;
  26. for( var i =0; i< funcs.length;i++ ) {
  27. if(typeof funcs[i] == "string" && !isFunction(this[funcs[i]])){
  28. throw ("class.js does not have a "+funcs[i]+" method!");
  29. }
  30. }
  31. return function class_cb() {
  32. var cur = concatArgs(args, arguments),
  33. isString,
  34. length = funcs.length,
  35. f = 0,
  36. func;
  37. for (; f < length; f++ ) {
  38. func = funcs[f];
  39. if (!func ) {
  40. continue;
  41. }
  42. isString = typeof func == "string";
  43. if ( isString && self._set_called ) {
  44. self.called = func;
  45. }
  46. cur = (isString ? self[func] : func).apply(self, cur || []);
  47. if ( f < length - 1 ) {
  48. cur = !isArray(cur) || cur._use_call ? [cur] : cur;
  49. }
  50. }
  51. return cur;
  52. };
  53. }
  54. };
  55. //Contact model
  56. //--------------
  57. //Presence updated with `updatePrecense`. While updating,
  58. //program selects best status from `Jschat.Contact.Statuses`
  59. Jschat.Contact = Backbone.Model.extend({
  60. updatePrecense: function(presence){
  61. var status;
  62. if ($(presence).attr('type')) {
  63. status = $(presence).attr('type');
  64. } else {
  65. if ($(presence).find('show').length) {
  66. status = $(presence).find('show').text();
  67. } else {
  68. status = 'available';
  69. }
  70. }
  71. if (_.indexOf(Jschat.Contact.Statuses, status) > _.indexOf(Jschat.Contact.Statuses, this.status)) {
  72. this.set({status: status});
  73. }
  74. }
  75. });
  76. Jschat.Contact.Statuses = ['unavailable', 'xa', 'dnd', 'away', 'available', 'chat'];
  77. //Roster model
  78. //-------------
  79. //
  80. //It has special method to hold information about
  81. //started conversation, current manager and so on.
  82. Jschat.Roster = Backbone.Collection.extend({
  83. initialize: function(){
  84. // While conversation started, program should keep messaging only with
  85. // selected manager
  86. this._freezeManager = false;
  87. this.manager = null;
  88. this.bind('change:status', function(contact, new_status){
  89. this.updateManager();
  90. });
  91. this.bind('add', function(){
  92. this.updateManager();
  93. });
  94. // all of the object's function properties will be bound to ``this``.
  95. _.bindAll(this);
  96. },
  97. // public method
  98. freezeManager: function(){
  99. this._freezeManager = true;
  100. },
  101. updateManager: function(){
  102. if (!this._freezeManager) {
  103. this.manager = this.reduce(function(old_val, new_val){
  104. // First available:
  105. if (old_val === null){
  106. return new_val;
  107. }
  108. var new_status = _.indexOf(Jschat.Contact.Statuses, new_val.get('status')),
  109. old_status = _.indexOf(Jschat.Contact.Statuses, old_val.get('status'));
  110. if (new_status >= old_status){
  111. return new_val;
  112. } else {
  113. return old_val;
  114. }
  115. }, this.manager);
  116. }
  117. },
  118. model: Jschat.Contact
  119. });
  120. //Static method to create rosters from XMPP stanzas
  121. Jschat.Roster.serializeRoster = function(roster){
  122. res = [];
  123. $(roster).find('item').each(function(index, el){
  124. if ($(el).attr('subscription') === 'both'){
  125. res.push({
  126. jid: $(el).attr('jid'),
  127. bare_jid: Strophe.getBareJidFromJid($(el).attr('jid')),
  128. name: $(el).attr('name'),
  129. status: 'unavailable'
  130. });
  131. };
  132. });
  133. return res;
  134. };
  135. //Message model
  136. //--------------
  137. //
  138. //Message can automatically detect direction by calling
  139. //`message.incoming()`
  140. Jschat.Message = Backbone.Model.extend({
  141. incoming: function(){
  142. var to = Strophe.getBareJidFromJid(this.to),
  143. myjid = Strophe.getBareJidFromJid(this.myjid);
  144. if (myjid === to) {
  145. return true;
  146. } else {
  147. return false;
  148. }
  149. },
  150. send: function(connection){
  151. connection.send($msg({
  152. to: this.get('to'),
  153. "type": 'chat'
  154. }).c('body').t(this.get('text')));
  155. return this;
  156. }
  157. });
  158. Jschat.ChatLog = Backbone.Collection.extend({
  159. model: Jschat.Message
  160. });
  161. //
  162. //Views
  163. //=====
  164. //
  165. //Template for chat history
  166. Jschat.message_template = Handlebars.compile('<div class="message {{#incoming }}in{{/incoming}}{{^incoming }}out{{/incoming}}">'+
  167. '<div class="nick">{{#incoming }}{{ from }}{{/incoming}}{{^incoming }}You:{{/incoming}}</div>'+
  168. '<div class="text">{{ text }}</div></div>');
  169. //Template for welcome message
  170. Jschat.welcome_template = Handlebars.compile('Name: {{ name }}, Email: {{ email }}');
  171. Jschat.viewstates = {
  172. offline: 0,
  173. connecting: 1,
  174. online: 2
  175. };
  176. //Chat view
  177. //----------
  178. //
  179. //Main view in module. It handles everything user action in chat:
  180. //Opening chat, sending messages, closing chat
  181. Jschat.ChatView = Backbone.View.extend({
  182. initialize: function(){
  183. this.status = Jschat.viewstates.offline; // Default status
  184. this.send_on_enter = true;
  185. this.msgValid = false; // Require both filled Name and text before send message
  186. this.bind('change:status', this.onStatusChange);
  187. this.bind('change:msgValid', this.onMsgValidChange);
  188. this.trigger('change:status');
  189. this.bind('add:message', this.onMessageAdd);
  190. _.bindAll(this);
  191. },
  192. render: function(){
  193. this.el.show('200');
  194. },
  195. events: {
  196. 'click #id_close': 'destroy',
  197. 'focusin input,textarea': 'focusin',
  198. 'focusout input,textarea': 'focusout',
  199. 'change input,textarea': 'onFormChange',
  200. 'keyup input,textarea': 'onFormChange',
  201. 'keyup textarea': 'onKeyUp',
  202. 'click #id_send': 'sendMsg'
  203. },
  204. destroy: function(){
  205. this.el.hide('200');
  206. },
  207. focusin: function(ev){
  208. $('label[for=' + $(ev.target).attr('id') + ']').hide();
  209. },
  210. focusout: function(ev){
  211. if ($(ev.target).val() === '') {
  212. $('label[for=' + $(ev.target).attr('id') + ']').show();
  213. }
  214. },
  215. onFormChange: function(){
  216. if ((this.el.find('#id_full_name').val().length > 0)
  217. && (this.el.find('#id_text').val().length > 0)) {
  218. this.msgValid = true;
  219. this.trigger('change:msgValid');
  220. } else {
  221. this.msgValid = false;
  222. this.trigger('change:msgValid');
  223. }
  224. },
  225. onKeyUp: function(ev){
  226. if((ev.keyCode == 13) && (this.send_on_enter)){
  227. this.sendMsg(ev);
  228. }
  229. },
  230. getUserinfo: function(){
  231. return {
  232. 'name': this.el.find('#id_full_name').val(),
  233. 'email': this.el.find('#id_email').val()
  234. };
  235. },
  236. setStatus: function(new_status){
  237. switch(new_status){
  238. case Jschat.viewstates.offline:
  239. this.status = Jschat.viewstates.offline;
  240. break;
  241. case Jschat.viewstates.connecting:
  242. this.status = Jschat.viewstates.connecting;
  243. break;
  244. case Jschat.viewstates.online:
  245. this.status = Jschat.viewstates.online;
  246. break;
  247. }
  248. this.trigger('change:status');
  249. },
  250. sendMsg: function(ev){
  251. ev.preventDefault();
  252. // check if form is valid
  253. if (this.status === Jschat.viewstates.online && this.msgValid) {
  254. this.trigger('send:message', this.el.find('textarea').val());
  255. this.clear();
  256. }
  257. return true;
  258. },
  259. clear: function(){
  260. this.el.find('textarea').val('');
  261. },
  262. onStatusChange: function(){
  263. switch(this.status){
  264. case Jschat.viewstates.online:
  265. if (this.msgValid) {
  266. this.el.find('#id_send').removeAttr('disabled').removeClass('disabled');
  267. }
  268. break;
  269. case Jschat.viewstates.offline:
  270. this.el.find('#id_send').attr('disabled', 'disabled').addClass('disabled');
  271. break;
  272. }
  273. },
  274. onMsgValidChange: function(){
  275. if (this.msgValid) {
  276. if(this.status === Jschat.viewstates.online) {
  277. this.el.find('#id_send').removeAttr('disabled').removeClass('disabled');
  278. }
  279. } else {
  280. this.el.find('#id_send').attr('disabled', 'disabled').addClass('disabled');
  281. }
  282. },
  283. onMessageAdd: function(message, chatlog, ev) {
  284. if (this.el.find('#online-messages').is(':hidden')) {
  285. this.el.find('#online-messages').show(200);
  286. }
  287. var chat = this.el.find('#online-message-list');
  288. chat.append(Jschat.message_template(message.toJSON()));
  289. chat.scrollTop(chat[0].scrollHeight);
  290. }
  291. });
  292. //
  293. //Main class
  294. //==========
  295. //
  296. Jschat.Xmpp = function(options) {
  297. if (!options) options = {};
  298. if (this.defaults) options = _.extend(this.defaults, options);
  299. this.options = options;
  300. this.initialize();
  301. };
  302. //Xmpp class implementation
  303. //-------------------------
  304. _.extend(Jschat.Xmpp.prototype, Jschat.JsmvcCallback, Backbone.Events, {
  305. // Default options can be overriden in constructor:
  306. //
  307. // `chat = new Jschat.Xmpp({'jid': 'me@jabber.org})`
  308. defaults: {
  309. jid: 'isaacueca@logoslogic.com',
  310. password: 'cigano',
  311. bosh_service: '/http-bind',
  312. view_el_id: 'online-block'
  313. },
  314. initialize: function(){
  315. this.connection = new Strophe.Connection(this.options.bosh_service);
  316. this.roster = new Jschat.Roster();
  317. this.chatlog = new Jschat.ChatLog();
  318. this.view = new Jschat.ChatView({
  319. el: $('#'+this.options.view_el_id)
  320. });
  321. this._welcomeSent = false;
  322. // this.connection.rawInput = function (data) { console.log('RECV: ' + data); };
  323. // this.connection.rawOutput = function (data) { console.log('SEND: ' + data); };
  324. // listen events
  325. this.bind('connected', this.onConnect);
  326. if (this.options.autoConnect){
  327. this.connect();
  328. }
  329. this.chatlog.bind('add', this.callback('onMessageAdd'));
  330. this.view.bind('send:message', this.callback('sendMessage'));
  331. },
  332. connect: function(){
  333. this.connection.connect(this.options.jid, this.options.password, this.callback('onConnectChange'));
  334. this.trigger('ui:connect');
  335. },
  336. onConnectChange: function(status_code, error){
  337. for (st in Strophe.Status) {
  338. if (status_code === Strophe.Status[st]) {
  339. // console.log('status: ' + st);
  340. }
  341. }
  342. if (status_code === Strophe.Status.CONNECTED) {
  343. this.trigger('connected');
  344. }
  345. },
  346. onConnect: function(){
  347. // request roster
  348. var roster_iq = $iq({type: 'get'}).c('query', {xmlns: 'jabber:iq:roster'});
  349. this.connection.sendIQ(roster_iq, this.callback('onRoster'));
  350. this.trigger('ui:roster');
  351. // add handlers
  352. var nickname = 'guest_'+Math.floor(Math.random()*1111001);
  353. this.connection.send(
  354. $pres({
  355. to: 'southpark3@conference.logoslogic.com' + "/" + nickname
  356. }).c('x', {xmlns: "http://jabber.org/protocol/muc"}));
  357. this.connection.addHandler(this.callback('onContactPresence'), null, 'presence');
  358. this.connection.addHandler(this.callback('onMessage'), null, 'message', 'chat');
  359. this.connection.addHandler(this.callback('onMessage'), null, 'message', 'groupchat');
  360. },
  361. onRoster: function(roster){
  362. this.connection.send($pres());
  363. this.trigger('ui:ready');
  364. this.view.setStatus(Jschat.viewstates.online);
  365. var items = Jschat.Roster.serializeRoster(roster);
  366. for (var i=0; i<items.length; i++) {
  367. this.roster.add(items[i]);
  368. }
  369. return true;
  370. },
  371. onContactPresence: function(presence){
  372. var from1 = $(presence).attr('from');
  373. console.log('onContactPresence from '+from1);
  374. var from = Strophe.getBareJidFromJid($(presence).attr('from')),
  375. contact = this.roster.detect(function(c){return c.get('bare_jid') === from;});
  376. if (contact) {
  377. contact.updatePrecense(presence);
  378. }
  379. if(this.options.autoChat){
  380. _.delay(function(self){
  381. self.sendWelcome();
  382. }, '2000', this);
  383. }
  384. return true;
  385. },
  386. // Public method, use it directly if you set `{autoChat: false}`
  387. sendWelcome: function(){
  388. if (!this._welcomeSent) {
  389. var userinfo = this.getUserinfo();
  390. this.roster.freezeManager();
  391. this._welcomeSent = true;
  392. this.sendMessage({
  393. text: userinfo,
  394. from: this.options.jid,
  395. to: this.roster.manager.get('jid'),
  396. hidden: true,
  397. dt: new Date()
  398. });
  399. }
  400. },
  401. // `sendMessage` used for send all messages
  402. sendMessage: function(message){
  403. if (!this._welcomeSent){
  404. this.sendWelcome();
  405. }
  406. if (typeof(message) === 'string'){
  407. var msg = new Jschat.Message({
  408. text: message,
  409. from: this.options.jid,
  410. to: this.roster.manager.get('jid'),
  411. incoming: false,
  412. dt: new Date()
  413. });
  414. } else {
  415. var msg = new Jschat.Message(message);
  416. }
  417. msg.send(this.connection);
  418. if (!msg.get('hidden')){
  419. this.chatlog.add(msg);
  420. }
  421. },
  422. // Prepare and render userinfo
  423. getUserinfo: function(){
  424. return Jschat.welcome_template(this.view.getUserinfo());
  425. },
  426. // Handler for incoming messages
  427. onMessage: function(message){
  428. console.log('on message'+message);
  429. var msg = new Jschat.Message({
  430. text: $(message).find('body').text(),
  431. from: $(message).attr('from'),
  432. to: $(message).attr('to'),
  433. incoming: true,
  434. dt: new Date()
  435. });
  436. this.chatlog.add(msg);
  437. return true;
  438. },
  439. // Only trigger view event
  440. onMessageAdd: function(message){
  441. this.view.trigger('add:message', message);
  442. }
  443. });