diff --git a/CHANGES.rst b/CHANGES.rst
index cde69b9f0..44ea02515 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,22 +4,27 @@ Changelog
0.3 (unreleased)
----------------
-- Add vCard support [jcbrand]
-- Remember custom status messages upon reload. [jcbrand]
-- Remove jquery-ui dependency. [jcbrand]
+- Add vCard support
+ [jcbrand]
+- Remember custom status messages upon reload.
+ [jcbrand]
+- Remove jquery-ui dependency.
+ [jcbrand]
- Use backbone.localStorage to store the contacts roster, open chatboxes and
- chat messages. [jcbrand]
-- Fixed user status handling, which wasn't 100% according to the
- spec. [jcbrand]
-- Separate messages according to day in chats. [jcbrand]
+ chat messages.
+ [jcbrand]
+- Fixed user status handling, which wasn't 100% according to the spec.
+ [jcbrand]
+- Separate messages according to day in chats.
+ [jcbrand]
- Add support for specifying the BOSH bind URL as configuration setting.
[jcbrand]
- Improve the message counter to only increment when the window is not focused
[witekdev]
- Make fetching of list of chatrooms on a server a configuration option.
[jcbrand]
-- Use service discovery to show whether a chatroom is password protected as
- well as its number of occupents. [jcbrand]
+- Use service discovery to show all available features on a room.
+ [jcbrand]
0.2 (2013-03-28)
diff --git a/Libraries/strophe.muc.js b/Libraries/strophe.muc.js
index fd7d3bd29..25d87b4af 100644
--- a/Libraries/strophe.muc.js
+++ b/Libraries/strophe.muc.js
@@ -57,6 +57,7 @@
* (String) nick - Optional nickname to use in the chat room.
* (Function) msg_handler_cb - The function call to handle messages from the specified chat room.
* (Function) pres_handler_cb - The function call back to handle presence in the chat room.
+ * (Function) roster_cb - The function call back to handle roster changes in the chat room.
* (String) password - The optional password to use. (password protected rooms only)
*/
var msg, room_nick, _this = this;
diff --git a/converse.css b/converse.css
index d2538ef91..df5d76e47 100644
--- a/converse.css
+++ b/converse.css
@@ -92,7 +92,11 @@ img.spinner {
display: block;
font-size: 12px;
padding: 0.5em 0 0 0.5em;
+ cursor: default;
+}
+ul.participant-list li.moderator {
+ color: #FE0007;
}
.chatroom form.sendXMPPMessage {
@@ -223,6 +227,7 @@ div.chat-title {
text-overflow: ellipsis;
white-space: nowrap;
text-shadow: rgba(0,0,0,0.51) 0 -1px 0;
+ height: 1em;
}
.chat-head-chatbox,
@@ -441,7 +446,7 @@ form.search-xmpp-contact input {
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
- width: 160px;
+ width: 170px;
}
#available-chatrooms dt,
@@ -468,6 +473,41 @@ dd.available-chatroom,
text-shadow: 0 1px 0 rgba(250, 250, 250, 1);
}
+.room-info {
+ font-size: 11px;
+ font-style: normal;
+ font-weight: normal;
+}
+
+p.room-info {
+ margin: 0;
+ padding: 0;
+ display: block;
+ white-space: normal;
+}
+
+a.room-info {
+ background: url('images/information.png') no-repeat right top;
+ width: 22px;
+ float: right;
+ display: none;
+}
+
+a.open-room {
+ display: inline-block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+}
+
+dd.available-chatroom:hover a.room-info {
+ display: inline-block;
+}
+
+dd.available-chatroom:hover a.open-room {
+ width: 75%;
+}
+
#converse-roster dd a.remove-xmpp-contact {
background: url('images/delete_icon.png') no-repeat right top;
padding: 0 0 1em 0;
@@ -477,11 +517,11 @@ dd.available-chatroom,
display: none;
}
-#converse-roster dd:hover *[class*="remove-xmpp-contact"] {
+#converse-roster dd:hover a.remove-xmpp-contact {
display: inline-block;
}
-#converse-roster dd:hover *[class*="open-chat"] {
+#converse-roster dd:hover a.open-chat {
width: 75%;
}
@@ -541,10 +581,12 @@ form#converse-login {
form#converse-login input {
display: block;
+ width: 90%;
}
form#converse-login .login-submit {
margin-top: 1em;
+ width: auto;
}
form.set-xmpp-status,
diff --git a/converse.js b/converse.js
index 39459f0d3..e95536e8b 100644
--- a/converse.js
+++ b/converse.js
@@ -684,14 +684,56 @@
events: {
'submit form.add-chatroom': 'createChatRoom',
'click input#show-rooms': 'showRooms',
- 'click a.open-room': 'createChatRoom'
+ 'click a.open-room': 'createChatRoom',
+ 'click a.room-info': 'showRoomInfo'
},
room_template: _.template(
- '
' +
- '' +
- '{{name}} {{occ}}'),
+ ''+
+ '{{name}}'+
+ ' '+
+ ''),
+
+ room_description_template: _.template(
+ ''+
+ '
Description: {{desc}}
' +
+ '
Occupants: {{occ}}
' +
+ '
Features:
'+
+ '{[ if (passwordprotected) { ]}' +
+ '- Requires authentication
' +
+ '{[ } ]}' +
+ '{[ if (hidden) { ]}' +
+ '- Hidden
' +
+ '{[ } ]}' +
+ '{[ if (membersonly) { ]}' +
+ '- Requires an invitation
' +
+ '{[ } ]}' +
+ '{[ if (moderated) { ]}' +
+ '- Moderated
' +
+ '{[ } ]}' +
+ '{[ if (nonanonymous) { ]}' +
+ '- Non-anonymous
' +
+ '{[ } ]}' +
+ '{[ if (open) { ]}' +
+ '- Open room
' +
+ '{[ } ]}' +
+ '{[ if (persistent) { ]}' +
+ '- Permanent room
' +
+ '{[ } ]}' +
+ '{[ if (publicroom) { ]}' +
+ '- Public
' +
+ '{[ } ]}' +
+ '{[ if (semianonymous) { ]}' +
+ '- Semi-anonymous
' +
+ '{[ } ]}' +
+ '{[ if (temporary) { ]}' +
+ '- Temporary room
' +
+ '{[ } ]}' +
+ '{[ if (unmoderated) { ]}' +
+ '- Unmoderated
' +
+ '{[ } ]}' +
+ '' +
+ '
'
+ ),
tab_template: _.template('Rooms'),
@@ -720,37 +762,23 @@
converse.connection.muc.listRooms(
this.muc_domain,
$.proxy(function (iq) { // Success
- var name, jid, i, that = this, $available_chatrooms = this.$el.find('#available-chatrooms');
- this.rdict = {};
+ var name, jid, i, fragment,
+ that = this,
+ $available_chatrooms = this.$el.find('#available-chatrooms');
this.rooms = $(iq).find('query').find('item');
- this.rooms.each(function (i) { that.rdict[$(this).attr('jid')] = this; });
- this.fragment = document.createDocumentFragment();
if (this.rooms.length) {
$available_chatrooms.html('Rooms on '+this.muc_domain+'');
- _.each(this.rooms, $.proxy(function (room, idx) {
- converse.connection.disco.info(
- $(room).attr('jid'),
- null,
- $.proxy(function (stanza) {
- var name = $(stanza).find('identity').attr('name');
- var desc = $(stanza).find('field[var="muc#roominfo_description"] value').text();
- var occ = $(stanza).find('field[var="muc#roominfo_occupants"] value').text();
- var locked = $(stanza).find('feature[var="muc_passwordprotected"]').length;
- var jid = $(stanza).attr('from');
- var classes = locked && 'locked' || '';
- delete this.rdict[jid];
- this.$el.find('#available-chatrooms').append(
- this.room_template({'name':name,
- 'desc':desc,
- 'occ':occ,
- 'jid':jid,
- 'classes': classes
- }));
- if (_.keys(this.rdict).length === 0) {
- $('input#show-rooms').show().siblings('img.spinner').remove();
- }
- }, this));
- }, this));
+ fragment = document.createDocumentFragment();
+ for (i=0; iNo rooms on '+this.muc_domain+'');
$('input#show-rooms').show().siblings('img.spinner').remove();
@@ -780,6 +808,53 @@
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.append('');
+ converse.connection.disco.info(
+ $(target).attr('data-room-jid'),
+ null,
+ $.proxy(function (stanza) {
+ var $stanza = $(stanza);
+ // All MUC features shown here: http://xmpp.org/registrar/disco-features.html
+ var desc = $stanza.find('field[var="muc#roominfo_description"] value').text();
+ var occ = $stanza.find('field[var="muc#roominfo_occupants"] value').text();
+ var hidden = $stanza.find('feature[var="muc_hidden"]').length;
+ var membersonly = $stanza.find('feature[var="muc_membersonly"]').length;
+ var moderated = $stanza.find('feature[var="muc_moderated"]').length;
+ var nonanonymous = $stanza.find('feature[var="muc_nonanonymous"]').length;
+ var open = $stanza.find('feature[var="muc_open"]').length;
+ var passwordprotected = $stanza.find('feature[var="muc_passwordprotected"]').length;
+ var persistent = $stanza.find('feature[var="muc_persistent"]').length;
+ var publicroom = $stanza.find('feature[var="muc_public"]').length;
+ var semianonymous = $stanza.find('feature[var="muc_semianonymous"]').length;
+ var temporary = $stanza.find('feature[var="muc_temporary"]').length;
+ var unmoderated = $stanza.find('feature[var="muc_unmoderated"]').length;
+ $dd.find('img.spinner').replaceWith(
+ this.room_description_template({
+ 'desc':desc,
+ 'occ':occ,
+ 'hidden':hidden,
+ 'membersonly':membersonly,
+ 'moderated':moderated,
+ 'nonanonymous':nonanonymous,
+ 'open':open,
+ 'passwordprotected':passwordprotected,
+ 'persistent':persistent,
+ 'publicroom': publicroom,
+ 'semianonymous':semianonymous,
+ 'temporary':temporary,
+ 'unmoderated':unmoderated
+ }));
+ }, this));
+ }
+ },
+
createChatRoom: function (ev) {
ev.preventDefault();
var name, server, jid, $name, $server, errors;
@@ -986,7 +1061,8 @@
this.model.get('nick'),
$.proxy(this.onChatRoomMessage, this),
$.proxy(this.onChatRoomPresence, this),
- $.proxy(this.onChatRoomRoster, this));
+ $.proxy(this.onChatRoomRoster, this),
+ null);
this.model.messages.on('add', this.showMessage, this);
this.model.on('destroy', function (model, response, options) {
@@ -1004,22 +1080,84 @@
onLeave: function () {},
+ showRoomConfigOptions: function (stanza) {
+ // FIXME: Show a proper configuration form
+ var $chat_content = this.$el.find('.chat-content'),
+ $stanza = $(stanza),
+ $fields = $stanza.find('field'),
+ title = $stanza.find('title').text(),
+ instructions = $stanza.find('instructions').text(),
+ i;
+ $chat_content.append(title);
+ $chat_content.append(instructions);
+ for (i=0; i<$fields.length; i++) {
+ $field = $($fields[i]);
+ $chat_content.append('');
+ // $chat_content.append('{{nick}}'
+ ),
+
onChatRoomRoster: function (roster, room) {
- // underscore size is needed because roster is an object
var controlboxview = converse.chatboxesview.views.controlbox,
roster_size = _.size(roster),
$participant_list = this.$el.find('.participant-list'),
- participants = [],
- i;
+ participants = [], keys = _.keys(roster), i;
this.$el.find('.participant-list').empty();
for (i=0; i' + Strophe.unescapeNode(_.keys(roster)[i]) + '');
+ participants.push(
+ this.occupant_template({
+ role: roster[keys[i]].role,
+ nick: Strophe.unescapeNode(keys[i])
+ }));
}
$participant_list.append(participants.join(""));
return true;
@@ -1344,7 +1498,6 @@
this.$el.addClass('current-xmpp-contact');
this.$el.html(this.template(item.toJSON()));
}
-
return this;
},
@@ -1802,7 +1955,6 @@
converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
this.save({'status_message': status_message});
}
-
});
converse.XMPPStatusView = Backbone.View.extend({
@@ -1834,7 +1986,6 @@
'' +
''),
-
renderStatusChangeForm: function (ev) {
ev.preventDefault();
var status_message = this.model.get('status') || 'offline';
@@ -1937,6 +2088,7 @@
* 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
+ * All features are shown here: http://xmpp.org/registrar/disco-features.html
*/
model: converse.Feature,
initialize: function () {
@@ -2114,8 +2266,8 @@
converse.onConnected = function (connection) {
this.connection = connection;
- // this.connection.xmlInput = function (body) { console.log(body); };
- // this.connection.xmlOutput = function (body) { console.log(body); };
+ this.connection.xmlInput = function (body) { console.log(body); };
+ this.connection.xmlOutput = function (body) { console.log(body); };
this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
this.domain = Strophe.getDomainFromJid(this.connection.jid);
this.features = new this.Features();
diff --git a/mock.js b/mock.js
new file mode 100644
index 000000000..38cd8fcf0
--- /dev/null
+++ b/mock.js
@@ -0,0 +1,43 @@
+(function (root, factory) {
+ define("mock",
+ ['converse'],
+ function() {
+ return factory();
+ });
+}(this, function (converse) {
+ var mock_connection = {
+ 'muc': {
+ 'listRooms': function () {},
+ 'join': function () {},
+ 'leave': function () {}
+ },
+ 'jid': 'dummy@localhost',
+ 'addHandler': function (handler, ns, name, type, id, from, options) {
+ return function () {};
+ },
+ 'send': function () {},
+ 'roster': {
+ 'add': function () {},
+ 'authorize': function () {},
+ 'unauthorize': function () {},
+ 'get': function () {},
+ 'subscribe': function () {},
+ 'registerCallback': function () {}
+ },
+ 'vcard': {
+ 'get': function (callback, jid) {
+ var name = jid.split('@')[0].replace('.', ' ').split(' ');
+ var firstname = name[0].charAt(0).toUpperCase()+name[0].slice(1);
+ var lastname = name[1].charAt(0).toUpperCase()+name[1].slice(1);
+ var fullname = firstname+' '+lastname;
+ var vcard = $iq().c('vCard').c('FN').t(fullname);
+ callback(vcard.tree());
+ }
+ },
+ 'disco': {
+ 'info': function () {},
+ 'items': function () {}
+ }
+ };
+ return mock_connection;
+}));
diff --git a/spec/ChatRoomSpec.js b/spec/ChatRoomSpec.js
new file mode 100644
index 000000000..6fa1f252b
--- /dev/null
+++ b/spec/ChatRoomSpec.js
@@ -0,0 +1,239 @@
+(function (root, factory) {
+ define([
+ "converse",
+ "mock"
+ ], function (converse, mock_connection) {
+ return factory(converse, mock_connection);
+ }
+ );
+} (this, function (converse, mock_connection) {
+ return describe("ChatRooms", $.proxy(function() {
+ var chatroom_names = [
+ 'Dyon van de Wege', 'Thomas Kalb', 'Dirk Theissen', 'Felix Hofmann', 'Ka Lek', 'Anne Ebersbacher'
+ ];
+ describe("A Chat Room", $.proxy(function () {
+ beforeEach($.proxy(function () {
+ if (!$("div#controlbox").is(':visible')) {
+ $('.toggle-online-users').click();
+ }
+ var roomspanel = this.chatboxesview.views.controlbox.roomspanel;
+ var $input = roomspanel.$el.find('input.new-chatroom-name');
+ var $server = roomspanel.$el.find('input.new-chatroom-server');
+ $input.val('lounge');
+ $server.val('muc.localhost');
+ roomspanel.$el.find('form').submit();
+ $('.toggle-online-users').click();
+ }, converse));
+
+ it("shows users currently present in the room", $.proxy(function () {
+ var chatroomview = this.chatboxesview.views['lounge@muc.localhost'];
+ var $participant_list = chatroomview.$el.find('.participant-list');
+ var roster = {}, room = {}, i;
+
+ for (i=0; i