瀏覽代碼

Initial work on adding chatroom bookmarks.

JC Brand 9 年之前
父節點
當前提交
052dd19252

+ 1 - 0
README.md

@@ -30,6 +30,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
 -   In-band registration [XEP 77](http://xmpp.org/extensions/xep-0077.html)
 -   In-band registration [XEP 77](http://xmpp.org/extensions/xep-0077.html)
 -   Contact rosters and groups
 -   Contact rosters and groups
 -   Contact subscriptions
 -   Contact subscriptions
+-   Chat room bookmarks [XEP 48](http://xmpp.org/extensions/xep-0048.html)
 -   Roster item exchange [XEP 144](http://xmpp.org/extensions/tmp/xep-0144-1.1.html)
 -   Roster item exchange [XEP 144](http://xmpp.org/extensions/tmp/xep-0144-1.1.html)
 -   Chat statuses (online, busy, away, offline)
 -   Chat statuses (online, busy, away, offline)
 -   Custom status messages
 -   Custom status messages

+ 2 - 0
config.js

@@ -47,6 +47,7 @@ require.config({
         
         
         // Converse
         // Converse
         "converse-api":             "src/converse-api",
         "converse-api":             "src/converse-api",
+        "converse-bookmarks":       "src/converse-bookmarks",
         "converse-chatview":        "src/converse-chatview",
         "converse-chatview":        "src/converse-chatview",
         "converse-controlbox":      "src/converse-controlbox",
         "converse-controlbox":      "src/converse-controlbox",
         "converse-core":            "src/converse-core",
         "converse-core":            "src/converse-core",
@@ -129,6 +130,7 @@ require.config({
         "chatbox":                  "src/templates/chatbox",
         "chatbox":                  "src/templates/chatbox",
         "chatroom":                 "src/templates/chatroom",
         "chatroom":                 "src/templates/chatroom",
         "chatroom_form":            "src/templates/chatroom_form",
         "chatroom_form":            "src/templates/chatroom_form",
+        "chatroom_bookmark_form":   "src/templates/chatroom_bookmark_form",
         "chatroom_password_form":   "src/templates/chatroom_password_form",
         "chatroom_password_form":   "src/templates/chatroom_password_form",
         "chatroom_nickname_form":   "src/templates/chatroom_nickname_form",
         "chatroom_nickname_form":   "src/templates/chatroom_nickname_form",
         "chatroom_sidebar":         "src/templates/chatroom_sidebar",
         "chatroom_sidebar":         "src/templates/chatroom_sidebar",

+ 1 - 0
converse.js

@@ -21,6 +21,7 @@ if (typeof define !== 'undefined') {
 
 
         "converse-chatview",    // Renders standalone chat boxes for single user chat
         "converse-chatview",    // Renders standalone chat boxes for single user chat
         "converse-controlbox",  // The control box
         "converse-controlbox",  // The control box
+        "converse-bookmarks",   // XEP-0048 Bookmarks
         "converse-mam",         // XEP-0313 Message Archive Management
         "converse-mam",         // XEP-0313 Message Archive Management
         "converse-muc",         // XEP-0045 Multi-user chat
         "converse-muc",         // XEP-0045 Multi-user chat
         "converse-vcard",       // XEP-0054 VCard-temp
         "converse-vcard",       // XEP-0054 VCard-temp

+ 11 - 1
css/converse.css

@@ -1265,14 +1265,18 @@
   cursor: pointer;
   cursor: pointer;
   display: inline-block;
   display: inline-block;
   float: right;
   float: right;
-  font-size: 9px;
+  font-size: 10px;
   margin: 0;
   margin: 0;
+  margin-left: 0.1em;
   margin-right: 0.2em;
   margin-right: 0.2em;
   padding: 0.5em 0.5em 0.3em 0.5em;
   padding: 0.5em 0.5em 0.3em 0.5em;
   text-decoration: none; }
   text-decoration: none; }
   #conversejs .chatbox-btn:active {
   #conversejs .chatbox-btn:active {
     position: relative;
     position: relative;
     top: 1px; }
     top: 1px; }
+  #conversejs .chatbox-btn.button-on {
+    background-color: white;
+    color: #F4A261; }
 #conversejs .chatbox {
 #conversejs .chatbox {
   display: block;
   display: block;
   float: right;
   float: right;
@@ -1963,6 +1967,9 @@
   margin: 0.3em 0; }
   margin: 0.3em 0; }
 #conversejs .chat-head-chatroom {
 #conversejs .chat-head-chatroom {
   background-color: #E76F51; }
   background-color: #E76F51; }
+  #conversejs .chat-head-chatroom .chatbox-btn.button-on {
+    background-color: white;
+    color: #E76F51; }
   #conversejs .chat-head-chatroom .chatroom-topic {
   #conversejs .chat-head-chatroom .chatroom-topic {
     color: white;
     color: white;
     font-size: 80%;
     font-size: 80%;
@@ -2070,6 +2077,9 @@
         #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container .validation-message {
         #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container .validation-message {
           font-size: 90%;
           font-size: 90%;
           color: #D24E2B; }
           color: #D24E2B; }
+        #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container .chatroom-form label,
+        #conversejs .chatroom .box-flyout .chatroom-body .chatroom-form-container .chatroom-form input[type=text] {
+          display: block; }
   #conversejs .chatroom .chat-textarea {
   #conversejs .chatroom .chat-textarea {
     border-bottom-right-radius: 0; }
     border-bottom-right-radius: 0; }
   #conversejs .chatroom .room-invite {
   #conversejs .chatroom .room-invite {

+ 1 - 0
docs/CHANGES.md

@@ -7,6 +7,7 @@
 - New event ['rosterGroupsFetched'](https://conversejs.org/docs/html/development.html#rosterGroupsFetched) [jcbrand]
 - New event ['rosterGroupsFetched'](https://conversejs.org/docs/html/development.html#rosterGroupsFetched) [jcbrand]
 - HTML templates are now loaded in the respective modules/plugins. [jcbrand]
 - HTML templates are now loaded in the respective modules/plugins. [jcbrand]
 - Start improving Content-Security-Policy compatibility by removing inline CSS. [mathiasertl]
 - Start improving Content-Security-Policy compatibility by removing inline CSS. [mathiasertl]
+- Add support for XEP-0048, chat room bookmarks [jcbrand]
 
 
 ## 2.0.0 (2016-09-16)
 ## 2.0.0 (2016-09-16)
 - #656 Online users count not shown initially [amanzur]
 - #656 Online users count not shown initially [amanzur]

+ 5 - 0
sass/_chatbox.scss

@@ -64,6 +64,7 @@
         float: right;
         float: right;
         font-size: $box-close-font-size;
         font-size: $box-close-font-size;
         margin: 0;
         margin: 0;
+        margin-left: 0.1em;
         margin-right: 0.2em;
         margin-right: 0.2em;
         padding: 0.5em 0.5em 0.3em 0.5em;
         padding: 0.5em 0.5em 0.3em 0.5em;
         text-decoration: none;
         text-decoration: none;
@@ -71,6 +72,10 @@
             position: relative;
             position: relative;
             top: 1px;
             top: 1px;
         }
         }
+        &.button-on {
+            background-color: white;
+            color: $chat-head-color;
+        }
     }
     }
     .chatbox {
     .chatbox {
         display: block;
         display: block;

+ 12 - 0
sass/_chatrooms.scss

@@ -8,6 +8,12 @@
 
 
     .chat-head-chatroom {
     .chat-head-chatroom {
         background-color: $chatroom-head-color;
         background-color: $chatroom-head-color;
+        .chatbox-btn {
+            &.button-on {
+                background-color: $chat-head-text-color;
+                color: $chatroom-head-color;
+            }
+        }
 
 
         .chatroom-topic {
         .chatroom-topic {
             color: white;
             color: white;
@@ -134,6 +140,12 @@
                         font-size: 90%;
                         font-size: 90%;
                         color: $error-color;
                         color: $error-color;
                     }
                     }
+                    .chatroom-form {
+                        label,
+                        input[type=text] {
+                            display: block;
+                        }
+                    }
                 }
                 }
             }
             }
         }
         }

