/*! * Converse.js (XMPP-based instant messaging with Strophe.js and backbone.js) * http://opkode.com * * Copyright (c) 2012 Jan-Carel Brand (jc@opkode.com) * Dual licensed under the MIT and GPL Licenses */ /* The following line defines global variables defined elsewhere. */ /*globals jQuery, portal_url*/ // AMD/global registrations (function (root, factory) { if (typeof define === 'function' && define.amd) { define([ 'Libraries/burry.js/burry', "Libraries/jquery.gritter.min", "Libraries/jquery.cookie", "Libraries/jquery.ba-dotimeout.min", "Libraries/underscore", "Libraries/underscore.string/lib/underscore.string", "Libraries/backbone", "Libraries/strophe", "Libraries/strophe.muc", "Libraries/strophe.roster", "Libraries/diff_match_patch", "Libraries/jarnxmpp.core.handlers", "Libraries/jarnxmpp.collaboration.protocol", "Libraries/jarnxmpp.collaboration.collaborate" ], function (Burry) { var store = new Burry.Store('collective.xmpp.chat'); // Use Mustache style syntax for variable interpolation _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; return factory(jarnxmpp, jQuery, store, _, console); } ); } else { // Browser globals var store = new Burry.Store('collective.xmpp.chat'); _.templateSettings = { evaluate : /\{\[([\s\S]+?)\]\}/g, interpolate : /\{\{([\s\S]+?)\}\}/g }; root.xmppchat = factory(jarnxmpp, jQuery, store, _, console || {log: function(){}}); } }(this, function (jarnxmpp, $, store, _, console) { var xmppchat = jarnxmpp; xmppchat.collections = { /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc * add-on for ejabberd wants the URL below. This might break for other * Jabber servers. */ 'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns' }; xmppchat.collections.getLastCollection = function (jid, callback) { var bare_jid = Strophe.getBareJidFromJid(jid), iq = $iq({'type':'get'}) .c('list', {'xmlns': this.URI, 'with': bare_jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('before').up() .c('max') .t('1'); xmppchat.connection.sendIQ(iq, callback, function () { console.log('Error while retrieving collections'); }); }; xmppchat.collections.getLastMessages = function (jid, callback) { var that = this; this.getLastCollection(jid, function (result) { // Retrieve the last page of a collection (max 30 elements). var $collection = $(result).find('chat'), jid = $collection.attr('with'), start = $collection.attr('start'), iq = $iq({'type':'get'}) .c('retrieve', {'start': start, 'xmlns': that.URI, 'with': jid }) .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'}) .c('max') .t('30'); xmppchat.connection.sendIQ(iq, callback); }); }; xmppchat.ClientStorage = Backbone.Model.extend({ // TODO: Messages must be encrypted with a key and salt initialize: function (own_jid) { this.set({ 'own_jid' : own_jid }); }, addMessage: function (jid, msg, direction) { var bare_jid = Strophe.getBareJidFromJid(jid), now = new Date().toISOString(), msgs = store.get(hex_sha1(this.get('own_jid')+bare_jid)) || []; if (msgs.length >= 30) { msgs.shift(); } msgs.push(now+' '+direction+' '+msg); store.set(hex_sha1(this.get('own_jid')+bare_jid), msgs); }, getMessages: function (jid) { var bare_jid = Strophe.getBareJidFromJid(jid); return store.get(hex_sha1(this.get('own_jid')+bare_jid)) || []; } }); xmppchat.ChatBox = Backbone.Model.extend({ initialize: function () { this.set({ 'user_id' : Strophe.getNodeFromJid(this.get('jid')), 'box_id' : hex_sha1(this.get('jid')), 'fullname' : this.get('fullname') }); } }); xmppchat.ChatBoxView = Backbone.View.extend({ length: 200, tagName: 'div', className: 'chatbox', events: { 'click .close-chatbox-button': 'closeChat', 'keypress textarea.chat-textarea': 'keyPressed' }, message_template: _.template( '
' + '{{time}} {{username}}: ' + '{{message}}' + '
'), action_template: _.template( '
' + '{{time}}: ' + '{{message}}' + '
'), appendMessage: function (message) { var time, now = new Date(), minutes = now.getMinutes().toString(), list, $chat_content, match; message = message.replace(//g,">").replace(/\"/g,""").replace(/^\s*/, ""); list = message.match(/\b(http:\/\/www\.\S+\.\w+|www\.\S+\.\w+|http:\/\/(?=[^w]){3}\S+[\.:]\S+)[^ ]+\b/g); if (list) { for (i = 0; i < list.length; i++) { message = message.replace(list[i], ""+ list[i] + "" ); } } if (minutes.length==1) {minutes = '0'+minutes;} time = now.toLocaleTimeString().substring(0,5); $chat_content = $(this.el).find('.chat-content'); $chat_content.find('div.chat-event').remove(); match = message.match(/^\/(.*?)(?: (.*))?$/); if ((match) && (match[1] === 'me')) { message = message.replace(/^\/me/, '*'+xmppchat.username); $chat_content.append(this.action_template({ 'sender': 'me', 'time': time, 'message': message, 'username': xmppchat.username, 'extra_classes': '' })); } else { $chat_content.append(this.message_template({ 'sender': 'me', 'time': time, 'message': message, 'username': 'me', 'extra_classes': '' })); } $chat_content.scrollTop($chat_content[0].scrollHeight); }, insertStatusNotification: function (user_id, message) { var $chat_content = this.$el.find('.chat-content'); $chat_content.find('div.chat-event').remove().end() .append($('
').text(user_id+' '+message)); $chat_content.scrollTop($chat_content[0].scrollHeight); }, messageReceived: function (message) { /* XXX: event.mtype should be 'xhtml' for XHTML-IM messages, but I only seem to get 'text'. */ var body = $(message).children('body').text(), jid = $(message).attr('from'), composing = $(message).find('composing'), $chat_content = $(this.el).find('.chat-content'), user_id = Strophe.getNodeFromJid(jid), delayed = $(message).find('delay').length > 0, fullname = this.model.get('fullname'), time, stamp; if (xmppchat.xmppstatus.getStatus() === 'offline') { // only update the UI if the user is not offline return; } if (!body) { if (composing.length > 0) { this.insertStatusNotification(fullname, 'is typing'); return; } } else { xmppchat.storage.addMessage(jid, body, 'from'); $chat_content.find('div.chat-event').remove(); if (delayed) { // XXX: Test properly (for really old messages we somehow need to show // their date as well) stamp = $(message).find('delay').attr('stamp'); time = (new Date(stamp)).toLocaleTimeString().substring(0,5); } else { time = (new Date()).toLocaleTimeString().substring(0,5); } $chat_content.append( this.message_template({ 'sender': 'them', 'time': time, 'message': body.replace(/
/g, ""), 'username': fullname.split(' ')[0], 'extra_classes': delayed && 'delayed' || '' })); $chat_content.scrollTop($chat_content[0].scrollHeight); } }, insertClientStoredMessages: function () { var msgs = xmppchat.storage.getMessages(this.model.get('jid')), $content = this.$el.find('.chat-content'); for (var i=0; i<_.size(msgs); i++) { var msg = msgs[i], msg_array = msg.split(' ', 2), date = msg_array[0], match; msg = String(msg).replace(/(.*?\s.*?\s)/, ''); match = msg.match(/^\/(.*?)(?: (.*))?$/); if (msg_array[1] == 'to') { $content.append( this.message_template({ 'sender': 'me', 'time': new Date(Date.parse(date)).toLocaleTimeString().substring(0,5), 'message': msg, 'username': 'me', 'extra_classes': 'delayed' })); } else { $content.append( this.message_template({ 'sender': 'them', 'time': new Date(Date.parse(date)).toLocaleTimeString().substring(0,5), 'message': msg, 'username': this.model.get('fullname').split(' ')[0], 'extra_classes': 'delayed' })); } } }, sendMessage: function (text) { // TODO: Also send message to all my own connected resources, so that // they can display it as well.... // TODO: Look in ChatPartners to see what resources we have for the recipient. // if we have one resource, we sent to only that resources, if we have multiple // we send to the bare jid. var bare_jid = this.model.get('jid'); var message = $msg({to: bare_jid, type: 'chat'}) .c('body').t(text).up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}); xmppchat.connection.send(message); xmppchat.storage.addMessage(bare_jid, text, 'to'); this.appendMessage(text); }, keyPressed: function (ev) { var $textarea = $(ev.target), message, notify, composing, that = this; if(ev.keyCode == 13) { message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { this.sendMessage(message); } $(this.el).data('composing', false); } else { composing = $(this.el).data('composing'); if (!composing) { notify = $msg({'to':this.model.get('jid'), 'type': 'chat'}) .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'}); xmppchat.connection.send(notify); $(this.el).data('composing', true); } } }, addChatToCookie: function () { var cookie = jQuery.cookie('chats-open-'+xmppchat.username), jid = this.model.get('jid'), new_cookie, open_chats = []; if (cookie) { open_chats = cookie.split('|'); } if (!_.has(open_chats, jid)) { // Update the cookie if this new chat is not yet in it. open_chats.push(jid); new_cookie = open_chats.join('|'); jQuery.cookie('chats-open-'+xmppchat.username, new_cookie, {path: '/'}); console.log('updated cookie = ' + new_cookie + '\n'); } }, removeChatFromCookie: function () { var cookie = jQuery.cookie('chats-open-'+xmppchat.username), open_chats = [], new_chats = []; if (cookie) { open_chats = cookie.split('|'); } for (var i=0; i < open_chats.length; i++) { if (open_chats[i] != this.model.get('jid')) { new_chats.push(open_chats[i]); } } if (new_chats.length) { jQuery.cookie('chats-open-'+xmppchat.username, new_chats.join('|'), {path: '/'}); } else { jQuery.cookie('chats-open-'+xmppchat.username, null, {path: '/'}); } }, closeChat: function () { var that = this; $('#'+this.model.get('box_id')).hide('fast', function () { that.removeChatFromCookie(that.model.get('id')); }); }, initialize: function (){ $('body').append($(this.el).hide()); xmppchat.roster.on('change', function (item, changed) { if (_.has(changed.changes, 'presence_type')) { if (this.$el.is(':visible')) { if (item.get('presence_type') === 'offline') { this.insertStatusNotification(this.model.get('fullname'), 'has gone offline'); } else if (item.get('presence_type') === 'away') { this.insertStatusNotification(this.model.get('fullname'), 'has gone away'); } else if ((item.get('presence_type') === 'busy') || (item.get('presence_type') === 'dnd')) { this.insertStatusNotification(this.model.get('fullname'), 'is busy'); } else if (item.get('presence_type') === 'online') { this.$el.find('div.chat-event').remove(); } } } else if (_.has(changed.changes, 'status')) { if (item.get('jid') === this.model.get('jid')) { this.$el.find('p.user-custom-message').text(item.get('status')); } } }, this); }, template: _.template( '
' + '
{{ fullname }}
' + 'X' + '

' + '

' + '
' + '
' + '