/*! * Converse.js (Web-based XMPP instant messaging client) * http://conversejs.org * * Copyright (c) 2012, Jan-Carel Brand * Licensed under the Mozilla Public License (MPL) */ // AMD/global registrations (function (root, factory) { if (typeof define === 'function' && define.amd) { define("converse", ["converse-dependencies", "converse-templates"], function(dependencies, templates) { var otr = dependencies.otr, moment = dependencies.moment; if (typeof otr !== "undefined") { return factory(jQuery, _, otr.OTR, otr.DSA, templates, moment); } else { return factory(jQuery, _, undefined, undefined, templates, moment); } } ); } else { root.converse = factory(jQuery, _, OTR, DSA, JST, moment); } }(this, function ($, _, OTR, DSA, templates, moment) { // "use strict"; // Cannot use this due to Safari bug. // See https://github.com/jcbrand/converse.js/issues/196 if (typeof console === "undefined" || typeof console.log === "undefined") { console = { log: function () {}, error: function () {} }; } // TODO: these non-backbone methods should all be moved to utils. $.fn.addHyperlinks = function() { if (this.length > 0) { this.each(function(i, obj) { var x = $(obj).html(); var list = x.match(/\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<]{2,200}\b/g ); if (list) { for (i=0; i"+ list[i] + "" ); } } $(obj).html(x); }); } return this; }; var contains = function (attr, query) { return function (item) { if (typeof attr === 'object') { var value = false; _.each(attr, function (a) { value = value || item.get(a).toLowerCase().indexOf(query.toLowerCase()) !== -1; }); return value; } else if (typeof attr === 'string') { return item.get(attr).toLowerCase().indexOf(query.toLowerCase()) !== -1; } else { throw new Error('Wrong attribute type. Must be string or array.'); } }; }; contains.not = function (attr, query) { return function (item) { return !(contains(attr, query)(item)); }; }; String.prototype.splitOnce = function (delimiter) { var components = this.split(delimiter); return [components.shift(), components.join(delimiter)]; }; var playNotification = function () { var audio; if (converse.play_sounds && typeof Audio !== "undefined"){ audio = new Audio("sounds/msg_received.ogg"); if (audio.canPlayType('/audio/ogg')) { audio.play(); } else { audio = new Audio("/sounds/msg_received.mp3"); audio.play(); } } }; $.fn.addEmoticons = function() { if (converse.visible_toolbar_buttons.emoticons) { if (this.length > 0) { this.each(function(i, obj) { var text = $(obj).html(); text = text.replace(/>:\)/g, ''); text = text.replace(/:\)/g, ''); text = text.replace(/:\-\)/g, ''); text = text.replace(/;\)/g, ''); text = text.replace(/;\-\)/g, ''); text = text.replace(/:D/g, ''); text = text.replace(/:\-D/g, ''); text = text.replace(/:P/g, ''); text = text.replace(/:\-P/g, ''); text = text.replace(/:p/g, ''); text = text.replace(/:\-p/g, ''); text = text.replace(/8\)/g, ''); text = text.replace(/:S/g, ''); text = text.replace(/:\\/g, ''); text = text.replace(/:\/ /g, ''); text = text.replace(/>:\(/g, ''); text = text.replace(/:\(/g, ''); text = text.replace(/:\-\(/g, ''); text = text.replace(/:O/g, ''); text = text.replace(/:\-O/g, ''); text = text.replace(/\=\-O/g, ''); text = text.replace(/\(\^.\^\)b/g, ''); text = text.replace(/<3/g, ''); $(obj).html(text); }); } } return this; }; var converse = { templates: templates, emit: function(evt, data) { $(this).trigger(evt, data); }, once: function(evt, handler) { $(this).one(evt, handler); }, on: function(evt, handler) { $(this).bind(evt, handler); }, off: function(evt, handler) { $(this).unbind(evt, handler); }, refreshWebkit: function () { /* This works around a webkit bug. Refresh the browser's viewport, * otherwise chatboxes are not moved along when one is closed. */ if ($.browser.webkit) { var conversejs = document.getElementById('conversejs'); conversejs.style.display = 'none'; conversejs.offsetHeight = conversejs.offsetHeight; conversejs.style.display = 'block'; } } }; converse.initialize = function (settings, callback) { var converse = this; // Constants // --------- var UNENCRYPTED = 0; var UNVERIFIED= 1; var VERIFIED= 2; var FINISHED = 3; var KEY = { ENTER: 13 }; var STATUS_WEIGHTS = { 'offline': 6, 'unavailable': 5, 'xa': 4, 'away': 3, 'dnd': 2, 'online': 1 }; var INACTIVE = 'inactive'; var ACTIVE = 'active'; var COMPOSING = 'composing'; var PAUSED = 'paused'; var HAS_CSPRNG = ((typeof crypto !== 'undefined') && ((typeof crypto.randomBytes === 'function') || (typeof crypto.getRandomValues === 'function') )); var HAS_CRYPTO = HAS_CSPRNG && ( (typeof CryptoJS !== "undefined") && (typeof OTR !== "undefined") && (typeof DSA !== "undefined") ); var OPENED = 'opened'; var CLOSED = 'closed'; // Default configuration values // ---------------------------- this.allow_contact_requests = true; this.allow_dragresize = true; this.allow_muc = true; this.allow_otr = true; this.animate = true; this.auto_list_rooms = false; this.auto_reconnect = false; this.auto_subscribe = false; this.bosh_service_url = undefined; // The BOSH connection manager URL. this.cache_otr_key = false; this.debug = false; this.default_box_height = 324; // The default height, in pixels, for the control box, chat boxes and chatrooms. this.expose_rid_and_sid = false; this.forward_messages = false; this.hide_muc_server = false; this.i18n = locales.en; this.message_carbons = false; this.no_trimming = false; // Set to true for phantomjs tests (where browser apparently has no width) this.play_sounds = false; this.prebind = false; this.roster_groups = false; this.show_controlbox_by_default = false; this.show_only_online_users = false; this.show_toolbar = true; this.storage = 'session'; this.use_otr_by_default = false; this.use_vcards = true; this.visible_toolbar_buttons = { 'emoticons': true, 'call': false, 'clear': true, 'toggle_participants': true }; this.xhr_custom_status = false; this.xhr_custom_status_url = ''; this.xhr_user_search = false; this.xhr_user_search_url = ''; // Allow only whitelisted configuration attributes to be overwritten _.extend(this, _.pick(settings, [ 'allow_contact_requests', 'allow_dragresize', 'allow_muc', 'allow_otr', 'animate', 'auto_list_rooms', 'auto_reconnect', 'auto_subscribe', 'bosh_service_url', 'cache_otr_key', 'connection', 'debug', 'default_box_height', 'message_carbons', 'expose_rid_and_sid', 'forward_messages', 'fullname', 'hide_muc_server', 'i18n', 'jid', 'no_trimming', 'play_sounds', 'prebind', 'rid', 'roster_groups', 'show_controlbox_by_default', 'show_only_online_users', 'show_toolbar', 'sid', 'storage', 'use_otr_by_default', 'use_vcards', 'xhr_custom_status', 'xhr_custom_status_url', 'xhr_user_search', 'xhr_user_search_url' ])); if (settings.visible_toolbar_buttons) { _.extend( this.visible_toolbar_buttons, _.pick(settings.visible_toolbar_buttons, [ 'emoticons', 'call', 'clear', 'toggle_participants' ] )); } $.fx.off = !this.animate; // Only allow OTR if we have the capability this.allow_otr = this.allow_otr && HAS_CRYPTO; // Only use OTR by default if allow OTR is enabled to begin with this.use_otr_by_default = this.use_otr_by_default && this.allow_otr; // Translation machinery // --------------------- var __ = $.proxy(function (str) { // Translation factory if (this.i18n === undefined) { this.i18n = locales.en; } var t = this.i18n.translate(str); if (arguments.length>1) { return t.fetch.apply(t, [].slice.call(arguments,1)); } else { return t.fetch(); } }, this); var ___ = function (str) { /* XXX: This is part of a hack to get gettext to scan strings to be * translated. Strings we cannot send to the function above because * they require variable interpolation and we don't yet have the * variables at scan time. * * See actionInfoMessages */ return str; }; // Translation aware constants // --------------------------- var OTR_CLASS_MAPPING = {}; OTR_CLASS_MAPPING[UNENCRYPTED] = 'unencrypted'; OTR_CLASS_MAPPING[UNVERIFIED] = 'unverified'; OTR_CLASS_MAPPING[VERIFIED] = 'verified'; OTR_CLASS_MAPPING[FINISHED] = 'finished'; var OTR_TRANSLATED_MAPPING = {}; OTR_TRANSLATED_MAPPING[UNENCRYPTED] = __('unencrypted'); OTR_TRANSLATED_MAPPING[UNVERIFIED] = __('unverified'); OTR_TRANSLATED_MAPPING[VERIFIED] = __('verified'); OTR_TRANSLATED_MAPPING[FINISHED] = __('finished'); var STATUSES = { 'dnd': __('This contact is busy'), 'online': __('This contact is online'), 'offline': __('This contact is offline'), 'unavailable': __('This contact is unavailable'), 'xa': __('This contact is away for an extended period'), 'away': __('This contact is away') }; var DESC_GROUP_TOGGLE = __('Click to hide these contacts'); var HEADER_CURRENT_CONTACTS = __('My contacts'); var HEADER_PENDING_CONTACTS = __('Pending contacts'); var HEADER_REQUESTING_CONTACTS = __('Contact requests'); var HEADER_UNGROUPED = __('Ungrouped'); var LABEL_CONTACTS = __('Contacts'); var LABEL_GROUPS = __('Groups'); var HEADER_WEIGHTS = {}; HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0; HEADER_WEIGHTS[HEADER_UNGROUPED] = 1; HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2; HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3; // Module-level variables // ---------------------- this.callback = callback || function () {}; this.initial_presence_sent = 0; this.msg_counter = 0; // Module-level functions // ---------------------- this.giveFeedback = function (message, klass) { $('.conn-feedback').attr('class', 'conn-feedback').text(message); if (klass) { $('.conn-feedback').addClass(klass); } }; this.log = function (txt, level) { if (this.debug) { if (level == 'error') { console.log('ERROR: '+txt); } else { console.log(txt); } } }; this.getVCard = function (jid, callback, errback) { if (!this.use_vcards) { if (callback) { callback(jid, jid); } return; } converse.connection.vcard.get( $.proxy(function (iq) { // Successful callback var $vcard = $(iq).find('vCard'); var fullname = $vcard.find('FN').text(), img = $vcard.find('BINVAL').text(), img_type = $vcard.find('TYPE').text(), url = $vcard.find('URL').text(); if (jid) { var contact = converse.roster.get(jid); if (contact) { fullname = _.isEmpty(fullname)? contact.get('fullname') || jid: fullname; contact.save({ 'fullname': fullname, 'image_type': img_type, 'image': img, 'url': url, 'vcard_updated': moment().format() }); } } if (callback) { callback(jid, fullname, img, img_type, url); } }, this), jid, function (iq) { // Error callback var contact = converse.roster.get(jid); if (contact) { contact.save({ 'vcard_updated': moment().format() }); } if (errback) { errback(iq); } } ); }; this.reconnect = function () { converse.giveFeedback(__('Reconnecting'), 'error'); converse.emit('reconnect'); if (!converse.prebind) { this.connection.connect( this.connection.jid, this.connection.pass, function (status, condition) { converse.onConnect(status, condition, true); }, this.connection.wait, this.connection.hold, this.connection.route ); } }; this.showLoginButton = function () { var view = converse.chatboxviews.get('controlbox'); if (typeof view.loginpanel !== 'undefined') { view.loginpanel.showLoginButton(); } }; this.onConnect = function (status, condition, reconnect) { var $button, $form; if ((status === Strophe.Status.CONNECTED) || (status === Strophe.Status.ATTACHED)) { if ((typeof reconnect !== 'undefined') && (reconnect)) { converse.log(status === Strophe.Status.CONNECTED ? 'Reconnected' : 'Reattached'); converse.onReconnected(); } else { converse.log(status === Strophe.Status.CONNECTED ? 'Connected' : 'Attached'); converse.onConnected(); } } else if (status === Strophe.Status.DISCONNECTED) { // TODO: Handle case where user manually logs out... converse.giveFeedback(__('Disconnected'), 'error'); if (converse.auto_reconnect) { converse.reconnect(); } else { converse.showLoginButton(); } } else if (status === Strophe.Status.Error) { converse.showLoginButton(); converse.giveFeedback(__('Error'), 'error'); } else if (status === Strophe.Status.CONNECTING) { converse.giveFeedback(__('Connecting')); } else if (status === Strophe.Status.CONNFAIL) { converse.showLoginButton(); converse.giveFeedback(__('Connection Failed'), 'error'); } else if (status === Strophe.Status.AUTHENTICATING) { converse.giveFeedback(__('Authenticating')); } else if (status === Strophe.Status.AUTHFAIL) { converse.showLoginButton(); converse.giveFeedback(__('Authentication Failed'), 'error'); } else if (status === Strophe.Status.DISCONNECTING) { converse.giveFeedback(__('Disconnecting'), 'error'); } }; this.applyHeightResistance = function (height) { /* This method applies some resistance/gravity around the * "default_box_height". If "height" is close enough to * default_box_height, then that is returned instead. */ if (typeof height === 'undefined') { return converse.default_box_height; } var resistance = 10; if ((height !== converse.default_box_height) && (Math.abs(height - converse.default_box_height) < resistance)) { return converse.default_box_height; } return height; }; this.updateMsgCounter = function () { if (this.msg_counter > 0) { if (document.title.search(/^Messages \(\d+\) /) == -1) { document.title = "Messages (" + this.msg_counter + ") " + document.title; } else { document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") "); } window.blur(); window.focus(); } else if (document.title.search(/^Messages \(\d+\) /) != -1) { document.title = document.title.replace(/^Messages \(\d+\) /, ""); } }; this.incrementMsgCounter = function () { this.msg_counter += 1; this.updateMsgCounter(); }; this.clearMsgCounter = function () { this.msg_counter = 0; this.updateMsgCounter(); }; this.initStatus = function (callback) { this.xmppstatus = new this.XMPPStatus(); var id = b64_sha1('converse.xmppstatus-'+converse.bare_jid); this.xmppstatus.id = id; // Appears to be necessary for backbone.browserStorage this.xmppstatus.browserStorage = new Backbone.BrowserStorage[converse.storage](id); this.xmppstatus.fetch({success: callback, error: callback}); }; this.registerGlobalEventHandlers = function () { $(document).click(function() { if ($('.toggle-otr ul').is(':visible')) { $('.toggle-otr ul', this).slideUp(); } if ($('.toggle-smiley ul').is(':visible')) { $('.toggle-smiley ul', this).slideUp(); } }); $(document).on('mousemove', $.proxy(function (ev) { if (!this.resized_chatbox || !this.allow_dragresize) { return true; } ev.preventDefault(); this.resized_chatbox.resizeChatBox(ev); }, this)); $(document).on('mouseup', $.proxy(function (ev) { if (!this.resized_chatbox || !this.allow_dragresize) { return true; } ev.preventDefault(); var height = this.applyHeightResistance(this.resized_chatbox.height); if (this.connection) { this.resized_chatbox.model.save({'height': height}); } else { this.resized_chatbox.model.set({'height': height}); } this.resized_chatbox = null; }, this)); $(window).on("blur focus", $.proxy(function (ev) { if ((this.windowState != ev.type) && (ev.type == 'focus')) { converse.clearMsgCounter(); } this.windowState = ev.type; },this)); $(window).on("resize", _.debounce($.proxy(function (ev) { this.chatboxviews.trimChats(); },this), 200)); }; this.onReconnected = function () { // We need to re-register all the event handlers on the newly // created connection. this.initStatus($.proxy(function () { this.registerRosterXHandler(); this.registerPresenceHandler(); this.chatboxes.registerMessageHandler(); converse.xmppstatus.sendPresence(); this.giveFeedback(__('Online Contacts')); }, this)); }; this.enableCarbons = function () { /* Ask the XMPP server to enable Message Carbons * See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling */ if (!this.message_carbons) { return; } var carbons_iq = new Strophe.Builder('iq', { from: this.connection.jid, id: 'enablecarbons', type: 'set' }) .c('enable', {xmlns: 'urn:xmpp:carbons:2'}); this.connection.send(carbons_iq); this.connection.addHandler(function(iq) { //TODO: check if carbons was enabled: }, null, "iq", null, "enablecarbons"); }; this.onConnected = function () { if (this.debug) { this.connection.xmlInput = function (body) { console.log(body); }; this.connection.xmlOutput = function (body) { console.log(body); }; Strophe.log = function (level, msg) { console.log(level+' '+msg); }; Strophe.error = function (msg) { console.log('ERROR: '+msg); }; } this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid); this.domain = Strophe.getDomainFromJid(this.connection.jid); this.minimized_chats = new converse.MinimizedChats({model: this.chatboxes}); this.features = new this.Features(); this.enableCarbons(); this.initStatus($.proxy(function () { this.roster = new converse.RosterContacts(); this.roster.browserStorage = new Backbone.BrowserStorage[this.storage]( b64_sha1('converse.contacts-'+this.bare_jid)); var rostergroups = new this.RosterGroups(); rostergroups.browserStorage = new Backbone.BrowserStorage[this.storage]( b64_sha1('converse.roster.groups'+this.bare_jid)); this.rosterview = new this.RosterView({model: rostergroups}); this.chatboxes.onConnected(); this.connection.roster.get(function () {}); this.giveFeedback(__('Online Contacts')); if (this.callback) { if (this.connection.service === 'jasmine tests') { // XXX: Call back with the internal converse object. This // object should never be exposed to production systems. // 'jasmine tests' is an invalid http bind service value, // so we're sure that this is just for tests. // // TODO: We might need to consider websockets, which // probably won't use the 'service' attr. Current // strophe.js version used by converse.js doesn't support // websockets. this.callback(this); } else { this.callback(); } } }, this)); converse.emit('ready'); }; // Backbone Models and Views // ------------------------- this.OTR = Backbone.Model.extend({ // A model for managing OTR settings. getSessionPassphrase: function () { if (converse.prebind) { var key = b64_sha1(converse.connection.jid), pass = window.sessionStorage[key]; if (typeof pass === 'undefined') { pass = Math.floor(Math.random()*4294967295).toString(); window.sessionStorage[key] = pass; } return pass; } else { return converse.connection.pass; } }, generatePrivateKey: function () { var key = new DSA(); var jid = converse.connection.jid; if (converse.cache_otr_key) { var cipher = CryptoJS.lib.PasswordBasedCipher; var pass = this.getSessionPassphrase(); if (typeof pass !== "undefined") { // Encrypt the key and set in sessionStorage. Also store instance tag. window.sessionStorage[b64_sha1(jid+'priv_key')] = cipher.encrypt(CryptoJS.algo.AES, key.packPrivate(), pass).toString(); window.sessionStorage[b64_sha1(jid+'instance_tag')] = instance_tag; window.sessionStorage[b64_sha1(jid+'pass_check')] = cipher.encrypt(CryptoJS.algo.AES, 'match', pass).toString(); } } return key; } }); this.Message = Backbone.Model; this.Messages = Backbone.Collection.extend({ model: converse.Message }); this.ChatBox = Backbone.Model.extend({ initialize: function () { var height = converse.applyHeightResistance(this.get('height')); if (this.get('box_id') !== 'controlbox') { this.messages = new converse.Messages(); this.messages.browserStorage = new Backbone.BrowserStorage[converse.storage]( b64_sha1('converse.messages'+this.get('jid')+converse.bare_jid)); this.save({ 'box_id' : b64_sha1(this.get('jid')), 'height': height, 'minimized': this.get('minimized') || false, 'otr_status': this.get('otr_status') || UNENCRYPTED, 'time_minimized': this.get('time_minimized') || moment(), 'time_opened': this.get('time_opened') || moment().valueOf(), 'user_id' : Strophe.getNodeFromJid(this.get('jid')), 'num_unread': this.get('num_unread') || 0, 'url': '' }); } else { this.set({ 'height': height, 'time_opened': moment(0).valueOf(), 'num_unread': this.get('num_unread') || 0 }); } }, maximize: function () { this.save({ 'minimized': false, 'time_opened': moment().valueOf() }); }, minimize: function () { this.save({ 'minimized': true, 'time_minimized': moment().format() }); }, getSession: function (callback) { var cipher = CryptoJS.lib.PasswordBasedCipher; var result, pass, instance_tag, saved_key, pass_check; if (converse.cache_otr_key) { pass = converse.otr.getSessionPassphrase(); if (typeof pass !== "undefined") { instance_tag = window.sessionStorage[b64_sha1(this.id+'instance_tag')]; saved_key = window.sessionStorage[b64_sha1(this.id+'priv_key')]; pass_check = window.sessionStorage[b64_sha1(this.connection.jid+'pass_check')]; if (saved_key && instance_tag && typeof pass_check !== 'undefined') { var decrypted = cipher.decrypt(CryptoJS.algo.AES, saved_key, pass); var key = DSA.parsePrivate(decrypted.toString(CryptoJS.enc.Latin1)); if (cipher.decrypt(CryptoJS.algo.AES, pass_check, pass).toString(CryptoJS.enc.Latin1) === 'match') { // Verified that the passphrase is still the same this.trigger('showHelpMessages', [__('Re-establishing encrypted session')]); callback({ 'key': key, 'instance_tag': instance_tag }); return; // Our work is done here } } } } // We need to generate a new key and instance tag this.trigger('showHelpMessages', [ __('Generating private key.'), __('Your browser might become unresponsive.')], null, true // show spinner ); setTimeout(function () { callback({ 'key': converse.otr.generatePrivateKey.apply(this), 'instance_tag': OTR.makeInstanceTag() }); }, 500); }, updateOTRStatus: function (state) { switch (state) { case OTR.CONST.STATUS_AKE_SUCCESS: if (this.otr.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED) { this.save({'otr_status': UNVERIFIED}); } break; case OTR.CONST.STATUS_END_OTR: if (this.otr.msgstate === OTR.CONST.MSGSTATE_FINISHED) { this.save({'otr_status': FINISHED}); } else if (this.otr.msgstate === OTR.CONST.MSGSTATE_PLAINTEXT) { this.save({'otr_status': UNENCRYPTED}); } break; } }, onSMP: function (type, data) { // Event handler for SMP (Socialist's Millionaire Protocol) // used by OTR (off-the-record). switch (type) { case 'question': this.otr.smpSecret(prompt(__( 'Authentication request from %1$s\n\nYour buddy is attempting to verify your identity, by asking you the question below.\n\n%2$s', [this.get('fullname'), data]))); break; case 'trust': if (data === true) { this.save({'otr_status': VERIFIED}); } else { this.trigger( 'showHelpMessages', [__("Could not verify this user's identify.")], 'error'); this.save({'otr_status': UNVERIFIED}); } break; default: throw new Error('Unknown type.'); } }, initiateOTR: function (query_msg) { // Sets up an OTR object through which we can send and receive // encrypted messages. // // If 'query_msg' is passed in, it means there is an alread incoming // query message from our buddy. Otherwise, it is us who will // send the query message to them. this.save({'otr_status': UNENCRYPTED}); var session = this.getSession($.proxy(function (session) { this.otr = new OTR({ fragment_size: 140, send_interval: 200, priv: session.key, instance_tag: session.instance_tag, debug: this.debug }); this.otr.on('status', $.proxy(this.updateOTRStatus, this)); this.otr.on('smp', $.proxy(this.onSMP, this)); this.otr.on('ui', $.proxy(function (msg) { this.trigger('showReceivedOTRMessage', msg); }, this)); this.otr.on('io', $.proxy(function (msg) { this.trigger('sendMessageStanza', msg); }, this)); this.otr.on('error', $.proxy(function (msg) { this.trigger('showOTRError', msg); }, this)); this.trigger('showHelpMessages', [__('Exchanging private key with buddy.')]); if (query_msg) { this.otr.receiveMsg(query_msg); } else { this.otr.sendQueryMsg(); } }, this)); }, endOTR: function () { if (this.otr) { this.otr.endOtr(); } this.save({'otr_status': UNENCRYPTED}); }, createMessage: function ($message) { var body = $message.children('body').text(), from = Strophe.getBareJidFromJid($message.attr('from')), composing = $message.find('composing'), paused = $message.find('paused'), delayed = $message.find('delay').length > 0, fullname = this.get('fullname'), stamp, time, sender; fullname = (_.isEmpty(fullname)? from: fullname).split(' ')[0]; if (!body) { if (composing.length || paused.length) { this.messages.add({ fullname: fullname, sender: 'them', delayed: delayed, time: moment().format(), composing: composing.length, paused: paused.length }); } } else { if (delayed) { stamp = $message.find('delay').attr('stamp'); time = stamp; } else { time = moment().format(); } if (from == converse.bare_jid) { sender = 'me'; } else { sender = 'them'; } this.messages.create({ fullname: fullname, sender: sender, delayed: delayed, time: time, message: body }); } }, receiveMessage: function ($message) { var $body = $message.children('body'); var text = ($body.length > 0 ? $body.text() : undefined); if ((!text) || (!converse.allow_otr)) { return this.createMessage($message); } if (text.match(/^\?OTRv23?/)) { this.initiateOTR(text); } else { if (_.contains([UNVERIFIED, VERIFIED], this.get('otr_status'))) { this.otr.receiveMsg(text); } else { if (text.match(/^\?OTR/)) { if (!this.otr) { this.initiateOTR(text); } else { this.otr.receiveMsg(text); } } else { // Normal unencrypted message. this.createMessage($message); } } } } }); this.ChatBoxView = Backbone.View.extend({ length: 200, tagName: 'div', className: 'chatbox', is_chatroom: false, // This is not a multi-user chatroom events: { 'click .close-chatbox-button': 'close', 'click .toggle-chatbox-button': 'minimize', 'keypress textarea.chat-textarea': 'keyPressed', 'click .toggle-smiley': 'toggleEmoticonMenu', 'click .toggle-smiley ul li': 'insertEmoticon', 'click .toggle-clear': 'clearMessages', 'click .toggle-otr': 'toggleOTRMenu', 'click .start-otr': 'startOTRFromToolbar', 'click .end-otr': 'endOTR', 'click .auth-otr': 'authOTR', 'click .toggle-call': 'toggleCall', 'mousedown .dragresize-tm': 'onDragResizeStart' }, initialize: function (){ this.model.messages.on('add', this.onMessageAdded, this); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); this.model.on('change', this.onChange, this); this.model.on('showOTRError', this.showOTRError, this); this.model.on('buddyStartsOTR', this.buddyStartsOTR, this); this.model.on('showHelpMessages', this.showHelpMessages, this); this.model.on('sendMessageStanza', this.sendMessageStanza, this); this.model.on('showSentOTRMessage', function (text) { this.showMessage({'message': text, 'sender': 'me'}); }, this); this.model.on('showReceivedOTRMessage', function (text) { this.showMessage({'message': text, 'sender': 'them'}); }, this); this.updateVCard(); this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); this.render().model.messages.fetch({add: true}); if (this.model.get('minimized')) { this.hide(); } else { this.show(); } if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) { this.model.initiateOTR(); } }, render: function () { this.$el.attr('id', this.model.get('box_id')) .html(converse.templates.chatbox( _.extend(this.model.toJSON(), { show_toolbar: converse.show_toolbar, label_personal_message: __('Personal message') } ) ) ); this.renderToolbar().renderAvatar(); converse.emit('chatBoxOpened', this); setTimeout(function () { converse.refreshWebkit(); }, 50); return this.showStatusMessage(); }, initDragResize: function () { this.prev_pageY = 0; // To store last known mouse position if (converse.connection) { this.height = this.model.get('height'); } return this; }, showStatusNotification: function (message, keep_old) { var $chat_content = this.$el.find('.chat-content'); if (!keep_old) { $chat_content.find('div.chat-event').remove(); } $chat_content.append($('
').text(message)); this.scrollDown(); }, toggleParticipants: function (ev) { if (ev) { ev.preventDefault(); ev.stopPropagation(); } var $el = $(ev.target); if ($el.hasClass("icon-hide-users")) { $el.removeClass('icon-hide-users').addClass('icon-show-users'); this.$('div.participants').animate({width: 0}).hide(); this.$('.chat-area').animate({width: '100%'}); this.$('form.sendXMPPMessage').animate({width: '100%'}); } else { $el.removeClass('icon-show-users').addClass('icon-hide-users'); this.$('.chat-area').animate({width: '200px'}, $.proxy(function () { this.$('div.participants').css({width: '100px'}).show(); }, this)); this.$('form.sendXMPPMessage').animate({width: '200px'}); } }, clearChatRoomMessages: function (ev) { ev.stopPropagation(); var result = confirm(__("Are you sure you want to clear the messages from this room?")); if (result === true) { this.$el.find('.chat-content').empty(); } return this; }, showMessage: function (msg_dict) { var $content = this.$el.find('.chat-content'), msg_time = moment(msg_dict.time) || moment, text = msg_dict.message, match = text.match(/^\/(.*?)(?: (.*))?$/), fullname = msg_dict.fullname || this.model.get('fullname'), // XXX Perhaps always use model's? template, username; if ((match) && (match[1] === 'me')) { text = text.replace(/^\/me/, ''); template = converse.templates.action; username = fullname; } else { template = converse.templates.message; username = msg_dict.sender === 'me' && __('me') || fullname; } $content.find('div.chat-event').remove(); var message = template({ 'sender': msg_dict.sender, 'time': msg_time.format('hh:mm'), 'username': username, 'message': '', 'extra_classes': msg_dict.delayed && 'delayed' || '' }); $content.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent()); this.scrollDown(); }, showHelpMessages: function (msgs, type, spinner) { var $chat_content = this.$el.find('.chat-content'), i, msgs_length = msgs.length; for (i=0; i'+msgs[i]+'')); } if (spinner === true) { $chat_content.append(''); } else if (spinner === false) { $chat_content.find('span.spinner').remove(); } return this.scrollDown(); }, onMessageAdded: function (message) { var time = message.get('time'), times = this.model.messages.pluck('time'), previous_message, idx, this_date, prev_date, text, match; // If this message is on a different day than the one received // prior, then indicate it on the chatbox. idx = _.indexOf(times, time)-1; if (idx >= 0) { previous_message = this.model.messages.at(idx); prev_date = moment(previous_message.get('time')); if (prev_date.isBefore(time, 'day')) { this_date = moment(time); this.$el.find('.chat-content').append(converse.templates.new_day({ isodate: this_date.format("YYYY-MM-DD"), datestring: this_date.format("dddd MMM Do YYYY") })); } } if (message.get(COMPOSING)) { this.showStatusNotification(message.get('fullname')+' '+__('is typing')); return; } else if (message.get(PAUSED)) { this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); return; } else { this.showMessage(_.clone(message.attributes)); } if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) { converse.incrementMsgCounter(); } return this.scrollDown(); }, sendMessageStanza: function (text) { /* * Sends the actual XML stanza to the XMPP server. */ // 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 timestamp = (new Date()).getTime(); var bare_jid = this.model.get('jid'); var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp}) .c('body').t(text).up() .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}); converse.connection.send(message); if (converse.forward_messages) { // Forward the message, so that other connected resources are also aware of it. var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp}) .c('forwarded', {xmlns:'urn:xmpp:forward:0'}) .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up() .cnode(message.tree()); converse.connection.send(forwarded); } }, sendMessage: function (text) { var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; if (match) { if (match[1] === "clear") { return this.clearMessages(); } else if (match[1] === "help") { msgs = [ '/help:'+__('Show this menu')+'', '/me:'+__('Write in the third person')+'', '/clear:'+__('Remove messages')+'' ]; this.showHelpMessages(msgs); return; } else if ((converse.allow_otr) && (match[1] === "endotr")) { return this.endOTR(); } else if ((converse.allow_otr) && (match[1] === "otr")) { return this.model.initiateOTR(); } } if (_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) { // Off-the-record encryption is active this.model.otr.sendMsg(text); this.model.trigger('showSentOTRMessage', text); } else { // We only save unencrypted messages. var fullname = converse.xmppstatus.get('fullname'); fullname = _.isEmpty(fullname)? converse.bare_jid: fullname; this.model.messages.create({ fullname: fullname, sender: 'me', time: moment().format(), message: text }); this.sendMessageStanza(text); } }, keyPressed: function (ev) { var $textarea = $(ev.target), message, notify, composing; if(ev.keyCode == KEY.ENTER) { ev.preventDefault(); message = $textarea.val(); $textarea.val('').focus(); if (message !== '') { if (this.model.get('chatroom')) { this.sendChatRoomMessage(message); } else { this.sendMessage(message); } converse.emit('messageSend', message); } this.$el.data('composing', false); } else if (!this.model.get('chatroom')) { // composing data is only for single user chat composing = this.$el.data('composing'); if (!composing) { if (ev.keyCode != 47) { // We don't send composing messages if the message // starts with forward-slash. notify = $msg({'to':this.model.get('jid'), 'type': 'chat'}) .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'}); converse.connection.send(notify); } this.$el.data('composing', true); } } }, onDragResizeStart: function (ev) { if (!converse.allow_dragresize) { return true; } // Record element attributes for mouseMove(). this.height = this.$el.children('.box-flyout').height(); converse.resized_chatbox = this; this.prev_pageY = ev.pageY; }, setChatBoxHeight: function (height) { if (!this.model.get('minimized')) { this.$el.children('.box-flyout')[0].style.height = converse.applyHeightResistance(height)+'px'; } }, resizeChatBox: function (ev) { var diff = ev.pageY - this.prev_pageY; if (!diff) { return; } this.height -= diff; this.prev_pageY = ev.pageY; this.setChatBoxHeight(this.height); }, clearMessages: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); if (result === true) { this.$el.find('.chat-content').empty(); this.model.messages.reset(); this.model.messages.browserStorage._clear(); } return this; }, insertEmoticon: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); var $textbox = this.$el.find('textarea.chat-textarea'); var value = $textbox.val(); var $target = $(ev.target); $target = $target.is('a') ? $target : $target.children('a'); if (value && (value[value.length-1] !== ' ')) { value = value + ' '; } $textbox.focus().val(value+$target.data('emoticon')+' '); }, toggleEmoticonMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-smiley ul').slideToggle(200); }, toggleOTRMenu: function (ev) { ev.stopPropagation(); this.$el.find('.toggle-otr ul').slideToggle(200); }, showOTRError: function (msg) { if (msg == 'Message cannot be sent at this time.') { this.showHelpMessages( [__('Your message could not be sent')], 'error'); } else if (msg == 'Received an unencrypted message.') { this.showHelpMessages( [__('We received an unencrypted message')], 'error'); } else if (msg == 'Received an unreadable encrypted message.') { this.showHelpMessages( [__('We received an unreadable encrypted message')], 'error'); } else { this.showHelpMessages(['Encryption error occured: '+msg], 'error'); } console.log("OTR ERROR:"+msg); }, buddyStartsOTR: function (ev) { this.showHelpMessages([__('This user has requested an encrypted session.')]); this.model.initiateOTR(); }, startOTRFromToolbar: function (ev) { $(ev.target).parent().parent().slideUp(); ev.stopPropagation(); this.model.initiateOTR(); }, endOTR: function (ev) { if (typeof ev !== "undefined") { ev.preventDefault(); ev.stopPropagation(); } this.model.endOTR(); }, authOTR: function (ev) { var scheme = $(ev.target).data().scheme; var result, question, answer; if (scheme === 'fingerprint') { result = confirm(__('Here are the fingerprints, please confirm them with %1$s, outside of this chat.\n\nFingerprint for you, %2$s: %3$s\n\nFingerprint for %1$s: %4$s\n\nIf you have confirmed that the fingerprints match, click OK, otherwise click Cancel.', [ this.model.get('fullname'), converse.xmppstatus.get('fullname')||converse.bare_jid, this.model.otr.priv.fingerprint(), this.model.otr.their_priv_pk.fingerprint() ] )); if (result === true) { this.model.save({'otr_status': VERIFIED}); } else { this.model.save({'otr_status': UNVERIFIED}); } } else if (scheme === 'smp') { alert(__('You will be prompted to provide a security question and then an answer to that question.\n\nYour buddy will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.')); question = prompt(__('What is your security question?')); if (question) { answer = prompt(__('What is the answer to the security question?')); this.model.otr.smpSecret(answer, question); } } else { this.showHelpMessages([__('Invalid authentication scheme provided')], 'error'); } }, toggleCall: function (ev) { ev.stopPropagation(); converse.emit('callButtonClicked', { connection: converse.connection, model: this.model }); }, onChange: function (item, changed) { if (_.has(item.changed, 'chat_status')) { var chat_status = item.get('chat_status'), fullname = item.get('fullname'); fullname = _.isEmpty(fullname)? item.get('jid'): fullname; if (this.$el.is(':visible')) { if (chat_status === 'offline') { this.showStatusNotification(fullname+' '+'has gone offline'); } else if (chat_status === 'away') { this.showStatusNotification(fullname+' '+'has gone away'); } else if ((chat_status === 'dnd')) { this.showStatusNotification(fullname+' '+'is busy'); } else if (chat_status === 'online') { this.$el.find('div.chat-event').remove(); } } converse.emit('buddyStatusChanged', item.attributes, item.get('chat_status')); } if (_.has(item.changed, 'status')) { this.showStatusMessage(); converse.emit('buddyStatusMessageChanged', item.attributes, item.get('status')); } if (_.has(item.changed, 'image')) { this.renderAvatar(); } if (_.has(item.changed, 'otr_status')) { this.renderToolbar().informOTRChange(); } if (_.has(item.changed, 'minimized')) { if (item.get('minimized')) { this.hide(); } else { this.maximize(); } } // TODO check for changed fullname as well }, showStatusMessage: function (msg) { msg = msg || this.model.get('status'); if (msg) { this.$el.find('p.user-custom-message').text(msg).attr('title', msg); } return this; }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } if (converse.connection) { this.model.destroy(); } else { this.model.trigger('hide'); } converse.emit('chatBoxClosed', this); return this; }, maximize: function () { // Restores a minimized chat box this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el).show('fast', $.proxy(function () { converse.refreshWebkit(); this.focus(); converse.emit('chatBoxMaximized', this); }, this)); }, minimize: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } // Minimizes a chat box this.model.minimize(); this.$el.hide('fast', converse.refreshwebkit); converse.emit('chatBoxMinimized', this); }, updateVCard: function () { var jid = this.model.get('jid'), contact = converse.roster.get(jid); if ((contact) && (!contact.get('vcard_updated'))) { converse.getVCard( jid, $.proxy(function (jid, fullname, image, image_type, url) { this.model.save({ 'fullname' : fullname || jid, 'url': url, 'image_type': image_type, 'image': image }); }, this), $.proxy(function (stanza) { converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); }, this) ); } }, informOTRChange: function () { var data = this.model.toJSON(); var msgs = []; if (data.otr_status == UNENCRYPTED) { msgs.push(__("Your messages are not encrypted anymore")); } else if (data.otr_status == UNVERIFIED){ msgs.push(__("Your messages are now encrypted but your buddy's identity has not been verified.")); } else if (data.otr_status == VERIFIED){ msgs.push(__("Your buddy's identify has been verified.")); } else if (data.otr_status == FINISHED){ msgs.push(__("Your buddy has ended encryption on their end, you should do the same.")); } return this.showHelpMessages(msgs, 'info', false); }, renderToolbar: function () { if (converse.show_toolbar) { var data = this.model.toJSON(); if (data.otr_status == UNENCRYPTED) { data.otr_tooltip = __('Your messages are not encrypted. Click here to enable OTR encryption.'); } else if (data.otr_status == UNVERIFIED){ data.otr_tooltip = __('Your messages are encrypted, but your buddy has not been verified.'); } else if (data.otr_status == VERIFIED){ data.otr_tooltip = __('Your messages are encrypted and your buddy verified.'); } else if (data.otr_status == FINISHED){ data.otr_tooltip = __('Your buddy has closed their end of the private session, you should do the same'); } this.$el.find('.chat-toolbar').html( converse.templates.toolbar( _.extend(data, { FINISHED: FINISHED, UNENCRYPTED: UNENCRYPTED, UNVERIFIED: UNVERIFIED, VERIFIED: VERIFIED, allow_otr: converse.allow_otr && !this.is_chatroom, label_clear: __('Clear all messages'), label_end_encrypted_conversation: __('End encrypted conversation'), label_hide_participants: __('Hide the list of participants'), label_refresh_encrypted_conversation: __('Refresh encrypted conversation'), label_start_call: __('Start a call'), label_start_encrypted_conversation: __('Start encrypted conversation'), label_verify_with_fingerprints: __('Verify with fingerprints'), label_verify_with_smp: __('Verify with SMP'), label_whats_this: __("What\'s this?"), otr_status_class: OTR_CLASS_MAPPING[data.otr_status], otr_translated_status: OTR_TRANSLATED_MAPPING[data.otr_status], show_call_button: converse.visible_toolbar_buttons.call, show_clear_button: converse.visible_toolbar_buttons.clear, show_emoticons: converse.visible_toolbar_buttons.emoticons, show_participants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_participants }) ) ); } return this; }, renderAvatar: function () { if (!this.model.get('image')) { return; } var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), canvas = $('').get(0); if (!(canvas.getContext && canvas.getContext('2d'))) { return this; } var ctx = canvas.getContext('2d'); var img = new Image(); // Create new Image object img.onload = function() { var ratio = img.width/img.height; ctx.drawImage(img, 0,0, 35*ratio, 35); }; img.src = img_src; this.$el.find('.chat-title').before(canvas); return this; }, focus: function () { this.$el.find('.chat-textarea').focus(); converse.emit('chatBoxFocused', this); return this; }, hide: function () { if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { this.$el.hide(); converse.refreshWebkit(); } return this; }, show: function (callback) { if (this.$el.is(':visible') && this.$el.css('opacity') == "1") { return this.focus(); } this.$el.fadeIn(callback); if (converse.connection) { // Without a connection, we haven't yet initialized // localstorage this.model.save(); this.initDragResize(); } return this; }, scrollDown: function () { var $content = this.$('.chat-content'); if ($content.is(':visible')) { $content.scrollTop($content[0].scrollHeight); } return this; } }); this.ContactsPanel = Backbone.View.extend({ tagName: 'div', className: 'controlbox-pane', id: 'users', events: { 'click a.toggle-xmpp-contact-form': 'toggleContactForm', 'submit form.add-xmpp-contact': 'addContactFromForm', 'submit form.search-xmpp-contact': 'searchContacts', 'click a.subscribe-to-user': 'addContactFromList' }, initialize: function (cfg) { cfg.$parent.append(this.$el); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); }, render: function () { var markup; var widgets = converse.templates.contacts_panel({ label_online: __('Online'), label_busy: __('Busy'), label_away: __('Away'), label_offline: __('Offline') }); this.$tabs.append(converse.templates.contacts_tab({label_contacts: LABEL_CONTACTS})); if (converse.xhr_user_search) { markup = converse.templates.search_contact({ label_contact_name: __('Contact name'), label_search: __('Search') }); } else { markup = converse.templates.add_contact_form({ label_contact_username: __('Contact username'), label_add: __('Add') }); } if (converse.allow_contact_requests) { widgets += converse.templates.add_contact_dropdown({ label_click_to_chat: __('Click to add new chat contacts'), label_add_contact: __('Add a contact') }); } this.$el.html(widgets); this.$el.find('.search-xmpp ul').append(markup); this.$el.append(converse.rosterview.$el); converse.rosterview.update(); // Will render live filter if needed. return this; }, toggleContactForm: function (ev) { ev.preventDefault(); this.$el.find('.search-xmpp').toggle('fast', function () { if ($(this).is(':visible')) { $(this).find('input.username').focus(); } }); }, searchContacts: function (ev) { ev.preventDefault(); $.getJSON(converse.xhr_user_search_url+ "?q=" + $(ev.target).find('input.username').val(), function (data) { var $ul= $('.search-xmpp ul'); $ul.find('li.found-user').remove(); $ul.find('li.chat-info').remove(); if (!data.length) { $ul.append('
  • '+__('No users found')+'
  • '); } $(data).each(function (idx, obj) { $ul.append( $('
  • ') .append( $('') .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain) .text(obj.fullname) ) ); }); }); }, addContactFromForm: function (ev) { ev.preventDefault(); var $input = $(ev.target).find('input'); var jid = $input.val(); if (! jid) { // this is not a valid JID $input.addClass('error'); return; } this.addContact(jid); $('.search-xmpp').hide(); }, addContactFromList: function (ev) { ev.preventDefault(); var $target = $(ev.target), jid = $target.attr('data-recipient'), name = $target.text(); this.addContact(jid, name); $target.parent().remove(); $('.search-xmpp').hide(); }, addContact: function (jid, name) { name = _.isEmpty(name)? jid: name; converse.connection.roster.add(jid, name, [], function (iq) { converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname')); }); } }); this.RoomsPanel = Backbone.View.extend({ tagName: 'div', id: 'chatrooms', events: { 'submit form.add-chatroom': 'createChatRoom', 'click input#show-rooms': 'showRooms', 'click a.open-room': 'createChatRoom', 'click a.room-info': 'showRoomInfo' }, initialize: function (cfg) { cfg.$parent.append( this.$el.html( converse.templates.room_panel({ 'server_input_type': converse.hide_muc_server && 'hidden' || 'text', 'label_room_name': __('Room name'), 'label_nickname': __('Nickname'), 'label_server': __('Server'), 'label_join': __('Join'), 'label_show_rooms': __('Show rooms') }) ).hide()); this.$tabs = cfg.$parent.parent().find('#controlbox-tabs'); this.on('update-rooms-list', function (ev) { this.updateRoomsList(); }); converse.xmppstatus.on("change", $.proxy(function (model) { if (!(_.has(model.changed, 'fullname'))) { return; } var $nick = this.$el.find('input.new-chatroom-nick'); if (! $nick.is(':focus')) { $nick.val(model.get('fullname')); } }, this)); }, render: function () { this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')})); return this; }, informNoRoomsFound: function () { var $available_chatrooms = this.$el.find('#available-chatrooms'); // # For translators: %1$s is a variable and will be replaced with the XMPP server name $available_chatrooms.html('
    '+__('No rooms on %1$s',this.muc_domain)+'
    '); $('input#show-rooms').show().siblings('span.spinner').remove(); }, updateRoomsList: function (domain) { converse.connection.muc.listRooms( this.muc_domain, $.proxy(function (iq) { // Success var name, jid, i, fragment, that = this, $available_chatrooms = this.$el.find('#available-chatrooms'); this.rooms = $(iq).find('query').find('item'); if (this.rooms.length) { // # For translators: %1$s is a variable and will be // # replaced with the XMPP server name $available_chatrooms.html('
    '+__('Rooms on %1$s',this.muc_domain)+'
    '); fragment = document.createDocumentFragment(); for (i=0; i'); this.muc_domain = server; this.updateRoomsList(); }, showRoomInfo: function (ev) { var target = ev.target, $dd = $(target).parent('dd'), $div = $dd.find('div.room-info'); if ($div.length) { $div.remove(); } else { $dd.find('span.spinner').remove(); $dd.append(''); converse.connection.disco.info( $(target).attr('data-room-jid'), null, $.proxy(function (stanza) { var $stanza = $(stanza); // All MUC features found here: http://xmpp.org/registrar/disco-features.html $dd.find('span.spinner').replaceWith( converse.templates.room_description({ 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(), 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(), 'hidden': $stanza.find('feature[var="muc_hidden"]').length, 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length, 'moderated': $stanza.find('feature[var="muc_moderated"]').length, 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length, 'open': $stanza.find('feature[var="muc_open"]').length, 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length, 'persistent': $stanza.find('feature[var="muc_persistent"]').length, 'publicroom': $stanza.find('feature[var="muc_public"]').length, 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length, 'temporary': $stanza.find('feature[var="muc_temporary"]').length, 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length, 'label_desc': __('Description:'), 'label_occ': __('Occupants:'), 'label_features': __('Features:'), 'label_requires_auth': __('Requires authentication'), 'label_hidden': __('Hidden'), 'label_requires_invite': __('Requires an invitation'), 'label_moderated': __('Moderated'), 'label_non_anon': __('Non-anonymous'), 'label_open_room': __('Open room'), 'label_permanent_room': __('Permanent room'), 'label_public': __('Public'), 'label_semi_anon': _('Semi-anonymous'), 'label_temp_room': _('Temporary room'), 'label_unmoderated': __('Unmoderated') })); }, this)); } }, createChatRoom: function (ev) { ev.preventDefault(); var name, $name, server, $server, jid, $nick = this.$el.find('input.new-chatroom-nick'), nick = $nick.val(), chatroom; if (!nick) { $nick.addClass('error'); } else { $nick.removeClass('error'); } if (ev.type === 'click') { jid = $(ev.target).attr('data-room-jid'); } else { $name = this.$el.find('input.new-chatroom-name'); $server= this.$el.find('input.new-chatroom-server'); server = $server.val(); name = $name.val().trim().toLowerCase(); $name.val(''); // Clear the input if (name && server) { jid = Strophe.escapeNode(name) + '@' + server; $name.removeClass('error'); $server.removeClass('error'); this.muc_domain = server; } else { if (!name) { $name.addClass('error'); } if (!server) { $server.addClass('error'); } return; } } if (!nick) { return; } chatroom = converse.chatboxviews.showChat({ 'id': jid, 'jid': jid, 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)), 'nick': nick, 'chatroom': true, 'box_id' : b64_sha1(jid) }); if (!chatroom.get('connected')) { converse.chatboxviews.get(jid).connect(null); } } }); this.ControlBoxView = converse.ChatBoxView.extend({ tagName: 'div', className: 'chatbox', id: 'controlbox', events: { 'click a.close-chatbox-button': 'close', 'click ul#controlbox-tabs li a': 'switchTab', 'mousedown .dragresize-tm': 'onDragResizeStart' }, initialize: function () { this.$el.insertAfter(converse.controlboxtoggle.$el); this.model.on('change', $.proxy(function (item, changed) { var i; if (_.has(item.changed, 'connected')) { this.render(); converse.features.on('add', $.proxy(this.featureAdded, this)); // Features could have been added before the controlbox was // initialized. Currently we're only interested in MUC var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'}); if (feature) { this.featureAdded(feature); } } }, this)); this.model.on('show', this.show, this); this.model.on('destroy', this.hide, this); this.model.on('hide', this.hide, this); }, render: function () { if ((!converse.prebind) && (!converse.connection)) { // Add login panel if the user still has to authenticate this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.loginpanel = new converse.LoginPanel({'$parent': this.$el.find('.controlbox-panes'), 'model': this}); this.loginpanel.render(); this.initDragResize(); } else if (!this.contactspanel) { this.$el.html(converse.templates.controlbox(this.model.toJSON())); this.contactspanel = new converse.ContactsPanel({'$parent': this.$el.find('.controlbox-panes')}); this.contactspanel.render(); converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus}); converse.xmppstatusview.render(); if (converse.allow_muc) { this.roomspanel = new converse.RoomsPanel({'$parent': this.$el.find('.controlbox-panes')}); this.roomspanel.render(); } this.initDragResize(); } return this; }, hide: function (callback) { this.$el.hide('fast', function () { converse.refreshWebkit(); converse.emit('chatBoxClosed', this); converse.controlboxtoggle.show(function () { if (typeof callback === "function") { callback(); } }); }); }, show: function () { converse.controlboxtoggle.hide($.proxy(function () { this.$el.show('fast', function () { converse.refreshWebkit(); }.bind(this)); converse.emit('controlBoxOpened', this); }, this)); return this; }, featureAdded: function (feature) { if ((feature.get('var') == 'http://jabber.org/protocol/muc') && (converse.allow_muc)) { this.roomspanel.muc_domain = feature.get('from'); var $server= this.$el.find('input.new-chatroom-server'); if (! $server.is(':focus')) { $server.val(this.roomspanel.muc_domain); } if (converse.auto_list_rooms) { this.roomspanel.trigger('update-rooms-list'); } } }, switchTab: function (ev) { ev.preventDefault(); var $tab = $(ev.target), $sibling = $tab.parent().siblings('li').children('a'), $tab_panel = $($tab.attr('href')); $($sibling.attr('href')).hide(); $sibling.removeClass('current'); $tab.addClass('current'); $tab_panel.show(); }, showHelpMessages: function (msgs) { // Override showHelpMessages in ChatBoxView, for now do nothing. return; } }); this.ChatRoomView = converse.ChatBoxView.extend({ length: 300, tagName: 'div', className: 'chatroom', events: { 'click .close-chatbox-button': 'close', 'click .toggle-chatbox-button': 'minimize', 'click .configure-chatroom-button': 'configureChatRoom', 'click .toggle-smiley': 'toggleEmoticonMenu', 'click .toggle-smiley ul li': 'insertEmoticon', 'click .toggle-clear': 'clearChatRoomMessages', 'click .toggle-participants a': 'toggleParticipants', 'keypress textarea.chat-textarea': 'keyPressed', 'mousedown .dragresize-tm': 'onDragResizeStart' }, is_chatroom: true, initialize: function () { this.connect(null); this.model.messages.on('add', this.onMessageAdded, this); this.model.on('change:minimized', function (item) { if (item.get('minimized')) { this.hide(); } else { this.maximize(); } }, this); this.model.on('destroy', function (model, response, options) { this.hide(); converse.connection.muc.leave( this.model.get('jid'), this.model.get('nick'), $.proxy(this.onLeave, this), undefined); }, this); this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el); this.render().model.messages.fetch({add: true}); if (this.model.get('minimized')) { this.hide(); } else { this.show(); } }, render: function () { this.$el.attr('id', this.model.get('box_id')) .html(converse.templates.chatroom(this.model.toJSON())); converse.emit('chatRoomOpened', this); setTimeout(function () { converse.refreshWebkit(); }, 50); return this; }, onCommandError: function (stanza) { this.showStatusNotification(__("Error: could not execute the command"), true); }, sendChatRoomMessage: function (body) { var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false], $chat_content, args; switch (match[1]) { case 'ban': args = match[2].splitOnce(' '); converse.connection.muc.ban(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; case 'clear': this.clearChatRoomMessages(); break; case 'deop': args = match[2].splitOnce(' '); converse.connection.muc.deop(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; case 'help': $chat_content = this.$el.find('.chat-content'); msgs = [ '/ban: ' +__('Ban user from room'), '/clear: ' +__('Remove messages'), '/help: ' +__('Show this menu'), '/kick: ' +__('Kick user from room'), '/me: ' +__('Write in 3rd person'), '/mute: ' +__("Remove user's ability to post messages"), '/nick: ' +__('Change your nickname'), '/topic: ' +__('Set room topic'), '/voice: ' +__('Allow muted user to post messages') ]; this.showHelpMessages(msgs); break; case 'kick': args = match[2].splitOnce(' '); converse.connection.muc.kick(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; case 'mute': args = match[2].splitOnce(' '); converse.connection.muc.mute(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; case 'nick': converse.connection.muc.changeNick(this.model.get('jid'), match[2]); break; case 'op': args = match[2].splitOnce(' '); converse.connection.muc.op(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; case 'topic': converse.connection.muc.setTopic(this.model.get('jid'), match[2]); break; case 'voice': args = match[2].splitOnce(' '); converse.connection.muc.voice(this.model.get('jid'), args[0], args[1], undefined, $.proxy(this.onCommandError, this)); break; default: this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body); break; } }, initInviteWidget: function () { var $el = this.$('input.invited-contact'); $el.typeahead({ minLength: 1, highlight: true }, { name: 'contacts-dataset', source: function (q, cb) { var results = []; _.each(converse.roster.filter(contains(['fullname', 'jid'], q)), function (n) { results.push({value: n.get('fullname'), jid: n.get('jid')}); }); cb(results); }, templates: { suggestion: _.template('

    {{value}}

    ') } }); $el.on('typeahead:selected', $.proxy(function (ev, suggestion, dname) { var reason = prompt( __(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.value, this.model.get('id')) + __("You may optionally include a message, explaining the reason for the invitation.") ); if (reason !== null) { converse.connection.muc.rooms[this.model.get('id')].directInvite(suggestion.jid, reason); converse.emit('roomInviteSent', this, suggestion.jid, reason); } $(ev.target).typeahead('val', ''); }, this)); return this; }, renderChatArea: function () { if (!this.$el.find('.chat-area').length) { this.$el.find('.chat-body').empty().append( converse.templates.chatarea({ 'show_toolbar': converse.show_toolbar, 'label_message': __('Message'), 'label_invitation': __('Invite...') }) ); this.initInviteWidget().renderToolbar(); } return this; }, connect: function (password) { if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) { // If the room exists, it already has event listeners, so we // doing add them again. converse.connection.muc.join( this.model.get('jid'), this.model.get('nick'), null, null, null, password); } else { converse.connection.muc.join( this.model.get('jid'), this.model.get('nick'), $.proxy(this.onChatRoomMessage, this), $.proxy(this.onChatRoomPresence, this), $.proxy(this.onChatRoomRoster, this), password); } }, onLeave: function () { this.model.set('connected', false); }, renderConfigurationForm: function (stanza) { var $form= this.$el.find('form.chatroom-form'), $stanza = $(stanza), $fields = $stanza.find('field'), title = $stanza.find('title').text(), instructions = $stanza.find('instructions').text(), i, j, options=[], $field, $options; var input_types = { 'text-private': 'password', 'text-single': 'textline', 'boolean': 'checkbox', 'hidden': 'hidden', 'list-single': 'dropdown' }; $form.find('span.spinner').remove(); $form.append($('').text(title)); if (instructions != title) { $form.append($('

    ').text(instructions)); } for (i=0; i<$fields.length; i++) { $field = $($fields[i]); if ($field.attr('type') == 'list-single') { options = []; $options = $field.find('option'); for (j=0; j<$options.length; j++) { options.push(converse.templates.select_option({ value: $($options[j]).find('value').text(), label: $($options[j]).attr('label') })); } $form.append(converse.templates.form_select({ name: $field.attr('var'), label: $field.attr('label'), options: options.join('') })); } else if ($field.attr('type') == 'boolean') { $form.append(converse.templates.form_checkbox({ name: $field.attr('var'), type: input_types[$field.attr('type')], label: $field.attr('label') || '', checked: $field.find('value').text() === "1" && 'checked="1"' || '' })); } else { $form.append(converse.templates.form_input({ name: $field.attr('var'), type: input_types[$field.attr('type')], label: $field.attr('label') || '', value: $field.find('value').text() })); } } $form.append(''); $form.append(''); $form.on('submit', $.proxy(this.saveConfiguration, this)); $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this)); }, saveConfiguration: function (ev) { ev.preventDefault(); var that = this; var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'), count = $inputs.length, configArray = []; $inputs.each(function () { var $input = $(this), value; if ($input.is('[type=checkbox]')) { value = $input.is(':checked') && 1 || 0; } else { value = $input.val(); } var cnode = $(converse.templates.field({ name: $input.attr('name'), value: value }))[0]; configArray.push(cnode); if (!--count) { converse.connection.muc.saveConfiguration( that.model.get('jid'), configArray, $.proxy(that.onConfigSaved, that), $.proxy(that.onErrorConfigSaved, that) ); } }); this.$el.find('div.chatroom-form-container').hide( function () { $(this).remove(); that.$el.find('.chat-area').show(); that.$el.find('.participants').show(); }); }, onConfigSaved: function (stanza) { // XXX }, onErrorConfigSaved: function (stanza) { this.showStatusNotification(__("An error occurred while trying to save the form.")); }, cancelConfiguration: function (ev) { ev.preventDefault(); var that = this; this.$el.find('div.chatroom-form-container').hide( function () { $(this).remove(); that.$el.find('.chat-area').show(); that.$el.find('.participants').show(); }); }, configureChatRoom: function (ev) { ev.preventDefault(); if (this.$el.find('div.chatroom-form-container').length) { return; } this.$el.find('.chat-area').hide(); this.$el.find('.participants').hide(); this.$el.find('.chat-body').append( $('

    '+ '
    '+ ''+ ''+ '
    ')); converse.connection.muc.configure( this.model.get('jid'), $.proxy(this.renderConfigurationForm, this) ); }, submitPassword: function (ev) { ev.preventDefault(); var password = this.$el.find('.chatroom-form').find('input[type=password]').val(); this.$el.find('.chatroom-form-container').replaceWith( ''); this.connect(password); }, renderPasswordForm: function () { this.$el.find('span.centered.spinner').remove(); this.$el.find('.chat-body').html( $('
    '+ '
    '+ ''+__('This chatroom requires a password')+'' + '' + '%1$s has been banned"), 303: ___("%1$s's nickname has changed"), 307: ___("%1$s has been kicked out"), 321: ___("%1$s has been removed because of an affiliation change"), 322: ___("%1$s has been removed for not being a member") }, newNicknameMessages: { 210: ___('Your nickname has been automatically changed to: %1$s'), 303: ___('Your nickname has been changed to: %1$s') }, showStatusMessages: function ($el, is_self) { /* Check for status codes and communicate their purpose to the user. * Allow user to configure chat room if they are the owner. * See: http://xmpp.org/registrar/mucstatus.html */ var $chat_content, disconnect_msgs = [], msgs = [], reasons = []; $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each($.proxy(function (idx, x) { var $item = $(x).find('item'); if (Strophe.getBareJidFromJid($item.attr('jid')) === converse.bare_jid && $item.attr('affiliation') === 'owner') { this.$el.find('a.configure-chatroom-button').show(); } $(x).find('item reason').each(function (idx, reason) { if ($(reason).text()) { reasons.push($(reason).text()); } }); $(x).find('status').each($.proxy(function (idx, stat) { var code = stat.getAttribute('code'); if (is_self && _.contains(_.keys(this.newNicknameMessages), code)) { this.model.save({'nick': Strophe.getResourceFromJid($el.attr('from'))}); msgs.push(__(this.newNicknameMessages[code], $item.attr('nick'))); } else if (is_self && _.contains(_.keys(this.disconnectMessages), code)) { disconnect_msgs.push(this.disconnectMessages[code]); } else if (!is_self && _.contains(_.keys(this.actionInfoMessages), code)) { msgs.push( __(this.actionInfoMessages[code], Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')))) ); } else if (_.contains(_.keys(this.infoMessages), code)) { msgs.push(this.infoMessages[code]); } else if (code !== '110') { if ($(stat).text()) { msgs.push($(stat).text()); // Sometimes the status contains human readable text and not a code. } } }, this)); }, this)); if (disconnect_msgs.length > 0) { for (i=0; i 0, subject = $message.children('subject').text(), match, template, dates, message_datetime, message_date, message_date_str; if (delayed) { message_datetime = moment($message.find('delay').attr('stamp')); } else { message_datetime = moment(); } // If this message is on a different day than the one received // prior, then indicate it on the chatbox. dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get(); message_date = message_datetime.clone().startOf('day'); message_date_str = message_date.format("YYYY-MM-DD"); if (_.indexOf(dates, message_date_str) === -1) { $chat_content.append(converse.templates.new_day({ isodate: message_date_str, datestring: message_date.format("dddd MMM Do YYYY") })); } this.showStatusMessages($message); if (subject) { this.$el.find('.chatroom-topic').text(subject).attr('title', subject); // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively // # Example: Topic set by JC Brand to: Hello World! $chat_content.append( converse.templates.info({ 'message': __('Topic set by %1$s to: %2$s', sender, subject) })); } if (!body) { return true; } var display_sender = sender === this.model.get('nick') && 'me' || 'room'; if (!delayed && display_sender === 'room' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(body)) { playNotification(); } this.showMessage({ 'message': body, 'sender': display_sender, 'fullname': sender, 'time': message_datetime.format() }); if (display_sender === 'room') { // We only emit an event if it's not our own message converse.emit('message', message); } return true; }, onChatRoomRoster: function (roster, room) { this.renderChatArea(); var controlboxview = converse.chatboxviews.get('controlbox'), roster_size = _.size(roster), $participant_list = this.$el.find('.participant-list'), participants = [], keys = _.keys(roster), i; this.$el.find('.participant-list').empty(); for (i=0; i'); $('body').append($el); } $el.html(converse.templates.chats_panel()); this.setElement($el, false); } else { this.setElement(_.result(this, 'el'), false); } }, onChatAdded: function (item) { var view = this.get(item.get('id')); if (!view) { if (item.get('chatroom')) { view = new converse.ChatRoomView({'model': item}); } else if (item.get('box_id') === 'controlbox') { view = new converse.ControlBoxView({model: item}).render(); } else { view = new converse.ChatBoxView({model: item}); } this.add(item.get('id'), view); } else { delete view.model; // Remove ref to old model to help garbage collection view.model = item; view.initialize(); } this.trimChats(view); }, trimChats: function (newchat) { /* This method is called when a newly created chat box will * be shown. * * It checks whether there is enough space on the page to show * another chat box. Otherwise it minimize the oldest chat box * to create space. */ if (converse.no_trimming || (this.model.length <= 1)) { return; } var oldest_chat, controlbox_width = 0, $minimized = converse.minimized_chats.$el, minimized_width = _.contains(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0, boxes_width = newchat ? newchat.$el.outerWidth(true) : 0, new_id = newchat ? newchat.model.get('id') : null, controlbox = this.get('controlbox'); if (!controlbox || !controlbox.$el.is(':visible')) { controlbox_width = converse.controlboxtoggle.$el.outerWidth(true); } else { controlbox_width = controlbox.$el.outerWidth(true); } _.each(this.getAll(), function (view) { var id = view.model.get('id'); if ((id !== 'controlbox') && (id !== new_id) && (!view.model.get('minimized'))) { boxes_width += view.$el.outerWidth(true); } }); if ((minimized_width + boxes_width + controlbox_width) > this.$el.outerWidth(true)) { oldest_chat = this.getOldestMaximizedChat(); if (oldest_chat) { oldest_chat.minimize(); } } }, getOldestMaximizedChat: function () { // Get oldest view (which is not controlbox) var i = 0; var model = this.model.sort().at(i); while (model.get('id') === 'controlbox' || model.get('minimized') === true) { i++; model = this.model.at(i); if (!model) { return null; } } return model; }, showChat: function (attrs) { /* Find the chat box and show it. * If it doesn't exist, create it. */ var chatbox = this.model.get(attrs.jid); if (chatbox) { if (chatbox.get('minimized')) { chatbox.maximize(); } else { chatbox.trigger('show'); } } else { chatbox = this.model.create(attrs, { 'error': function (model, response) { converse.log(response.responseText); } }); } return chatbox; } }); this.MinimizedChatBoxView = Backbone.View.extend({ tagName: 'div', className: 'chat-head', events: { 'click .close-chatbox-button': 'close', 'click .restore-chat': 'restore' }, initialize: function () { this.model.messages.on('add', this.updateUnreadMessagesCounter, this); this.model.on('showSentOTRMessage', this.updateUnreadMessagesCounter, this); this.model.on('showReceivedOTRMessage', this.updateUnreadMessagesCounter, this); this.model.on('change:minimized', this.clearUnreadMessagesCounter, this); }, render: function () { var data = _.extend( this.model.toJSON(), { 'tooltip': __('Click to restore this chat') } ); if (this.model.get('chatroom')) { data.title = this.model.get('name'); this.$el.addClass('chat-head-chatroom'); } else { data.title = this.model.get('fullname'); this.$el.addClass('chat-head-chatbox'); } return this.$el.html(converse.templates.trimmed_chat(data)); }, clearUnreadMessagesCounter: function () { this.model.set({'num_unread': 0}); this.render(); }, updateUnreadMessagesCounter: function () { this.model.set({'num_unread': this.model.get('num_unread') + 1}); this.render(); }, close: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.remove(); this.model.destroy(); converse.emit('chatBoxClosed', this); return this; }, restore: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.remove(); this.model.maximize(); }, 200) }); this.MinimizedChats = Backbone.Overview.extend({ el: "#minimized-chats", events: { "click #toggle-minimized-chats": "toggle" }, initialize: function () { this.initToggle(); this.model.on("add", this.onChanged, this); this.model.on("destroy", this.removeChat, this); this.model.on("change:minimized", this.onChanged, this); this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this); }, initToggle: function () { this.toggleview = new converse.MinimizedChatsToggleView({ model: new converse.MinimizedChatsToggle() }); var id = b64_sha1('converse.minchatstoggle'+converse.bare_jid); this.toggleview.model.id = id; // Appears to be necessary for backbone.browserStorage this.toggleview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id); this.toggleview.model.fetch(); }, render: function () { if (this.keys().length === 0) { this.$el.hide('fast'); } else if (this.keys().length === 1) { this.$el.show('fast'); } return this.$el; }, toggle: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')}); this.$('.minimized-chats-flyout').toggle(); }, onChanged: function (item) { if (item.get('id') !== 'controlbox' && item.get('minimized')) { this.addChat(item); } else if (this.get(item.get('id'))) { this.removeChat(item); } }, addChat: function (item) { var existing = this.get(item.get('id')); if (existing && existing.$el.parent().length !== 0) { return; } var view = new converse.MinimizedChatBoxView({model: item}); this.$('.minimized-chats-flyout').append(view.render()); this.add(item.get('id'), view); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, removeChat: function (item) { this.remove(item.get('id')); this.toggleview.model.set({'num_minimized': this.keys().length}); this.render(); }, updateUnreadMessagesCounter: function () { var ls = this.model.pluck('num_unread'), count = 0, i; for (i=0; i name2? 1 : 0); } else { return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1; } }, subscribeToSuggestedItems: function (msg) { $(msg).find('item').each(function () { var $this = $(this), jid = $this.attr('jid'), action = $this.attr('action'), fullname = $this.attr('name'); if (action === 'add') { converse.connection.roster.add(jid, fullname, [], function (iq) { converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname')); }); } }); return true; }, isSelf: function (jid) { return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid)); }, addResource: function (bare_jid, resource) { var item = this.get(bare_jid), resources; if (item) { resources = item.get('resources'); if (resources) { if (_.indexOf(resources, resource) == -1) { resources.push(resource); item.set({'resources': resources}); } } else { item.set({'resources': [resource]}); } } }, removeResource: function (bare_jid, resource) { var item = this.get(bare_jid), resources, idx; if (item) { resources = item.get('resources'); idx = _.indexOf(resources, resource); if (idx !== -1) { resources.splice(idx, 1); item.set({'resources': resources}); return resources.length; } } return 0; }, subscribeBack: function (jid) { var bare_jid = Strophe.getBareJidFromJid(jid); if (converse.connection.roster.findItem(bare_jid)) { converse.connection.roster.authorize(bare_jid); converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname')); } else { converse.connection.roster.add(jid, '', [], function (iq) { converse.connection.roster.authorize(bare_jid); converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname')); }); } }, unsubscribe: function (jid) { /* Upon receiving the presence stanza of type "unsubscribed", * the user SHOULD acknowledge receipt of that subscription state * notification by sending a presence stanza of type "unsubscribe" * this step lets the user's server know that it MUST no longer * send notification of the subscription state change to the user. */ converse.xmppstatus.sendPresence('unsubscribe'); if (converse.connection.roster.findItem(jid)) { converse.connection.roster.remove(jid, function (iq) { converse.rosterview.model.remove(jid); }); } }, getNumOnlineContacts: function () { var count = 0, ignored = ['offline', 'unavailable'], models = this.models, models_length = models.length, i; if (converse.show_only_online_users) { ignored = _.union(ignored, ['dnd', 'xa', 'away']); } for (i=0; i b.toLowerCase() ? 1 : 0); } else if (a_is_special && b_is_special) { return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0); } else if (!a_is_special && b_is_special) { return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1; } else if (a_is_special && !b_is_special) { return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1; } } }); this.RosterView = Backbone.Overview.extend({ tagName: 'div', id: 'converse-roster', events: { "keydown .roster-filter": "liveFilter", "click .onX": "clearFilter", "mousemove .x": "togglePointer", "change .filter-type": "changeFilterType" }, initialize: function () { this.registerRosterHandler(); this.registerRosterXHandler(); this.registerPresenceHandler(); converse.roster.on("add", this.onContactAdd, this); converse.roster.on('change', this.onContactChange, this); converse.roster.on("destroy", this.update, this); converse.roster.on("remove", this.update, this); this.model.on("add", this.onGroupAdd, this); this.model.on("reset", this.reset, this); this.render(); this.model.fetch({ silent: true, success: $.proxy(this.positionFetchedGroups, this) }); converse.roster.fetch({add: true}); }, render: function () { this.$el.html(converse.templates.roster({ placeholder: __('Type to filter'), label_contacts: LABEL_CONTACTS, label_groups: LABEL_GROUPS })); return this; }, changeFilterType: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } this.clearFilter(); this.filter( this.$('.roster-filter').val(), ev.target.value ); }, tog: function (v) { return v?'addClass':'removeClass'; }, togglePointer: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var el = ev.target; $(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX'); }, filter: function (query, type) { var matches; query = query.toLowerCase(); if (type === 'groups') { _.each(this.getAll(), function (view, idx) { if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) { view.hide(); } else if (view.model.contacts.length > 0) { view.show(); } }); } else { _.each(this.getAll(), function (view) { view.filter(query, type); }); } }, liveFilter: _.debounce(function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } var q = ev.target.value; var t = this.$('.filter-type').val(); $(ev.target)[this.tog(q)]('x'); this.filter(q, t); }, 300), clearFilter: function (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); $(ev.target).removeClass('x onX').val(''); } this.filter(''); }, showHideFilter: function () { var $filter = this.$('.roster-filter'); var $type = this.$('.filter-type'); var visible = $filter.is(':visible'); if (visible && $filter.val().length > 0) { // Don't hide if user is currently filtering. return; } if (this.$('.roster-contacts').hasScrollBar()) { if (!visible) { $filter.show(); $type.show(); } } else { $filter.hide(); $type.hide(); } return this; }, update: function () { // XXX: Is this still being used/valid? var $count = $('#online-count'); $count.text('('+converse.roster.getNumOnlineContacts()+')'); if (!$count.is(':visible')) { $count.show(); } return this.showHideFilter(); }, reset: function () { converse.roster.reset(); this.removeAll(); this.render().update(); return this; }, registerRosterHandler: function () { // Register handlers that depend on the roster converse.connection.roster.registerCallback( $.proxy(converse.roster.rosterHandler, converse.roster), null, 'presence', null); }, registerRosterXHandler: function () { converse.connection.addHandler( $.proxy(converse.roster.subscribeToSuggestedItems, converse.roster), 'http://jabber.org/protocol/rosterx', 'message', null); }, registerPresenceHandler: function () { converse.connection.addHandler( $.proxy(function (presence) { converse.roster.presenceHandler(presence); return true; }, this), null, 'presence', null); }, onGroupAdd: function (group) { var view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); this.positionGroup(view); }, onContactAdd: function (contact) { this.addRosterContact(contact).update(); if (!contact.get('vcard_updated')) { // This will update the vcard, which triggers a change // request which will rerender the roster contact. converse.getVCard(contact.get('jid')); } }, onContactChange: function (contact) { this.updateChatBox(contact).update(); }, updateChatBox: function (contact, changed) { var chatbox = converse.chatboxes.get(contact.get('jid')), changes = {}; if (!chatbox) { return this; } if (_.has(contact.changed, 'chat_status')) { changes.chat_status = contact.get('chat_status'); } if (_.has(contact.changed, 'status')) { changes.status = contact.get('status'); } chatbox.save(changes); return this; }, positionFetchedGroups: function (model, resp, options) { /* Instead of throwing an add event for each group * fetched, we wait until they're all fetched and then * we position them. * Works around the problem of positionGroup not * working when all groups besides the one being * positioned aren't already in inserted into the * roster DOM element. */ model.sort(); model.each($.proxy(function (group, idx) { var view = this.get(group.get('name')); if (!view) { view = new converse.RosterGroupView({model: group}); this.add(group.get('name'), view.render()); } if (idx === 0) { this.$('.roster-contacts').append(view.$el); } else { this.appendGroup(view); } }, this)); }, positionGroup: function (view) { /* Place the group's DOM element in the correct alphabetical * position amongst the other groups in the roster. */ var index = this.model.indexOf(view.model); if (index === 0) { this.$('.roster-contacts').prepend(view.$el); } else if (index == (this.model.length-1)) { this.appendGroup(view); } else { $(this.$('.roster-group').eq(index)).before(view.$el); } return this; }, appendGroup: function (view) { /* Add the group at the bottom of the roster */ var $last = this.$('.roster-group').last(); var $siblings = $last.siblings('dd'); if ($siblings.length > 0) { $siblings.last().after(view.$el); } else { $last.after(view.$el); } return this; }, getGroup: function (name) { /* Returns the group as specified by name. * Creates the group if it doesn't exist. */ var view = this.get(name); if (view) { return view.model; } return this.model.create({name: name, id: b64_sha1(name)}); }, addContactToGroup: function (contact, name) { this.getGroup(name).contacts.add(contact); }, addCurrentContact: function (contact) { var groups; if (converse.roster_groups) { groups = contact.get('groups'); if (groups.length === 0) { groups = [HEADER_UNGROUPED]; } } else { groups = [HEADER_CURRENT_CONTACTS]; } _.each(groups, $.proxy(function (name) { this.addContactToGroup(contact, name); }, this)); }, addRosterContact: function (contact) { var view; if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') { this.addCurrentContact(contact); } else { view = (new converse.RosterContactView({model: contact})).render(); if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) { this.addContactToGroup(contact, HEADER_PENDING_CONTACTS); } else if (contact.get('requesting') === true) { this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS); } } return this; } }); this.XMPPStatus = Backbone.Model.extend({ initialize: function () { this.set({ 'status' : this.get('status') || 'online' }); this.on('change', $.proxy(function (item) { if (this.get('fullname') === undefined) { converse.getVCard( null, // No 'to' attr when getting one's own vCard $.proxy(function (jid, fullname, image, image_type, url) { this.save({'fullname': fullname}); }, this) ); } if (_.has(item.changed, 'status')) { converse.emit('statusChanged', this.get('status')); } if (_.has(item.changed, 'status_message')) { converse.emit('statusMessageChanged', this.get('status_message')); } }, this)); }, sendPresence: function (type) { if (type === undefined) { type = this.get('status') || 'online'; } var status_message = this.get('status_message'), presence; // Most of these presence types are actually not explicitly sent, // but I add all of them here fore reference and future proofing. if ((type === 'unavailable') || (type === 'probe') || (type === 'error') || (type === 'unsubscribe') || (type === 'unsubscribed') || (type === 'subscribe') || (type === 'subscribed')) { presence = $pres({'type':type}); } else { if (type === 'online') { presence = $pres(); } else { presence = $pres().c('show').t(type).up(); } if (status_message) { presence.c('status').t(status_message); } } converse.connection.send(presence); }, setStatus: function (value) { this.sendPresence(value); this.save({'status': value}); }, setStatusMessage: function (status_message) { converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message)); this.save({'status_message': status_message}); if (this.xhr_custom_status) { $.ajax({ url: this.xhr_custom_status_url, type: 'POST', data: {'msg': status_message} }); } } }); this.XMPPStatusView = Backbone.View.extend({ el: "span#xmpp-status-holder", events: { "click a.choose-xmpp-status": "toggleOptions", "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm", "submit #set-custom-xmpp-status": "setStatusMessage", "click .dropdown dd ul li a": "setStatus" }, toggleOptions: function (ev) { ev.preventDefault(); $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast'); }, renderStatusChangeForm: function (ev) { ev.preventDefault(); var status_message = this.model.get('status') || 'offline'; var input = converse.templates.change_status_message({ 'status_message': status_message, 'label_custom_status': __('Custom status'), 'label_save': __('Save') }); this.$el.find('.xmpp-status').replaceWith(input); this.$el.find('.custom-xmpp-status').focus().focus(); }, setStatusMessage: function (ev) { ev.preventDefault(); var status_message = $(ev.target).find('input').val(); if (status_message === "") { } this.model.setStatusMessage(status_message); }, setStatus: function (ev) { ev.preventDefault(); var $el = $(ev.target), value = $el.attr('data-value'); this.model.setStatus(value); this.$el.find(".dropdown dd ul").hide(); }, getPrettyStatus: function (stat) { var pretty_status; if (stat === 'chat') { pretty_status = __('online'); } else if (stat === 'dnd') { pretty_status = __('busy'); } else if (stat === 'xa') { pretty_status = __('away for long'); } else if (stat === 'away') { pretty_status = __('away'); } else { pretty_status = __(stat) || __('online'); } return pretty_status; }, updateStatusUI: function (model) { if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) { return; } var stat = model.get('status'); // # For translators: the %1$s part gets replaced with the status // # Example, I am online var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat)); this.$el.find('#fancy-xmpp-status-select').html( converse.templates.chat_status({ 'chat_status': stat, 'status_message': status_message, 'desc_custom_status': __('Click here to write a custom status message'), 'desc_change_status': __('Click to change your chat status') })); }, initialize: function () { this.model.on("change", this.updateStatusUI, this); }, render: function () { // Replace the default dropdown with something nicer var $select = this.$el.find('select#select-xmpp-status'), chat_status = this.model.get('status') || 'offline', options = $('option', $select), $options_target, options_list = [], that = this; this.$el.html(converse.templates.choose_status()); this.$el.find('#fancy-xmpp-status-select') .html(converse.templates.chat_status({ 'status_message': this.model.get('status_message') || __("I am %1$s", this.getPrettyStatus(chat_status)), 'chat_status': chat_status, 'desc_custom_status': __('Click here to write a custom status message'), 'desc_change_status': __('Click to change your chat status') })); // iterate through all the