+ 1 - 1
sass/_variables.scss

@@ -75,4 +75,4 @@ $box-close-button-padding-top: 4px !default;
 $box-close-button-padding-bottom: 4px !default;
 $box-close-button-padding-bottom: 4px !default;
 $box-close-button-padding-left: 4px !default;
 $box-close-button-padding-left: 4px !default;
 $box-close-button-padding-right: 4px !default;
 $box-close-button-padding-right: 4px !default;
-$box-close-font-size: 9px !default;
+$box-close-font-size: 10px !default;

+ 257 - 0
spec/bookmarks.js

@@ -0,0 +1,257 @@
+/*global converse */
+(function (root, factory) {
+    define([
+        "jquery",
+        "utils",
+        "mock",
+        "test_utils"
+        ], factory);
+} (this, function ($, utils, mock, test_utils) {
+    "use strict";
+    var $iq = converse_api.env.$iq,
+        Strophe = converse_api.env.Strophe;
+
+    describe("A chat room", function () {
+
+        it("can be bookmarked", function () {
+            var sent_stanza, IQ_id;
+            var sendIQ = converse.connection.sendIQ;
+            spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
+                sent_stanza = iq;
+                IQ_id = sendIQ.bind(this)(iq, callback, errback);
+            });
+            spyOn(converse.connection, 'getUniqueId').andCallThrough();
+
+            test_utils.openChatRoom('theplay', 'conference.shakespeare.lit', 'JC');
+            var jid = 'theplay@conference.shakespeare.lit';
+            var view = converse.chatboxviews.get(jid);
+            spyOn(view, 'renderBookmarkForm').andCallThrough();
+            spyOn(view, 'cancelConfiguration').andCallThrough();
+            spyOn(view, 'onBookmarkAdded').andCallThrough();
+            spyOn(view, 'onBookmarkError').andCallThrough();
+
+            var $bookmark = view.$el.find('.icon-pushpin');
+            $bookmark.click();
+            expect(view.renderBookmarkForm).toHaveBeenCalled();
+
+            view.$el.find('.button-cancel').click();
+            expect(view.cancelConfiguration).toHaveBeenCalled();
+            expect($bookmark.hasClass('on-button'), false);
+
+            $bookmark.click();
+            expect(view.renderBookmarkForm).toHaveBeenCalled();
+
+            /* Client uploads data:
+             * --------------------
+             *  <iq from='juliet@capulet.lit/balcony' type='set' id='pip1'>
+             *      <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+             *          <publish node='storage:bookmarks'>
+             *              <item id='current'>
+             *                  <storage xmlns='storage:bookmarks'>
+             *                      <conference name='The Play&apos;s the Thing'
+             *                                  autojoin='true'
+             *                                  jid='theplay@conference.shakespeare.lit'>
+             *                          <nick>JC</nick>
+             *                      </conference>
+             *                  </storage>
+             *              </item>
+             *          </publish>
+             *          <publish-options>
+             *              <x xmlns='jabber:x:data' type='submit'>
+             *                  <field var='FORM_TYPE' type='hidden'>
+             *                      <value>http://jabber.org/protocol/pubsub#publish-options</value>
+             *                  </field>
+             *                  <field var='pubsub#persist_items'>
+             *                      <value>true</value>
+             *                  </field>
+             *                  <field var='pubsub#access_model'>
+             *                      <value>whitelist</value>
+             *                  </field>
+             *              </x>
+             *          </publish-options>
+             *      </pubsub>
+             *  </iq>
+             */
+            var $form = view.$el.find('.chatroom-form');
+            $form.find('input[name="name"]').val('Play&apos;s the Thing');
+            $form.find('input[name="autojoin"]').prop('checked', true);
+            $form.find('input[name="nick"]').val('JC');
+            $form.submit();
+            expect($bookmark.hasClass('on-button'), true);
+
+            expect(sent_stanza.toLocaleString()).toBe(
+                "<iq type='set' from='dummy@localhost/resource' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                    "<pubsub xmlns='http://jabber.org/protocol/pubsub'>"+
+                        "<publish node='storage:bookmarks'>"+
+                            "<item id='current'>"+
+                                "<storage xmlns='storage:bookmarks'>"+
+                                    "<conference name='Play&amp;apos;s the Thing' autojoin='true' jid='theplay@conference.shakespeare.lit'>"+
+                                        "<nick>JC</nick>"+
+                                    "</conference>"+
+                                "</storage>"+
+                            "</item>"+
+                        "</publish>"+
+                    "</pubsub>"+
+                    "<publish-options>"+
+                        "<x xmlns='jabber:x:data' type='submit'>"+
+                            "<field var='FORM_TYPE' type='hidden'>"+
+                                "<value>http://jabber.org/protocol/pubsub#publish-options</value>"+
+                            "</field>"+
+                            "<field var='pubsub#persist_items'>"+
+                                "<value>true</value>"+
+                            "</field>"+
+                            "<field var='pubsub#access_model'>"+
+                                "<value>whitelist</value>"+
+                            "</field>"+
+                        "</x>"+
+                    "</publish-options>"+
+                "</iq>"
+            );
+
+            /* Server acknowledges successful storage
+             *
+             * <iq to='juliet@capulet.lit/balcony' type='result' id='pip1'/>
+             */
+            var stanza = $iq({
+                'to':converse.connection.jid,
+                'type':'result',
+                'id':IQ_id
+            });
+            converse.connection._dataRecv(test_utils.createRequest(stanza));
+            expect(view.onBookmarkAdded).toHaveBeenCalled();
+
+        });
+
+        describe("when bookmarked", function () {
+            it("displays that it's bookmarked through its bookmark icon", function () {
+                // TODO
+                // Mock bookmark data received from the server.
+                // Open the room
+                // Check that the icon has 'button-on' class.
+            });
+
+            it("can be unbookmarked", function () {
+                // TODO
+                // Mock bookmark data received from the server.
+                // Open the room
+                // Click the bookmark icon to unbookmark
+            });
+        });
+    });
+
+    describe("Bookmarks", function () {
+
+        beforeEach(function () {
+            window.sessionStorage.clear();
+        });
+
+        it("can be pushed from the XMPP server", function () {
+            // TODO
+            /* The stored data is automatically pushed to all of the user's
+             * connected resources.
+             *
+             * Publisher receives event notification
+             * -------------------------------------
+             * <message from='juliet@capulet.lit'
+             *         to='juliet@capulet.lit/balcony'
+             *         type='headline'
+             *         id='rnfoo1'>
+             * <event xmlns='http://jabber.org/protocol/pubsub#event'>
+             *     <items node='storage:bookmarks'>
+             *     <item id='current'>
+             *         <storage xmlns='storage:bookmarks'>
+             *         <conference name='The Play&apos;s the Thing'
+             *                     autojoin='true'
+             *                     jid='theplay@conference.shakespeare.lit'>
+             *             <nick>JC</nick>
+             *         </conference>
+             *         </storage>
+             *     </item>
+             *     </items>
+             * </event>
+             * </message>
+
+             * <message from='juliet@capulet.lit'
+             *         to='juliet@capulet.lit/chamber'
+             *         type='headline'
+             *         id='rnfoo2'>
+             * <event xmlns='http://jabber.org/protocol/pubsub#event'>
+             *     <items node='storage:bookmarks'>
+             *     <item id='current'>
+             *         <storage xmlns='storage:bookmarks'>
+             *         <conference name='The Play&apos;s the Thing'
+             *                     autojoin='true'
+             *                     jid='theplay@conference.shakespeare.lit'>
+             *             <nick>JC</nick>
+             *         </conference>
+             *         </storage>
+             *     </item>
+             *     </items>
+             * </event>
+             * </message>
+             */
+        });
+
+        it("can be retrieved from the XMPP server", function () {
+            var sent_stanza, IQ_id,
+                sendIQ = converse.connection.sendIQ;
+            spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
+                sent_stanza = iq;
+                IQ_id = sendIQ.bind(this)(iq, callback, errback);
+            });
+            converse.emit('connected');
+
+            /* Client requests all items
+             * -------------------------
+             *
+             *  <iq from='juliet@capulet.lit/randomID' type='get' id='retrieve1'>
+             *  <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+             *      <items node='storage:bookmarks'/>
+             *  </pubsub>
+             *  </iq>
+             */
+            expect(sent_stanza.toLocaleString()).toBe(
+                "<iq from='dummy@localhost/resource' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                "<pubsub xmlns='http://jabber.org/protocol/pubsub'>"+
+                    "<items node='storage:bookmarks'/>"+
+                "</pubsub>"+
+                "</iq>"
+            );
+
+            /*
+             * Server returns all items
+             * ------------------------
+             * <iq type='result'
+             *     to='juliet@capulet.lit/randomID'
+             *     id='retrieve1'>
+             * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+             *     <items node='storage:bookmarks'>
+             *     <item id='current'>
+             *         <storage xmlns='storage:bookmarks'>
+             *         <conference name='The Play&apos;s the Thing'
+             *                     autojoin='true'
+             *                     jid='theplay@conference.shakespeare.lit'>
+             *             <nick>JC</nick>
+             *         </conference>
+             *         </storage>
+             *     </item>
+             *     </items>
+             * </pubsub>
+             * </iq>
+             */
+            expect(converse.bookmarks.models.length).toBe(0);
+            var stanza = $iq({'to': converse.connection.jid, 'type':'result', 'id':IQ_id})
+                .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                    .c('items', {'node': 'storage:bookmarks'})
+                        .c('item', {'id': 'current'})
+                            .c('storage', {'xmlns': 'storage:bookmarks'})
+                                .c('conference', {
+                                    'name': 'The Play&apos;s the Thing',
+                                    'autojoin': 'true',
+                                    'jid': 'theplay@conference.shakespeare.lit'
+                                }).c('nick').t('JC');
+            converse.connection._dataRecv(test_utils.createRequest(stanza));
+            expect(converse.bookmarks.models.length).toBe(1);
+        });
+    });
+}));

