Include entity capabilities hash in outgoing presences

Also, started some work on using jsdoc for rendering API documentation.
Ideally that would go into a separate commit but that would take ages to
untangle.
This commit is contained in:
JC Brand 2018-05-11 13:31:48 +02:00
parent ab1c19a974
commit 38499917a9
19 changed files with 378 additions and 46 deletions

View File

@ -5,6 +5,7 @@
## New Features
- #161 XEP-0363: HTTP File Upload
- #194 Include entity capabilities in outgoing presence stanzas
- #337 API call to update a VCard
- #1094 Show room members who aren't currently online
- It's now also possible to edit your VCard via the UI

View File

@ -1,8 +1,7 @@
# You can set these variables from the command line.
UGLIFYJS ?= node_modules/.bin/uglifyjs
BABEL ?= node_modules/.bin/babel
BOURBON = ./node_modules/bourbon/app/assets/stylesheets/
BOOTSTRAP = ./node_modules/
BOURBON = ./node_modules/bourbon/app/assets/stylesheets/
BUILDDIR = ./docs
BUNDLE ?= ./.bundle/bin/bundle
CHROMIUM ?= ./node_modules/.bin/run-headless-chromium
@ -11,14 +10,16 @@ ESLINT ?= ./node_modules/.bin/eslint
HTTPSERVE ?= ./node_modules/.bin/http-server
HTTPSERVE_PORT ?= 8000
INKSCAPE ?= inkscape
JSDOC ?= ./node_modules/.bin/jsdoc
OXIPNG ?= oxipng
PAPER =
PO2JSON ?= ./node_modules/.bin/po2json
RJS ?= ./node_modules/.bin/r.js
SASS ?= ./.bundle/bin/sass
SPHINXBUILD ?= ./bin/sphinx-build
SED ?= sed
SPHINXBUILD ?= ./bin/sphinx-build
SPHINXOPTS =
OXIPNG ?= oxipng
UGLIFYJS ?= node_modules/.bin/uglifyjs
# In the case user wishes to use RVM
@ -235,3 +236,7 @@ html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: apidoc
apidoc:
$(JSDOC) -d docs/html/api src/converse-disco.js

View File

@ -37,8 +37,8 @@
notify_all_room_messages: [
'discuss@conference.conversejs.org'
],
// bosh_service_url: 'http://chat.example.org:5280/http-bind/',
bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
bosh_service_url: 'http://chat.example.org:5280/http-bind/',
// bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
message_archiving: 'always',
debug: true
});

View File

@ -437,6 +437,64 @@ The **disco** grouping
This grouping collects API functions related to `service discovery
<https://xmpp.org/extensions/xep-0030.html>`_.
The **disco.own** grouping
~~~~~~~~~~~~~~~~~~~~~~~~~~
The **disco.own.features** grouping
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
add
***
Paramters:
* (String) name
get
***
Returns all of the identities registered for this client (i.e. instance of Converse.js).
.. code-block:: javascript
const identities = _converse.api.disco.own.identities.get();
The **disco.own.identities** grouping
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
add
***
Paramters:
* (String) category
* (String) type
* (String) name
* (String) lang
Lets you add new identities for this client (i.e. instance of Converse.js).
.. code-block:: javascript
_converse.api.disco.own.identities.add('client', 'web', 'Converse.js');
get
***
Returns all of the identities registered for this client (i.e. instance of Converse.js).
.. code-block:: javascript
const identities = _converse.api.disco.own.identities.get();
clear
*****
Clears all previously set identities.
getIdentity
~~~~~~~~~~~

113
package-lock.json generated
View File

