瀏覽代碼

Merge branch 'master' into gh-pages

Conflicts:
	index.html
JC Brand 12 年之前
父節點
當前提交
4caabf5013
共有 6 個文件被更改,包括 341 次插入52 次删除
  1. 232 0
      Libraries/strophe.disco.js
  2. 0 1
      Libraries/strophe.js
  3. 13 5
      converse.css
  4. 88 36
      converse.js
  5. 7 10
      index.html
  6. 1 0
      main.js

+ 232 - 0
Libraries/strophe.disco.js

@@ -0,0 +1,232 @@
+/*
+  Copyright 2010, François de Metz <francois@2metz.fr>
+*/
+
+/**
+ * Disco Strophe Plugin
+ * Implement http://xmpp.org/extensions/xep-0030.html
+ * TODO: manage node hierarchies, and node on info request
+ */
+Strophe.addConnectionPlugin('disco',
+{
+    _connection: null,
+    _identities : [],
+    _features : [],
+    _items : [],
+    /** Function: init
+     * Plugin init
+     *
+     * Parameters:
+     *   (Strophe.Connection) conn - Strophe connection
+     */
+    init: function(conn)
+    {
+    this._connection = conn;
+        this._identities = [];
+        this._features   = [];
+        this._items      = [];
+        // disco info
+        conn.addHandler(this._onDiscoInfo.bind(this), Strophe.NS.DISCO_INFO, 'iq', 'get', null, null);
+        // disco items
+        conn.addHandler(this._onDiscoItems.bind(this), Strophe.NS.DISCO_ITEMS, 'iq', 'get', null, null);
+    },
+    /** Function: addIdentity
+     * See http://xmpp.org/registrar/disco-categories.html
+     * Parameters:
+     *   (String) category - category of identity (like client, automation, etc ...)
+     *   (String) type - type of identity (like pc, web, bot , etc ...)
+     *   (String) name - name of identity in natural language
+     *   (String) lang - lang of name parameter
+     *
+     * Returns:
+     *   Boolean
+     */
+    addIdentity: function(category, type, name, lang)
+    {
+        for (var i=0; i<this._identities.length; i++)
+        {
+            if (this._identities[i].category == category &&
+                this._identities[i].type == type &&
+                this._identities[i].name == name &&
+                this._identities[i].lang == lang)
+            {
+                return false;
+            }
+        }
+        this._identities.push({category: category, type: type, name: name, lang: lang});
+        return true;
+    },
+    /** Function: addFeature
+     *
+     * Parameters:
+     *   (String) var_name - feature name (like jabber:iq:version)
+     *
+     * Returns:
+     *   boolean
+     */
+    addFeature: function(var_name)
+    {
+        for (var i=0; i<this._features.length; i++)
+        {
+             if (this._features[i] == var_name)
+                 return false;
+        }
+        this._features.push(var_name);
+        return true;
+    },
+    /** Function: removeFeature
+     *
+     * Parameters:
+     *   (String) var_name - feature name (like jabber:iq:version)
+     *
+     * Returns:
+     *   boolean
+     */
+    removeFeature: function(var_name)
+    {
+        for (var i=0; i<this._features.length; i++)
+        {
+             if (this._features[i] === var_name){
+                 this._features.splice(i,1)
+                 return true;
+             }
+        }
+        return false;
+    },
+    /** Function: addItem
+     *
+     * Parameters:
+     *   (String) jid
+     *   (String) name
+     *   (String) node
+     *   (Function) call_back
+     *
+     * Returns:
+     *   boolean
+     */
+    addItem: function(jid, name, node, call_back)
+    {
+        if (node && !call_back)
+            return false;
+        this._items.push({jid: jid, name: name, node: node, call_back: call_back});
+        return true;
+    },
+    /** Function: info
+     * Info query
+     *
+     * Parameters:
+     *   (Function) call_back
+     *   (String) jid
+     *   (String) node
+     */
+    info: function(jid, node, success, error, timeout)
+    {
+        var attrs = {xmlns: Strophe.NS.DISCO_INFO};
+        if (node)
+            attrs.node = node;
+
+        var info = $iq({from:this._connection.jid,
+                         to:jid, type:'get'}).c('query', attrs);
+        this._connection.sendIQ(info, success, error, timeout);
+    },
+    /** Function: items
+     * Items query
+     *
+     * Parameters:
+     *   (Function) call_back
+     *   (String) jid
+     *   (String) node
+     */
+    items: function(jid, node, success, error, timeout)
+    {
+        var attrs = {xmlns: Strophe.NS.DISCO_ITEMS};
+        if (node)
+            attrs.node = node;
+
+        var items = $iq({from:this._connection.jid,
+                         to:jid, type:'get'}).c('query', attrs);
+        this._connection.sendIQ(items, success, error, timeout);
+    },
+
+    /** PrivateFunction: _buildIQResult
+     */
+    _buildIQResult: function(stanza, query_attrs)
+    {
+        var id   =  stanza.getAttribute('id');
+        var from = stanza.getAttribute('from');
+        var iqresult = $iq({type: 'result', id: id});
+
+        if (from !== null) {
+            iqresult.attrs({to: from});
+        }
+
+        return iqresult.c('query', query_attrs);
+    },
+
+    /** PrivateFunction: _onDiscoInfo
+     * Called when receive info request
+     */
+    _onDiscoInfo: function(stanza)
+    {
+        var node = stanza.getElementsByTagName('query')[0].getAttribute('node');
+        var attrs = {xmlns: Strophe.NS.DISCO_INFO};
+        if (node)
+        {
+            attrs.node = node;
+        }
+        var iqresult = this._buildIQResult(stanza, attrs);
+        for (var i=0; i<this._identities.length; i++)
+        {
+            var attrs = {category: this._identities[i].category,
+                         type    : this._identities[i].type};
+            if (this._identities[i].name)
+                attrs.name = this._identities[i].name;
+            if (this._identities[i].lang)
+                attrs['xml:lang'] = this._identities[i].lang;
+            iqresult.c('identity', attrs).up();
+        }
+        for (var i=0; i<this._features.length; i++)
+        {
+            iqresult.c('feature', {'var':this._features[i]}).up();
+        }
+        this._connection.send(iqresult.tree());
+        return true;
+    },
+    /** PrivateFunction: _onDiscoItems
+     * Called when receive items request
+     */
+    _onDiscoItems: function(stanza)
+    {
+        var query_attrs = {xmlns: Strophe.NS.DISCO_ITEMS};
+        var node = stanza.getElementsByTagName('query')[0].getAttribute('node');
+        if (node)
+        {
+            query_attrs.node = node;
+            var items = [];
+            for (var i = 0; i < this._items.length; i++)
+            {
+                if (this._items[i].node == node)
+                {
+                    items = this._items[i].call_back(stanza);
+                    break;
+                }
+            }
+        }
+        else
+        {
+            var items = this._items;
+        }
+        var iqresult = this._buildIQResult(stanza, query_attrs);
+        for (var i = 0; i < items.length; i++)
+        {
+            var attrs = {jid:  items[i].jid};
+            if (items[i].name)
+                attrs.name = items[i].name;
+            if (items[i].node)
+                attrs.node = items[i].node;
+            iqresult.c('item', attrs).up();
+        }
+        this._connection.send(iqresult.tree());
+        return true;
+    }
+});

