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