@ -907,6 +907,12 @@
"dev": true,
"optional": true
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true
},
"bootstrap": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0.tgz",
@ -1052,6 +1058,15 @@
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
"dev": true
},
"catharsis": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz",
"integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=",
"dev": true,
"requires": {
"underscore-contrib": "0.3.0"
}
},
"center-align": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
@ -3562,6 +3577,43 @@
"esprima": "4.0.0"
}
},
"js2xmlparser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz",
"integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=",
"dev": true,
"requires": {
"xmlcreate": "1.0.2"
}
},
"jsdoc": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz",
"integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==",
"dev": true,
"requires": {
"babylon": "7.0.0-beta.19",
"bluebird": "3.5.1",
"catharsis": "0.8.9",
"escape-string-regexp": "1.0.5",
"js2xmlparser": "3.0.0",
"klaw": "2.0.0",
"marked": "0.3.19",
"mkdirp": "0.5.1",
"requizzle": "0.2.1",
"strip-json-comments": "2.0.1",
"taffydb": "2.6.2",
"underscore": "1.8.3"
},
"dependencies": {
"babylon": {
"version": "7.0.0-beta.19",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz",
"integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==",
"dev": true
}
}
},
"jsesc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz",
@ -3631,6 +3683,15 @@
"is-buffer": "1.1.6"
}
},
"klaw": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz",
"integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=",
"dev": true,
"requires": {
"graceful-fs": "4.1.11"
}
},
"latest-version": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz",
@ -3768,6 +3829,12 @@
"yallist": "2.1.2"
}
},
"marked": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz",
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==",
"dev": true
},
"micromatch": {
"version": "2.3.11",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
@ -8604,6 +8671,23 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"requizzle": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz",
"integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=",
"dev": true,
"requires": {
"underscore": "1.6.0"
},
"dependencies": {
"underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
"dev": true
}
}
},
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
@ -9402,6 +9486,12 @@
}
}
},
"taffydb": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
"dev": true
},
"tempfile": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tempfile/-/tempfile-1.1.1.tgz",
@ -9547,6 +9637,23 @@
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
"dev": true
},
"underscore-contrib": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz",
"integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=",
"dev": true,
"requires": {
"underscore": "1.6.0"
},
"dependencies": {
"underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
"dev": true
}
}
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.3.tgz",
@ -9871,6 +9978,12 @@
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=",
"dev": true
},
"xmlcreate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
"integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=",
"dev": true
},
"xss": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/xss/-/xss-0.3.7.tgz",

View File

@ -54,6 +54,7 @@
"jasmine-core": "2.6.4",
"jed": "1.1.1",
"jquery": "3.2.1",
"jsdoc": "^3.5.5",
"jshint": "^2.9.4",
"lodash": "4.17.4",
"lodash-template-loader": "^2.0.0",

View File

@ -18,12 +18,36 @@
describe("A sent presence stanza", function () {
it("includes a entity capabilities node",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
_converse.api.disco.own.identities.clear();
_converse.api.disco.own.features.clear();
_converse.api.disco.own.identities.add("client", "pc", "Exodus 0.9.1");
_converse.api.disco.own.features.add("http://jabber.org/protocol/caps");
_converse.api.disco.own.features.add("http://jabber.org/protocol/disco#info");
_converse.api.disco.own.features.add("http://jabber.org/protocol/disco#items");
_converse.api.disco.own.features.add("http://jabber.org/protocol/muc");
const presence = _converse.xmppstatus.constructPresence();
expect(presence.toLocaleString()).toBe(
"<presence xmlns='jabber:client'>"+
"<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='QgayPKawpkPSDYmwT/WM94uAlu0='/>"+
"</presence>")
done();
}));
it("has a given priority", mock.initConverse(function (_converse) {
var pres = _converse.xmppstatus.constructPresence('online', 'Hello world');
expect(pres.toLocaleString()).toBe(
"<presence xmlns='jabber:client'>"+
"<status>Hello world</status>"+
"<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+
"</presence>"
);
_converse.priority = 2;
@ -33,6 +57,7 @@
"<show>away</show>"+
"<status>Going jogging</status>"+
"<priority>2</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+
"</presence>"
);
@ -43,6 +68,7 @@
"<show>dnd</show>"+
"<status>Doing taxes</status>"+
"<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+
"</presence>"
);
}));
@ -68,7 +94,11 @@
modal.el.querySelector('[type="submit"]').click();
expect(view.model.sendPresence).toHaveBeenCalled();
expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString())
.toBe("<presence xmlns='jabber:client'><status>My custom status</status><priority>0</priority></presence>")
.toBe("<presence xmlns='jabber:client'>"+
"<status>My custom status</status>"+
"<priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+
"</presence>")
return test_utils.waitUntil(function () {
return modal.el.getAttribute('aria-hidden') === "true";
@ -82,7 +112,9 @@
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.el.querySelector('[type="submit"]').click();
expect(_converse.connection.send.calls.mostRecent().args[0].toLocaleString())
.toBe("<presence xmlns='jabber:client'><show>dnd</show><status>My custom status</status><priority>0</priority></presence>")
.toBe("<presence xmlns='jabber:client'><show>dnd</show><status>My custom status</status><priority>0</priority>"+
"<c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='https://conversejs.org' ver='1J7kq1MEvnB6ea6vKcgCsSE37gw='/>"+
"</presence>")
done();
});
}));

View File