+ 0 - 1
Libraries/strophe.js

@@ -1917,7 +1917,6 @@ Strophe.Handler.prototype = {
         try {
         try {
             result = this.handler(elem);
             result = this.handler(elem);
         } catch (e) {
         } catch (e) {
-            result = this.handler(elem);
             if (e.sourceURL) {
             if (e.sourceURL) {
                 Strophe.fatal("error: " + this.handler +
                 Strophe.fatal("error: " + this.handler +
                               " " + e.sourceURL + ":" +
                               " " + e.sourceURL + ":" +

+ 13 - 5
converse.css

@@ -415,6 +415,19 @@ form.search-xmpp-contact input {
     margin-top: 0.5em;
     margin-top: 0.5em;
 }
 }
 
 
+#available-chatrooms {
+    height: 225px;
+    overflow-y: scroll;
+}
+
+#available-chatrooms dd {
+    overflow-x: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    display: inline-block;
+    width: 160px;
+}
+
 #available-chatrooms dt,
 #available-chatrooms dt,
 #converse-roster dt {
 #converse-roster dt {
     font-weight: normal;
     font-weight: normal;
@@ -430,11 +443,6 @@ form.search-xmpp-contact input {
     padding-top: 1em;
     padding-top: 1em;
 }
 }
 
 
-#available-chatrooms li {
-    display: block;
-}
-
-
 dd.available-chatroom,
 dd.available-chatroom,
 #converse-roster dd {
 #converse-roster dd {
     font-weight: bold;
     font-weight: bold;

+ 88 - 36
converse.js

@@ -1,8 +1,8 @@
 /*!
 /*!
- * Converse.js (XMPP-based instant messaging with Strophe.js and backbone.js)
+ * Converse.js (Web-based XMPP instant messaging client)
  * http://conversejs.org
  * http://conversejs.org
  *
  *
- * Copyright (c) 2012 Jan-Carel Brand (jc@opkode.com)
+ * Copyright (c) 2012, Jan-Carel Brand <jc@opkode.com>
  * Dual licensed under the MIT and GPL Licenses
  * Dual licensed under the MIT and GPL Licenses
  */
  */
 
 
@@ -22,7 +22,8 @@
                 "strophe": "Libraries/strophe",
                 "strophe": "Libraries/strophe",
                 "strophe.muc": "Libraries/strophe.muc",
                 "strophe.muc": "Libraries/strophe.muc",
                 "strophe.roster": "Libraries/strophe.roster",
                 "strophe.roster": "Libraries/strophe.roster",
-                "strophe.vcard": "Libraries/strophe.vcard"
+                "strophe.vcard": "Libraries/strophe.vcard",
+                "strophe.disco": "Libraries/strophe.disco"
             },
             },
 
 
             // define module dependencies for modules not using define
             // define module dependencies for modules not using define