+ 0 - 4
src/converse-api.js

@@ -5,10 +5,6 @@
 // Licensed under the Mozilla Public License (MPLv2)
 // Licensed under the Mozilla Public License (MPLv2)
 //
 //
 /*global define */
 /*global define */
-
-/* This is a Converse.js plugin which add support for multi-user chat rooms, as
- * specified in XEP-0045 Multi-user chat.
- */
 (function (root, factory) {
 (function (root, factory) {
     define("converse-api", [
     define("converse-api", [
             "jquery",
             "jquery",

+ 216 - 0
src/converse-bookmarks.js

@@ -0,0 +1,216 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global Backbone, define */
+
+/* This is a Converse.js plugin which add support for bookmarks specified
+ * in XEP-0048.
+ */
+(function (root, factory) {
+    define("converse-bookmarks", [
+            "jquery",
+            "underscore",
+            "moment_with_locales",
+            "strophe",
+            "utils",
+            "converse-core",
+            "converse-api",
+            "converse-muc",
+            "tpl!chatroom_bookmark_form"
+        ],
+        factory);
+}(this, function ($, _, moment, strophe, utils, converse, converse_api, muc, chatroom_bookmark_form) {
+
+    var __ = utils.__.bind(converse),
+        Strophe = converse_api.env.Strophe,
+        $iq = converse_api.env.$iq,
+        b64_sha1 = converse_api.env.b64_sha1;
+
+    // Add new HTML templates.
+    converse.templates.chatroom_bookmark_form = chatroom_bookmark_form;
+
+    converse_api.plugins.add('converse-bookmarks', {
+        overrides: {
+            // Overrides mentioned here will be picked up by converse.js's
+            // plugin architecture they will replace existing methods on the
+            // relevant objects or classes.
+            //
+            // New functions which don't exist yet can also be added.
+            
+            RoomsPanel: {
+                /* TODO: show bookmarked rooms in the rooms panel */
+            },
+
+            ChatRoomView: {
+                events: {
+                    'click .toggle-bookmark': 'toggleBookmark'
+                },
+
+                render: function (options) {
+                    this._super.render.apply(this, arguments);
+                    var label_bookmark = _('Bookmark this room');
+                    // TODO: check if bookmarked, and if so, add button-on class
+                    this.$el.find('.chat-head-chatroom .icon-wrench').before(
+                        '<a class="chatbox-btn toggle-bookmark icon-pushpin" title="'+label_bookmark+'"></a>');
+                    return this;
+                },
+
+                renderBookmarkForm: function () {
+                    var $body = this.$('.chatroom-body');
+                    $body.children().addClass('hidden');
+                    $body.append(
+                        converse.templates.chatroom_bookmark_form({
+                            heading: __('Bookmark this room'),
+                            label_name: __('The name for this bookmark:'),
+                            label_autojoin: __('Would you like this room to be automatically joined upon startup?'),
+                            label_nick: __('What should your nickname for this room be?'),
+                            default_nick: this.model.get('nick'),
+                            label_submit: __('Save'),
+                            label_cancel: __('Cancel')
+                        }));
+                    this.$('.chatroom-form').submit(this.addBookmark.bind(this));
+                    this.$('.chatroom-form .button-cancel').on('click', this.cancelConfiguration.bind(this));
+                },
+
+                addBookmark: function (ev) {
+                    ev.preventDefault();
+
+                    converse.bookmarks.create({
+                        'id': this.model.get('id'),
+                        'autojoin': this.$el.find('.chatroom-form').find('input[name=autojoin]').val(),
+                        'name':  this.$el.find('.chatroom-form').find('input[name=name]').val(),
+                        'nick':  this.$el.find('.chatroom-form').find('input[name=nick]').val()
+                    });
+                    this.$('.icon-pushpin').addClass('button-on');
+
+                    var that = this,
+                        $form = $(ev.target);
+                    this.sendBookmarkStanza(
+                        $form.find('input[name="name"]').val(),
+                        $form.find('input[name="autojoin"]').prop('checked'),
+                        $form.find('input[name="nick"]').val()
+                    );
+                    this.$el.find('div.chatroom-form-container').hide(
+                        function () {
+                            $(this).remove();
+                            that.$('.chatroom-body').children().removeClass('hidden');
+                        });
+                },
+
+                sendBookmarkStanza: function (name, autojoin, nick) {
+                    name = name || converse.connection.jid;
+                    var stanza = $iq({
+                            'type': 'set',
+                            'from': converse.connection.jid,
+                        })
+                        .c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                            .c('publish', {'node': 'storage:bookmarks'})
+                                .c('item', {'id': 'current'})
+                                    .c('storage', {'xmlns':'storage:bookmarks'})
+                                        .c('conference', {
+                                            'name': name,
+                                            'autojoin': autojoin,
+                                            'jid': this.model.get('jid'), 
+                                        }).c('nick').t(nick).up()
+                                        .up()
+                                    .up()
+                                .up()
+                            .up()
+                        .up()
+                        .c('publish-options')
+                            .c('x', {'xmlns': Strophe.NS.XFORM, 'type':'submit'})
+                                .c('field', {'var':'FORM_TYPE', 'type':'hidden'})
+                                    .c('value').t('http://jabber.org/protocol/pubsub#publish-options').up().up()
+                                .c('field', {'var':'pubsub#persist_items'})
+                                    .c('value').t('true').up().up()
+                                .c('field', {'var':'pubsub#access_model'})
+                                    .c('value').t('whitelist');
+                    converse.connection.sendIQ(stanza, this.onBookmarkAdded, this.onBookmarkError);
+                },
+
+                onBookmarkAdded: function (iq) {
+                    converse.log("Bookmark successfully added");
+                    converse.log(iq);
+                },
+                    
+                onBookmarkError: function (iq) {
+                    converse.log("Error while trying to add bookmark");
+                    converse.log(iq);
+                    window.alert(__("Sorry, something went wrong while trying to save your bookmark."));
+                },
+
+                toggleBookmark: function (ev) {
+                    if (ev) {
+                        ev.preventDefault();
+                        ev.stopPropagation();
+                    }
+                    if (!converse.bookmarks.get(this.model.get('id'))) {
+                        this.renderBookmarkForm();
+                    } else {
+                        converse.bookmarks.remove({
+                            'id': this.model.get('id')
+                        });
+                        this.$('.icon-pushpin').removeClass('button-on');
+                    }
+                }
+            }
+        },
+
+        initialize: function () {
+            /* The initialize function gets called as soon as the plugin is
+             * loaded by converse.js's plugin machinery.
+             */
+            var converse = this.converse;
+            converse.Bookmarks = Backbone.Collection.extend({
+                
+                onCachedBookmarksFetched: function () {
+                    if (!window.sessionStorage.getItem(this.browserStorage.name) || this.models.length > 0) {
+                        // There weren't any cached bookmarks, so we query to XMPP
+                        // server
+                        var stanza = $iq({
+                            'from': converse.connection.jid,
+                            'type': 'get',
+                        }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
+                            .c('items', {'node': 'storage:bookmarks'});
+                        converse.connection.sendIQ(stanza, this.onBookmarksReceived.bind(this), this.onBookmarksReceivedError);
+                    }
+                },
+
+                onBookmarksReceived: function (iq) {
+                    var rooms = $(iq).find('items[node="storage:bookmarks"] item[id="current"] storage conference');
+                    _.each(rooms, function (room) {
+                        this.create({
+                            'jid': room.jid,
+                            'name': room.name,
+                            'autojoin': room.autojoin,
+                            'nick': room.querySelector('nick').text
+                        });
+                    }.bind(this));
+                },
+
+                onBookmarksReceivedError: function (iq) {
+                    converse.log('Error while fetching bookmarks');
+                    converse.log(iq);
+                }
+            });
+
+            converse.initBookmarks = function () {
+                converse.bookmarks = new converse.Bookmarks();
+                var id = b64_sha1('converse.room-bookmarks');
+                converse.bookmarks.id = id;
+                converse.bookmarks.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
+                converse.bookmarks.fetch({
+                    'add': true,
+                    'success': converse.bookmarks.onCachedBookmarksFetched.bind(converse.bookmarks),
+                    'error':  converse.bookmarks.onCachedBookmarksFetched.bind(converse.bookmarks)
+
+                });
+            };
+            converse.on('connected', converse.initBookmarks);
+            converse.on('reconnected', converse.initBookmarks);
+        }
+    });
+}));

+ 1 - 0
src/converse-core.js

@@ -171,6 +171,7 @@
         Strophe.addNamespace('XFORM', 'jabber:x:data');
         Strophe.addNamespace('XFORM', 'jabber:x:data');
         Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
         Strophe.addNamespace('NICK', 'http://jabber.org/protocol/nick');
         Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
         Strophe.addNamespace('HINTS', 'urn:xmpp:hints');
+        Strophe.addNamespace('PUBSUB', 'http://jabber.org/protocol/pubsub');
 
 
         // Instance level constants
         // Instance level constants
         this.TIMEOUTS = { // Set as module attr so that we can override in tests.
         this.TIMEOUTS = { // Set as module attr so that we can override in tests.

+ 5 - 1
src/converse-muc.js

@@ -278,7 +278,11 @@
 
 
                 render: function () {
                 render: function () {
                     this.$el.attr('id', this.model.get('box_id'))
                     this.$el.attr('id', this.model.get('box_id'))
-                            .html(converse.templates.chatroom(this.model.toJSON()));
+                            .html(converse.templates.chatroom(
+                                    _.extend(this.model.toJSON(), {
+                                        info_close: __('Close and leave this room'),
+                                        info_configure: __('Configure this room'),
+                                    })));
                     this.renderChatArea();
                     this.renderChatArea();
                     window.setTimeout(converse.refreshWebkit, 50);
                     window.setTimeout(converse.refreshWebkit, 50);
                     return this;
                     return this;

+ 2 - 2
src/templates/chatroom.html

@@ -3,8 +3,8 @@
     <div class="dragresize dragresize-topleft"></div>
     <div class="dragresize dragresize-topleft"></div>
     <div class="dragresize dragresize-left"></div>
     <div class="dragresize dragresize-left"></div>
     <div class="chat-head chat-head-chatroom">
     <div class="chat-head chat-head-chatroom">
-        <a class="chatbox-btn close-chatbox-button icon-close"></a>
-        <a class="chatbox-btn configure-chatroom-button icon-wrench" style="display:none"></a>
+        <a class="chatbox-btn close-chatbox-button icon-close" title="{{info_close}}"></a>
+        <a class="chatbox-btn configure-chatroom-button icon-wrench" title="{{info_configure}} "style="display:none"></a>
         <div class="chat-title"> {{ _.escape(name) }} </div>
         <div class="chat-title"> {{ _.escape(name) }} </div>
         <p class="chatroom-topic"><p/>
         <p class="chatroom-topic"><p/>
     </div>
     </div>

+ 17 - 0
src/templates/chatroom_bookmark_form.html

@@ -0,0 +1,17 @@
+<div class="chatroom-form-container">
+    <form class="pure-form converse-form chatroom-form">
+        <fieldset>
+            <legend>{{heading}}</legend>
+            <label>{{label_name}}</label>
+            <input type="text" name="name" required="required"/>
+            <label>{{label_autojoin}}</label>
+            <input type="checkbox" name="autojoin"/>
+            <label>{{label_nick}}</label>
+            <input type="text" name="nick" value="{{default_nick}}"/>
+        </fieldset>
+        <fieldset>
+            <input class="pure-button button-primary" type="submit" value="{{label_submit}}"/>
+            <input class="pure-button button-cancel" type="button" value="{{label_cancel}}"/>
+        </fieldset>
+    </form>
+</div>

+ 1 - 0
tests/main.js

@@ -70,6 +70,7 @@ require([
                 "console-runner",
                 "console-runner",
                 //"spec/transcripts",
                 //"spec/transcripts",
                 "spec/converse",
                 "spec/converse",
+                "spec/bookmarks",
                 "spec/headline",
                 "spec/headline",
                 "spec/disco",
                 "spec/disco",
                 "spec/protocol",
                 "spec/protocol",