@ -10,7 +10,7 @@
_converse.api.user.status.message.set("I'm also happy!");
expect(_converse.connection.send).toHaveBeenCalled();
var $stanza = $(_converse.connection.send.calls.argsFor(0)[0].tree());
expect($stanza.children().length).toBe(2);
expect($stanza.children().length).toBe(3);
expect($stanza.children('status').length).toBe(1);
expect($stanza.children('status').text()).toBe("I'm also happy!");
expect($stanza.children('show').length).toBe(0);

View File

@ -71,6 +71,7 @@ require.config({
"converse-bookmarks": "src/converse-bookmarks",
"converse-chatboxes": "src/converse-chatboxes",
"converse-caps": "src/converse-caps",
"converse-chatview": "src/converse-chatview",
"converse-controlbox": "src/converse-controlbox",
"converse-core": "src/converse-core",

63
src/converse-caps.js Normal file
View File

@ -0,0 +1,63 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define(["converse-core"], factory);
}(this, function (converse) {
const { Strophe, $build, _, b64_sha1 } = converse.env;
Strophe.addNamespace('CAPS', "http://jabber.org/protocol/caps");
function propertySort (array, property) {
return array.sort((a, b) => { return a[property] > b[property] ? -1 : 1 });
}
function generateVerificationString (_converse) {
const identities = _converse.api.disco.own.identities.get(),
features = _converse.api.disco.own.features.get();
if (identities.length > 1) {
propertySort(identities, "category");
propertySort(identities, "type");
propertySort(identities, "lang");
}
let S = _.reduce(
identities,
(result, id) => `${result}${id.category}/${id.type}/${_.get(id, 'lang', '')}/${id.name}<`,
"");
features.sort();
S = _.reduce(features, (result, feature) => `${result}${feature}<`, S);
return b64_sha1(S);
}
function createCapsNode (_converse) {
return $build("c", {
'xmlns': Strophe.NS.CAPS,
'hash': "sha-1",
'node': "https://conversejs.org",
'ver': generateVerificationString(_converse)
}).nodeTree;
}
converse.plugins.add('converse-caps', {
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.
XMPPStatus: {
constructPresence () {
const presence = this.__super__.constructPresence.apply(this, arguments);
presence.root().cnode(createCapsNode(this.__super__._converse));
return presence;
}
}
}
});
}));

View File