@@ -38,22 +39,11 @@
                     //module value.
                     //module value.
                     exports: 'Backbone'
                     exports: 'Backbone'
                 },
                 },
-
-                'underscore': {
-                    exports: '_'
-                },
-
-                'strophe.muc': {
-                    deps: ['strophe', 'jquery']
-                },
-
-                'strophe.roster': {
-                    deps: ['strophe', 'jquery']
-                },
-
-                'strophe.vcard': {
-                    deps: ['strophe', 'jquery']
-                }
+                'underscore':   { exports: '_' },
+                'strophe.muc':  { deps: ['strophe', 'jquery'] },
+                'strophe.roster':   { deps: ['strophe', 'jquery'] },
+                'strophe.vcard':    { deps: ['strophe', 'jquery'] },
+                'strophe.disco':    { deps: ['strophe', 'jquery'] }
             }
             }
         });
         });
 
 
@@ -63,7 +53,8 @@
             "sjcl",
             "sjcl",
             "strophe.muc",
             "strophe.muc",
             "strophe.roster",
             "strophe.roster",
-            "strophe.vcard"
+            "strophe.vcard",
+            "strophe.disco"
             ], function() {
             ], function() {
                 // Use Mustache style syntax for variable interpolation
                 // Use Mustache style syntax for variable interpolation
                 _.templateSettings = {
                 _.templateSettings = {
@@ -82,7 +73,6 @@
         root.converse = factory(jQuery, _, console || {log: function(){}});
         root.converse = factory(jQuery, _, console || {log: function(){}});
     }
     }
 }(this, function ($, _, console) {
 }(this, function ($, _, console) {
-
     var converse = {};
     var converse = {};
     converse.msg_counter = 0;
     converse.msg_counter = 0;
 
 
@@ -728,11 +718,10 @@
             this.on('update-rooms-list', function (ev) {
             this.on('update-rooms-list', function (ev) {
                 this.updateRoomsList();
                 this.updateRoomsList();
             });
             });
-            this.trigger('update-rooms-list');
         },
         },
 
 
         updateRoomsList: function () {
         updateRoomsList: function () {
-            converse.connection.muc.listRooms(converse.muc_domain, $.proxy(function (iq) {
+            converse.connection.muc.listRooms(this.muc_domain, $.proxy(function (iq) {
                 var name, jid, i,
                 var name, jid, i,
                     rooms = $(iq).find('query').find('item'),
                     rooms = $(iq).find('query').find('item'),
                     rooms_length = rooms.length,
                     rooms_length = rooms.length,
@@ -744,7 +733,7 @@
                     $available_chatrooms.find('dt').hide();
                     $available_chatrooms.find('dt').hide();
                 }
                 }
                 for (i=0; i<rooms_length; i++) {
                 for (i=0; i<rooms_length; i++) {
-                    name = Strophe.unescapeNode($(rooms[i]).attr('name'));
+                    name = Strophe.unescapeNode($(rooms[i]).attr('name')||$(rooms[i]).attr('jid'));
                     jid = $(rooms[i]).attr('jid');
                     jid = $(rooms[i]).attr('jid');
                     $available_chatrooms.append(this.room_template({'name':name, 'jid':jid}));
                     $available_chatrooms.append(this.room_template({'name':name, 'jid':jid}));
                 }
                 }
@@ -762,7 +751,7 @@
                 name = input.val().trim().toLowerCase();
                 name = input.val().trim().toLowerCase();
                 input.val(''); // Clear the input
                 input.val(''); // Clear the input
                 if (name) {
                 if (name) {
-                    jid = Strophe.escapeNode(name) + '@' + converse.muc_domain;
+                    jid = Strophe.escapeNode(name) + '@' + this.muc_domain;
                 } else {
                 } else {
                     return;
                     return;
                 }
                 }
@@ -790,8 +779,16 @@
         initialize: function () {
         initialize: function () {
             this.$el.appendTo(converse.chatboxesview.$el);
             this.$el.appendTo(converse.chatboxesview.$el);
             this.model.on('change', $.proxy(function (item, changed) {
             this.model.on('change', $.proxy(function (item, changed) {
+                var i;
                 if (_.has(item.changed, 'connected')) {
                 if (_.has(item.changed, 'connected')) {
                     this.render();
                     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);
+                    }
                 }
                 }
                 if (_.has(item.changed, 'visible')) {
                 if (_.has(item.changed, 'visible')) {
                     if (item.changed.visible === true) {
                     if (item.changed.visible === true) {
@@ -799,7 +796,6 @@
                     }
                     }
                 }
                 }
             }, this));
             }, this));
-
             this.model.on('show', this.show, this);
             this.model.on('show', this.show, this);
             this.model.on('destroy', this.hide, this);
             this.model.on('destroy', this.hide, this);
             this.model.on('hide', this.hide, this);
             this.model.on('hide', this.hide, this);
@@ -808,6 +804,17 @@
             }
             }
         },
         },
 
 
+        featureAdded: function (feature) {
+            if (feature.get('var') == 'http://jabber.org/protocol/muc') {
+                if (!this.roomspanel) {
+                    this.roomspanel = new converse.RoomsPanel();
+                    this.roomspanel.muc_domain = feature.get('from');
+                    this.roomspanel.$parent = this.$el;
+                    this.roomspanel.render().trigger('update-rooms-list');
+                }
+            }
+        },
+
         template: _.template(
         template: _.template(
             '<div class="chat-head oc-chat-head">'+
             '<div class="chat-head oc-chat-head">'+
                 '<ul id="controlbox-tabs"></ul>'+
                 '<ul id="controlbox-tabs"></ul>'+
@@ -847,10 +854,6 @@
                 this.contactspanel = new converse.ContactsPanel();
                 this.contactspanel = new converse.ContactsPanel();
                 this.contactspanel.$parent = this.$el;
                 this.contactspanel.$parent = this.$el;
                 this.contactspanel.render();
                 this.contactspanel.render();
-                // TODO: Only add the rooms panel if the server supports MUC
-                this.roomspanel = new converse.RoomsPanel();
-                this.roomspanel.$parent = this.$el;
-                this.roomspanel.render();
             }
             }
             return this;
             return this;
         }
         }
@@ -1842,8 +1845,7 @@
                             }));
                             }));
             // iterate through all the <option> elements and add option values
             // iterate through all the <option> elements and add option values
             options.each(function(){
             options.each(function(){
-                options_list.push(that.option_template({
-                                                        'value': $(this).val(),
+                options_list.push(that.option_template({'value': $(this).val(),
                                                         'text': $(this).text()
                                                         'text': $(this).text()
                                                         }));
                                                         }));
             });
             });