@ -797,8 +797,8 @@
_converse.on('addClientFeatures', () => {
_converse.api.disco.addFeature(Strophe.NS.HTTPUPLOAD);
_converse.api.disco.addFeature(Strophe.NS.OUTOFBAND);
_converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD);
_converse.api.disco.own.features.add(Strophe.NS.OUTOFBAND);
});
_converse.api.listen.on('pluginsInitialized', () => {

View File

@ -1147,7 +1147,7 @@
_converse.on('connected', () => {
// Advertise that we support XEP-0382 Message Spoilers
_converse.api.disco.addFeature(Strophe.NS.SPOILER);
_converse.api.disco.own.features.add(Strophe.NS.SPOILER);
});
/************************ BEGIN API ************************/

View File

@ -74,6 +74,7 @@
'converse-bookmarks',
'converse-chatboxes',
'converse-chatview',
'converse-caps',
'converse-controlbox',
'converse-core',
'converse-disco',

View File

@ -1,9 +1,8 @@
// Converse.js (A browser based XMPP chat client)
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
//
/* This is a Converse.js plugin which add support for XEP-0030: Service Discovery */
@ -205,14 +204,14 @@
function addClientFeatures () {
// See http://xmpp.org/registrar/disco-categories.html
_converse.api.disco.addIdentity('client', 'web', 'Converse.js');
_converse.api.disco.own.identities.add('client', 'web', 'Converse.js');
_converse.api.disco.addFeature(Strophe.NS.BOSH);
_converse.api.disco.addFeature(Strophe.NS.CHATSTATES);
_converse.api.disco.addFeature(Strophe.NS.DISCO_INFO);
_converse.api.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
_converse.api.disco.own.features.add(Strophe.NS.BOSH);
_converse.api.disco.own.features.add(Strophe.NS.CHATSTATES);
_converse.api.disco.own.features.add(Strophe.NS.DISCO_INFO);
_converse.api.disco.own.features.add(Strophe.NS.ROSTERX); // Limited support
if (_converse.message_carbons) {
_converse.api.disco.addFeature(Strophe.NS.CARBONS);
_converse.api.disco.own.features.add(Strophe.NS.CARBONS);
}
_converse.emit('addClientFeatures');
return this;
@ -287,7 +286,83 @@
/* We extend the default converse.js API to add methods specific to service discovery */
_.extend(_converse.api, {
/**
* The service discovery API
* @namespace
*/
'disco': {
/**
* The "own" grouping
* @namespace
*/
'own': {
/**
* The "identities" grouping
* @namespace
*/
'identities': {
/**
* Clears all previously registered identities.
* @function
*
* @example
* _converse.api.disco.own.identities.clear();
*/
clear () {
plugin._identities = []
},
/**
* Lets you add new identities for this client (i.e. instance of Converse.js)
* @function
*
* @param {String} category - server, client, gateway, directory, etc.
* @param {String} type - phone, pc, web, etc.
* @param {String} name - "Converse.js"
* @param {String} lang - en, el, de, etc.
*
* @example
* _converse.api.disco.own.identities.clear();
*/
add (category, type, name, lang) {
for (var i=0; i<plugin._identities.length; i++) {
if (plugin._identities[i].category == category &&
plugin._identities[i].type == type &&
plugin._identities[i].name == name &&
plugin._identities[i].lang == lang) {
return false;
}
}
plugin._identities.push({category: category, type: type, name: name, lang: lang});
},
/**
* Returns all of the identities registered for this client
* (i.e. instance of Converse.js).
* @function
*
* @example
* const identities = _converse.api.disco.own.identities.get();
*/
get () {
return plugin._identities;
}
},
'features': {
add (name) {
for (var i=0; i<plugin._features.length; i++) {
if (plugin._features[i] == name) { return false; }
}
plugin._features.push(name);
},
clear () {
plugin._features = []
},
get () {
return plugin._features;
}
}
},
'info' (jid, node, callback, errback, timeout) {
const attrs = {xmlns: Strophe.NS.DISCO_INFO};
if (node) {
@ -363,25 +438,6 @@
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
'addIdentity' (category, type, name, lang) {
for (var i=0; i<plugin._identities.length; i++) {
if (plugin._identities[i].category == category &&
plugin._identities[i].type == type &&
plugin._identities[i].name == name &&
plugin._identities[i].lang == lang) {
return false;
}
}
plugin._identities.push({category: category, type: type, name: name, lang: lang});
},
'addFeature' (name) {
for (var i=0; i<plugin._features.length; i++) {
if (plugin._features[i] == name) { return false; }
}
plugin._features.push(name);
},
'getIdentity' (category, type, entity_jid) {
/* Returns a Promise which resolves with a map indicating
* whether an identity with a given type is provided by

View File

@ -384,7 +384,7 @@
});
_converse.on('addClientFeatures', () => {
_converse.api.disco.addFeature(Strophe.NS.MAM);
_converse.api.disco.own.features.add(Strophe.NS.MAM);
});
_converse.on('afterMessagesFetched', (chatboxview) => {

View File

@ -1,7 +1,7 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2012-2018, the Converse.js developers
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
@ -1176,10 +1176,10 @@
/************************ BEGIN Event Handlers ************************/
_converse.on('addClientFeatures', () => {
if (_converse.allow_muc) {
_converse.api.disco.addFeature(Strophe.NS.MUC);
_converse.api.disco.own.features.add(Strophe.NS.MUC);
}
if (_converse.allow_muc_invitations) {
_converse.api.disco.addFeature('jabber:x:conference'); // Invites
_converse.api.disco.own.features.add('jabber:x:conference'); // Invites
}
});
_converse.on('chatBoxesFetched', autoJoinRooms);

View File

@ -57,7 +57,7 @@
_converse.registerPongHandler = function () {
if (!_.isUndefined(_converse.connection.disco)) {
_converse.api.disco.addFeature(Strophe.NS.PING);
_converse.api.disco.own.features.add(Strophe.NS.PING);
}
_converse.connection.ping.addPingHandler(_converse.pong);
};

View File

@ -108,7 +108,7 @@
_converse.on('addClientFeatures', () => {
_converse.api.disco.addFeature(Strophe.NS.VCARD);
_converse.api.disco.own.features.add(Strophe.NS.VCARD);
});
_.extend(_converse.api, {

View File

@ -8,6 +8,7 @@ if (typeof define !== 'undefined') {
* Any of the following components may be removed if they're not needed.
*/
"converse-bookmarks", // XEP-0048 Bookmarks
"converse-caps",
"converse-chatview", // Renders standalone chat boxes for single user chat
"converse-controlbox", // The control box
"converse-dragresize", // Allows chat boxes to be resized by dragging them