@@ -1854,6 +1856,57 @@
         }
         }
     });
     });
 
 
+    converse.Feature = Backbone.Model.extend();
+    converse.Features = Backbone.Collection.extend({
+        /* This collection stores Feature Models, representing features
+         * provided by available XMPP entities (e.g. servers)
+         *
+         * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
+         */
+        model: converse.Feature,
+        initialize: function () {
+            this.localStorage = new Backbone.LocalStorage(
+                hex_sha1('converse.features'+converse.bare_jid));
+            if (this.localStorage.records.length === 0) {
+                // localStorage is empty, so we've likely never queried this
+                // domain for features yet
+                converse.connection.disco.info(converse.domain, null, this.onInfo, this.onError);
+                converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this), $.proxy(this.onError, this));
+            } else {
+                this.fetch({add:true});
+            }
+        },
+
+        onItems: function (stanza) {
+            $(stanza).find('query item').each($.proxy(function (idx, item) {
+                converse.connection.disco.info(
+                    $(item).attr('jid'),
+                    null,
+                    $.proxy(this.onInfo, this),
+                    $.proxy(this.onError, this));
+            }, this));
+        },
+
+        onInfo: function (stanza) {
+            var $stanza = $(stanza);
+            if (($stanza.find('identity[category=server][type=im]').length === 0) &&
+                ($stanza.find('identity[category=conference][type=text]').length === 0)) {
+                // This isn't an IM server component
+                return;
+            }
+            $stanza.find('feature').each($.proxy(function (idx, feature) {
+                this.create({
+                    'var': $(feature).attr('var'),
+                    'from': $stanza.attr('from')
+                });
+            }, this));
+        },
+
+        onError: function (stanza) {
+            console.log("Error while doing service discovery");
+        }
+    });
+
     converse.LoginPanel = Backbone.View.extend({
     converse.LoginPanel = Backbone.View.extend({
         tagName: 'div',
         tagName: 'div',
         id: "login-dialog",
         id: "login-dialog",
@@ -1868,7 +1921,7 @@
             '<input type="text" id="jid">' +
             '<input type="text" id="jid">' +
             '<label>Password:</label>' +
             '<label>Password:</label>' +
             '<input type="password" id="password">' +
             '<input type="password" id="password">' +
-            '<input type="submit" name="submit"/>' +
+            '<button type="submit">Log In</button>' +
             '</form">'),
             '</form">'),
 
 
         bosh_url_input: _.template(
         bosh_url_input: _.template(
@@ -1946,6 +1999,7 @@
                 template.find('form').append(this.bosh_url_input);
                 template.find('form').append(this.bosh_url_input);
             }
             }
             this.$parent.find('#controlbox-panes').append(this.$el.html(template));
             this.$parent.find('#controlbox-panes').append(this.$el.html(template));
+            this.$el.find('input#jid').focus();
             return this;
             return this;
         }
         }
     });
     });
@@ -1985,13 +2039,11 @@
 
 
     converse.onConnected = function (connection) {
     converse.onConnected = function (connection) {
         this.connection = connection;
         this.connection = connection;
-
-        this.animate = true; // Use animations
         this.connection.xmlInput = function (body) { console.log(body); };
         this.connection.xmlInput = function (body) { console.log(body); };
         this.connection.xmlOutput = function (body) { console.log(body); };
         this.connection.xmlOutput = function (body) { console.log(body); };
         this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
         this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
         this.domain = Strophe.getDomainFromJid(this.connection.jid);
         this.domain = Strophe.getDomainFromJid(this.connection.jid);
-        this.muc_domain = 'conference.' +  this.domain;
+        this.features = new this.Features();
 
 
         // Set up the roster
         // Set up the roster
         this.roster = new this.RosterItems();
         this.roster = new this.RosterItems();

+ 7 - 10
index.html

@@ -43,18 +43,19 @@
 
 
     <h2>Features</h2>
     <h2>Features</h2>
     <ul>
     <ul>
-        <li>Single and multi-user chat</li>
+        <li>Single-user chat</li>
+        <li>Multi-user chat in chatrooms (<a href="http://xmpp.org/extensions/xep-0045.html">XEP 45</a>)</li>
+        <li>vCard support (<a href="http://xmpp.org/extensions/xep-0054.html">XEP 54</a>)</li>
+        <li>Service discovery (<a href="http://xmpp.org/extensions/xep-0030.html">XEP 30</a>)</li>
         <li>Contact rosters</li>
         <li>Contact rosters</li>
         <li>Manually or automically subscribe to other contacts</li>
         <li>Manually or automically subscribe to other contacts</li>
-        <li>Roster item exchange (<a href="http://xmpp.org/extensions/tmp/xep-0144-1.1.html">XEP 144</a>)</li>
         <li>Accept or decline contact requests</li>
         <li>Accept or decline contact requests</li>
+        <li>Roster item exchange (<a href="http://xmpp.org/extensions/tmp/xep-0144-1.1.html">XEP 144</a>)</li>
         <li>Chat statuses (online, busy, away, offline)</li>
         <li>Chat statuses (online, busy, away, offline)</li>
         <li>Custom status messages</li>
         <li>Custom status messages</li>
         <li>Typing notifications</li>
         <li>Typing notifications</li>
         <li>Third person messages (/me )</li>
         <li>Third person messages (/me )</li>
-        <li>Multi-user chat in chatrooms (<a href="http://xmpp.org/extensions/xep-0045.html">XEP 45</a>)</li>
         <li>Chatroom Topics</li>
         <li>Chatroom Topics</li>
-        <li>vCard support (<a href="http://xmpp.org/extensions/xep-0054.html">XEP 54</a>)</li>
     </ul>
     </ul>
 
 
     <h2>CMS Integration</h2>
     <h2>CMS Integration</h2>
@@ -76,8 +77,7 @@
 
 
     <h2>Demo</h2>
     <h2>Demo</h2>
     <p><a href="#" class="chat toggle-online-users">Click this link</a> or click the link on the bottom right corner of this page.</a></p>
     <p><a href="#" class="chat toggle-online-users">Click this link</a> or click the link on the bottom right corner of this page.</a></p>
-    <p>
-        You can log in with any existing federated Jabber/XMPP account, or create a new one at any of these providers:
+    <p>You can log in with any existing federated Jabber/XMPP account, or create a new one at any of these providers:
         <ul>
         <ul>
             <li><a href="http://jabber.org" target="_blank">jabber.org</a></li>
             <li><a href="http://jabber.org" target="_blank">jabber.org</a></li>
             <li><a href="https://jappix.com" target="_blank">jappix.com</a></li>
             <li><a href="https://jappix.com" target="_blank">jappix.com</a></li>
@@ -98,10 +98,7 @@
         establish an authenticated connection on the server side and then attach to
         establish an authenticated connection on the server side and then attach to
         this connection in your browser.
         this connection in your browser.
    </p>
    </p>
-    <p><strong>Converse.js</strong> already supports this usecase, but you'll have to
-    do more manual work yourself.
-    </p>
-
+    <p><strong>Converse.js</strong> already supports this usecase, but you'll have to do more manual work yourself.</p>
 
 
     <h2>Tests</h2>
     <h2>Tests</h2>
    </p>
    </p>

+ 1 - 0
main.js

@@ -1,5 +1,6 @@
 require(["jquery", "converse"], function($, converse) {
 require(["jquery", "converse"], function($, converse) {
     converse.initialize({
     converse.initialize({
+        animate: true,
         bosh_service_url: 'https://bind.opkode.im',
         bosh_service_url: 'https://bind.opkode.im',
         prebind: false,
         prebind: false,
         xhr_user_search: false,
         xhr_user_search: false,