Quellcode durchsuchen

Merge remote-tracking branch 'origin' into bootstrap4

JC Brand vor 7 Jahren
Ursprung
Commit
617019af0e
100 geänderte Dateien mit 34157 neuen und 8707 gelöschten Zeilen
  1. 1 1
      .github/PULL_REQUEST_TEMPLATE.md
  2. 61 0
      CHANGES.md
  3. 1 1
      COPYRIGHT
  4. 13 9
      Makefile
  5. 23 3
      README.md
  6. 19 19
      css/converse-muc-embedded.css
  7. 35 19
      css/converse.css
  8. 292 0
      css/fonts.css
  9. 35 19
      css/inverse.css
  10. 4 2
      css/mobile.css
  11. 8 1
      css/theme.css
  12. 2 2
      demo/anonymous.html
  13. 13 38
      demo/embedded.html
  14. 2 2
      demo/index.html
  15. 2 2
      demo/without_bundled_dependencies.html
  16. 2 2
      dev.html
  17. 22764 0
      dist/converse-muc-embedded.js
  18. 329 278
      dist/converse-no-dependencies.js
  19. 1763 693
      dist/converse.js
  20. 1 1
      docs/source/_static/style.css
  21. 2 2
      docs/source/conf.py
  22. 97 31
      docs/source/configuration.rst
  23. 58 25
      docs/source/developer_api.rst
  24. 13 13
      docs/source/events.rst
  25. 6 6
      docs/source/features.rst
  26. 1 1
      docs/source/manual.rst
  27. 3 3
      docs/source/other_frameworks.rst
  28. 1 1
      docs/source/plugin_development.rst
  29. 3 3
      docs/source/quickstart.rst
  30. 13 15
      index.html
  31. 0 0
      locale/af/LC_MESSAGES/converse.json
  32. 271 243
      locale/af/LC_MESSAGES/converse.po
  33. 0 0
      locale/ca/LC_MESSAGES/converse.json
  34. 270 243
      locale/ca/LC_MESSAGES/converse.po
  35. 316 293
      locale/converse.pot
  36. 0 0
      locale/de/LC_MESSAGES/converse.json
  37. 355 330
      locale/de/LC_MESSAGES/converse.po
  38. 0 0
      locale/es/LC_MESSAGES/converse.json
  39. 321 294
      locale/es/LC_MESSAGES/converse.po
  40. 0 0
      locale/fr/LC_MESSAGES/converse.json
  41. 321 295
      locale/fr/LC_MESSAGES/converse.po
  42. 0 0
      locale/he/LC_MESSAGES/converse.json
  43. 321 294
      locale/he/LC_MESSAGES/converse.po
  44. 0 0
      locale/hu/LC_MESSAGES/converse.json
  45. 320 293
      locale/hu/LC_MESSAGES/converse.po
  46. 0 0
      locale/id/LC_MESSAGES/converse.json
  47. 323 296
      locale/id/LC_MESSAGES/converse.po
  48. 0 0
      locale/it/LC_MESSAGES/converse.json
  49. 299 272
      locale/it/LC_MESSAGES/converse.po
  50. 0 0
      locale/ja/LC_MESSAGES/converse.json
  51. 349 320
      locale/ja/LC_MESSAGES/converse.po
  52. 0 0
      locale/nb/LC_MESSAGES/converse.json
  53. 328 303
      locale/nb/LC_MESSAGES/converse.po
  54. 0 0
      locale/nl/LC_MESSAGES/converse.json
  55. 545 562
      locale/nl/LC_MESSAGES/converse.po
  56. 0 0
      locale/pl/LC_MESSAGES/converse.json
  57. 270 243
      locale/pl/LC_MESSAGES/converse.po
  58. 0 0
      locale/pt_BR/LC_MESSAGES/converse.json
  59. 502 527
      locale/pt_BR/LC_MESSAGES/converse.po
  60. 0 0
      locale/ru/LC_MESSAGES/converse.json
  61. 474 479
      locale/ru/LC_MESSAGES/converse.po
  62. 0 0
      locale/uk/LC_MESSAGES/converse.json
  63. 280 253
      locale/uk/LC_MESSAGES/converse.po
  64. 0 0
      locale/zh_CN/LC_MESSAGES/converse.json
  65. 321 294
      locale/zh_CN/LC_MESSAGES/converse.po
  66. 0 0
      locale/zh_TW/LC_MESSAGES/converse.json
  67. 339 309
      locale/zh_TW/LC_MESSAGES/converse.po
  68. BIN
      logo/keycdn.png
  69. 42 0
      logo/mastodon.svg
  70. BIN
      logo/wikisuite-white.png
  71. BIN
      logo/wikisuite.png
  72. 2 2
      mobile.html
  73. 310 381
      package-lock.json
  74. 5 6
      package.json
  75. 32 10
      sass/_chatbox.scss
  76. 4 4
      sass/_chatrooms.scss
  77. 2 1
      sass/_controlbox.scss
  78. 27 24
      sass/_core.scss
  79. 78 78
      sass/_fonts.scss
  80. 1 1
      sass/_muc_embedded.scss
  81. 3 0
      sass/only-fonts.scss
  82. 527 438
      spec/bookmarks.js
  83. 5 5
      spec/chatbox.js
  84. 189 183
      spec/chatroom.js
  85. 2 2
      spec/disco.js
  86. 66 1
      spec/otr.js
  87. 1 1
      spec/protocol.js
  88. 240 0
      spec/spoilers.js
  89. 69 31
      src/converse-bookmarks.js
  90. 46 16
      src/converse-chatboxes.js
  91. 257 94
      src/converse-chatview.js
  92. 18 10
      src/converse-controlbox.js
  93. 54 35
      src/converse-core.js
  94. 48 4
      src/converse-disco.js
  95. 9 4
      src/converse-dragresize.js
  96. 3 2
      src/converse-fullscreen.js
  97. 4 5
      src/converse-headline.js
  98. 4 6
      src/converse-mam.js
  99. 1 1
      src/converse-minimize.js
  100. 16 27
      src/converse-muc-embedded.js

+ 1 - 1
.github/PULL_REQUEST_TEMPLATE.md

@@ -2,7 +2,7 @@ Thanks for making a pull request to converse.js!
 
 Before submitting your request, please make sure the following conditions are met:
 
-- [ ] Add a changelog entry for your change in `docs/CHANGES.md`
+- [ ] Add a changelog entry for your change in `CHANGES.md`
 - [ ] When adding a configuration variable, please make sure to
       document it in `docs/source/configuration.rst`
 - [ ] Please add a test for your change. Tests can be run in the commandline

+ 61 - 0
CHANGES.md

@@ -1,5 +1,66 @@
 # Changelog
 
+## 3.3.3 (2018-02-14)
+
+### Bugfixes
+- Attribute error when empty IQ stanza is returned for vCard query
+- In fullscreen view, sometimes a background MUC would come into the foreground
+  when a new message appears inside it.
+
+### Security fixes
+
+- CVE-2018-6591: Don't allow PEP bookmarks if `pubsub#publish-options` is not advertised by the server.
+
+    In previous versions of converse.js, bookmarks sent to servers that don't
+    support `pubsub#publish-options` were visible to all your contacts, even
+    though they should be kept private. This is due to those servers simply
+    ignoring the `pubsub#publish-options` directive and converse.js not checking
+    first whether `pubsub#publish-options` is supported before setting bookmarks
+    via PEP.
+
+    More info here: https://gultsch.de/converse_bookmarks.html
+
+### New features
+- XEP-0382 Spoiler Messages (currently only for private chats)
+- Listen for new room bookmarks pushed from the user's PEP service.
+- Simplified the [embedded](https://conversejs.org/demo/embedded.html) usecase.
+    - No need to manually blacklist or whitelist any plugins.
+    - Relies on the [view_mode](https://conversejs.org/docs/html/configurations.html#view-mode) being set to `'embedded'`.
+    - The main `converse.js` build can be used for the embedded usecase.
+    - Maintain MUC session upon page reload
+
+### API changes
+- New API method `_converse.disco.getIdentity` to check whether a JID has a given identity.
+
+### Configuration settings
+- `auto_reconnect` is now set to `true` by default.
+- New configuration setting [allow_public_bookmarks](https://conversejs.org/docs/html/configurations.html#allow-public-bookmarks)
+- New configuration setting [root](https://conversejs.org/docs/html/configurations.html#root)
+- The [view_mode](https://conversejs.org/docs/html/configurations.html#view-mode) setting now has a new possible value: `embedded`
+
+### Translation updates
+- Chinese (Traditional), French, German, Portuguese (Brazil), Russian, Ukrainian
+
+## 3.3.2 (2018-01-29)
+
+### Bugfixes
+
+- Various fixes for IE11.
+- Could not register on Ejabberd 18. `"Missing attribute 'id' in tag qualified by namespace 'jabber:client'"`
+- #878 Ending slash in link not recognized
+- #921 FATAL error when `visible_toolbar_buttons.emoji = false`
+- #959 Add padding for the iPhone X (to the mobile CSS).
+- #993 `moment.format` is not a function error when sending a message.
+- #994 TypeError when using the `user.login` API.
+- #995 `ChildNode.replaceWith` is not available in Internet Explorer or Safari. Use `Node.replaceChild` instead.
+- #999 MUC Chat Send button causes page reload
+- #1000 Scroll to bottom when maximizing a chat room.
+- #1003 Handle bare MUC room JIDs
+
+### Translation changes
+
+- Updated Dutch, French, Japanese, Norwegian Bokmål and Ukrainian translations
+
 ## 3.3.1 (2018-01-18)
 
 ### UI/UX changes

+ 1 - 1
COPYRIGHT

@@ -2,7 +2,7 @@
  *
  *  An XMPP chat client that runs in the browser.
  *
- *  Version: 3.3.1
+ *  Version: 3.3.3
  *
  *  Copyright: JC Brand 2012-2017
  *  Except for 3rd party dependencies.

+ 13 - 9
Makefile

@@ -10,7 +10,7 @@ CHROMIUM		?= ./node_modules/.bin/run-headless-chromium
 CLEANCSS		?= ./node_modules/clean-css-cli/bin/cleancss --skip-rebase
 ESLINT		  	?= ./node_modules/.bin/eslint
 HTTPSERVE	   	?= ./node_modules/.bin/http-server
-HTTPSERVE_PORT	        ?= 8000
+HTTPSERVE_PORT  ?= 8000
 PAPER		   	=
 PO2JSON		 	?= ./node_modules/.bin/po2json
 RJS			 	?= ./node_modules/.bin/r.js
@@ -72,7 +72,7 @@ serve_bg: dev
 ########################################################################
 ## Translation machinery
 
-GETTEXT = xgettext --language="JavaScript" --keyword=__ --keyword=___ --from-code=UTF-8 --output=locale/converse.pot dist/converse-no-dependencies.js --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=3.3.1 -c
+GETTEXT = xgettext --language="JavaScript" --keyword=__ --keyword=___ --from-code=UTF-8 --output=locale/converse.pot dist/converse-no-dependencies.js --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=3.3.3 -c
 
 .PHONY: pot
 pot: dist/converse-no-dependencies.js
@@ -101,6 +101,7 @@ release:
 	$(SED) -ri s/version\ =\ \'[0-9]\+\.[0-9]\+\.[0-9]\+\'/version\ =\ \'$(VERSION)\'/ docs/source/conf.py
 	$(SED) -ri s/release\ =\ \'[0-9]\+\.[0-9]\+\.[0-9]\+\'/release\ =\ \'$(VERSION)\'/ docs/source/conf.py
 	$(SED) -ri "s/(Unreleased)/`date +%Y-%m-%d`/" CHANGES.md
+	$(SED) -ri "s/cdn.conversejs.org\/[0-9]+\.[0-9]+\.[0-9]+/cdn.conversejs.org\/$(VERSION)/" docs/source/quickstart.rst
 	make pot
 	make po
 	make po2json
@@ -131,21 +132,21 @@ dev: stamp-bundler stamp-npm
 ## Builds
 
 .PHONY: css
-css: sass/*.scss css/converse.css css/converse.min.css css/mobile.min.css css/theme.min.css css/converse-muc-embedded.min.css css/inverse.css css/inverse.min.css
+css: sass/*.scss css/converse.css css/converse.min.css css/mobile.min.css css/theme.min.css css/converse-muc-embedded.min.css css/inverse.css css/inverse.min.css css/fonts.css
 
-css/inverse.css:: stamp-bundler sass sass/*
+css/inverse.css:: stamp-bundler sass sass/*.scss
 	$(SASS) -I $(BOURBON) -I $(BOOTSTRAP) sass/inverse/inverse.scss css/inverse.css
 
 css/inverse.min.css:: css/inverse.css
 	$(CLEANCSS) css/inverse.css > css/inverse.min.css
 
-css/converse-muc-embedded.css:: stamp-bundler sass/*
+css/converse-muc-embedded.css:: stamp-bundler sass/*.scss
 	$(SASS) -I $(BOURBON) -I $(BOOTSTRAP) sass/_muc_embedded.scss css/converse-muc-embedded.css
 
-css/converse-muc-embedded.min.css:: stamp-bundler sass css/converse-muc-embedded.css
+css/converse-muc-embedded.min.css:: dev sass css/converse-muc-embedded.css
 	$(CLEANCSS) css/converse-muc-embedded.css > css/converse-muc-embedded.min.css
 
-css/converse.css:: stamp-bundler sass/*
+css/converse.css:: stamp-bundler sass/*.scss
 	$(SASS) -I $(BOURBON) -I $(BOOTSTRAP) sass/converse/converse.scss css/converse.css
 
 css/converse.min.css:: css/converse.css
@@ -154,9 +155,12 @@ css/converse.min.css:: css/converse.css
 css/theme.min.css:: stamp-npm css/theme.css
 	$(CLEANCSS) css/theme.css > css/theme.min.css
 
-css/mobile.min.css:: stamp-npm sass/*
+css/mobile.min.css:: stamp-npm sass/*.scss
 	$(CLEANCSS) css/mobile.css > css/mobile.min.css
 
+css/fonts.css:: dev sass/*.scss
+	$(SASS) -I $(BOURBON_TEMPLATES) sass/only-fonts.scss css/fonts.css
+
 .PHONY: watch
 watch: stamp-bundler
 	$(SASS) --watch -I $(BOURBON) -I $(BOOTSTRAP) sass/converse/converse.scss:css/converse.css sass/_muc_embedded.scss:css/converse-muc-embedded.css sass/inverse/inverse.scss:css/inverse.css
@@ -219,7 +223,7 @@ eslint: stamp-npm
 
 .PHONY: check
 check: eslint
-	LOG_CR_VERBOSITY=INFO $(CHROMIUM) --no-sandbox http://localhost:$(HTTPSERVE_PORT)/tests.html
+	LOG_CR_VERBOSITY=INFO $(CHROMIUM) --no-sandbox http://localhost:$(HTTPSERVE_PORT)/tests/index.html
 
 ########################################################################
 ## Documentation

+ 23 - 3
README.md

@@ -1,7 +1,6 @@
 # converse.js
 
-[![Greenkeeper badge](https://badges.greenkeeper.io/jcbrand/converse.js.svg)](https://greenkeeper.io/)
-
+[![inVerse](https://inverse.chat/badge.svg?room=discuss@conference.conversejs.org)](https://inverse.chat/#converse/room?jid=discuss@conference.conversejs.org)
 [![Travis](https://api.travis-ci.org/jcbrand/converse.js.png?branch=master)](https://travis-ci.org/jcbrand/converse.js)
 [![Bountysource bounties](https://img.shields.io/bountysource/team/converse.js/activity.svg?maxAge=2592000)](https://www.bountysource.com/teams/converse.js/issues?tracker_ids=194169)
 [![Translation status](https://hosted.weblate.org/widgets/conversejs/-/svg-badge.svg)](https://hosted.weblate.org/engage/conversejs/?utm_source=widget)
@@ -42,7 +41,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
 ## Features
 
 -   A [plugin architecture](https://conversejs.org/docs/html/plugin_development.html) based on [pluggable.js](https://jcbrand.github.io/pluggable.js/)
--   Single-user chat
+-   Single-user and group chats
 -   Contacts and groups
 -   Multi-user chat rooms [XEP 45](http://xmpp.org/extensions/xep-0045.html)
 -   Direct invitations to chat rooms [XEP 249](http://xmpp.org/extensions/xep-0249.html)
@@ -59,6 +58,7 @@ which shows you how to use the CDN (content delivery network) to quickly get a d
 -   Third person "/me" messages [XEP 245](http://xmpp.org/extensions/xep-0245.html)
 -   XMPP Ping [XEP 199](http://xmpp.org/extensions/xep-0199.html)
 -   Server-side archiving of messages [XEP 313](http://xmpp.org/extensions/xep-0313.html)
+-   Hidden Messages (aka Spoilers) [XEP 382](http://xmpp.org/extensions/xep-0382.html)
 -   Client state indication [XEP 352](http://xmpp.org/extensions/xep-0352.html)
 -   Off-the-record encryption
 -   Translated into 16 languages
@@ -103,3 +103,23 @@ For support queries and discussions, please join the mailing list: <conversejs@l
 Also take a look at the [mailing list archives](http://librelist.com/browser/conversejs).
 
 Issues can be logged on the [Github issue tracker](https://github.com/jcbrand/converse.js/issues).
+
+## Donations
+
+A heartfelt thanks for everyone who has supported this project over the years.
+Many people have contributed testing, bugfixes, features and corrections.
+
+Recently we have started accepting donations via [Patreon](https://www.patreon.com/jcbrand) and [Liberapay](https://liberapay.com/jcbrand).
+
+The following people are making recurring donations:
+
+* [Rafael](https://www.patreon.com/user/creators?u=4340078)
+* [mt7479](https://www.patreon.com/user/creators?u=3892290)
+* [roelra](https://www.patreon.com/user/creators?u=5958918)
+* [Guus der Kinderen](https://www.patreon.com/user/creators?u=8302585)
+* An anonymous backer on Liberapay
+
+Additionally this project is supported by
+
+* [![KeyCDN](https://conversejs.org/logo/keycdn.png)](https://www.keycdn.com/)
+* [![Wikisuite](https://conversejs.org/logo/wikisuite.png)](http://wikisuite.org)

+ 19 - 19
css/converse-muc-embedded.css

@@ -3,7 +3,7 @@
     https://coolors.co/app/264653-2a9d8f-e9c46a-f4a261-e76f51
     http://paletton.com/#uid=70a0u0kkNs+b4JOgryLpxqpsbkI
  */
-#converse-embedded-chat {
+#conversejs.converse-embedded {
   -webkit-box-sizing: border-box;
   -moz-box-sizing: border-box;
   box-sizing: border-box;
@@ -11,56 +11,56 @@
   right: auto;
   position: relative;
   width: 100%; }
-  #converse-embedded-chat *, #converse-embedded-chat *:before, #converse-embedded-chat *:after {
+  #conversejs.converse-embedded *, #conversejs.converse-embedded *:before, #conversejs.converse-embedded *:after {
     -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
     box-sizing: border-box; }
-  #converse-embedded-chat form.pure-form.converse-centered-form {
+  #conversejs.converse-embedded form.pure-form.converse-centered-form {
     position: absolute;
     top: 30%;
     transform: translateY(-50%); }
-  #converse-embedded-chat .chatroom {
+  #conversejs.converse-embedded .chatroom {
     width: auto; }
-  #converse-embedded-chat .flyout {
+  #conversejs.converse-embedded .flyout {
     bottom: auto;
     display: block;
     position: relative; }
-  #converse-embedded-chat .chatbox {
+  #conversejs.converse-embedded .chatbox {
     float: none; }
-    #converse-embedded-chat .chatbox .box-flyout {
+    #conversejs.converse-embedded .chatbox .box-flyout {
       box-shadow: none; }
-    #converse-embedded-chat .chatbox .chat-title {
+    #conversejs.converse-embedded .chatbox .chat-title {
       padding: 0.3em;
       font-size: 120%; }
-  #converse-embedded-chat .chatbox-btn {
+  #conversejs.converse-embedded .chatbox-btn {
     display: none; }
-  #converse-embedded-chat .chatroom .box-flyout {
+  #conversejs.converse-embedded .chatroom .box-flyout {
     min-width: auto;
     width: 100%;
     height: 55vh; }
-    #converse-embedded-chat .chatroom .box-flyout .chat-body {
+    #conversejs.converse-embedded .chatroom .box-flyout .chat-body {
       height: -webkit-calc(100% - 55px);
       height: calc(100% - 55px); }
-    #converse-embedded-chat .chatroom .box-flyout .occupants-heading {
+    #conversejs.converse-embedded .chatroom .box-flyout .occupants-heading {
       font-size: 120%; }
-    #converse-embedded-chat .chatroom .box-flyout .chat-content {
+    #conversejs.converse-embedded .chatroom .box-flyout .chat-content {
       height: calc(100% - 97px); }
-      #converse-embedded-chat .chatroom .box-flyout .chat-content .chat-message {
+      #conversejs.converse-embedded .chatroom .box-flyout .chat-content .chat-message {
         margin: 0.5em;
         font-size: 120%; }
-    #converse-embedded-chat .chatroom .box-flyout .sendXMPPMessage .chat-textarea {
+    #conversejs.converse-embedded .chatroom .box-flyout .sendXMPPMessage .chat-textarea {
       padding: 0.5em;
       font-size: 110%; }
-    #converse-embedded-chat .chatroom .box-flyout .chatroom-body .chatroom-form-container {
+    #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chatroom-form-container {
       font-size: 180%;
       float: left;
       height: 100%;
       position: relative; }
-      #converse-embedded-chat .chatroom .box-flyout .chatroom-body .chatroom-form-container input {
+      #conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chatroom-form-container input {
         font-size: 60%; }
-    #converse-embedded-chat .chatroom .box-flyout .occupants .occupant-list {
+    #conversejs.converse-embedded .chatroom .box-flyout .occupants .occupant-list {
       padding-left: 0.3em; }
-      #converse-embedded-chat .chatroom .box-flyout .occupants .occupant-list li.occupant {
+      #conversejs.converse-embedded .chatroom .box-flyout .occupants .occupant-list li.occupant {
         font-size: 120%; }
 
 /*# sourceMappingURL=converse-muc-embedded.css.map */

+ 35 - 19
css/converse.css

@@ -4758,6 +4758,8 @@
 #converse-embedded-chat,
 #conversejs {
   margin-left: -0.5em;
+  padding-left: env(safe-area-inset-left);
+  padding-right: env(safe-area-inset-right);
   bottom: 0;
   height: auto;
   width: 100vw;
@@ -5250,6 +5252,9 @@
     #conversejs .chatbox {
       margin: 0;
       width: 100%; } }
+  #converse-embedded-chat .chatbox .spoiler,
+  #conversejs .chatbox .spoiler {
+    background-color: #e7f7ee; }
   #converse-embedded-chat .chatbox .box-flyout,
   #conversejs .chatbox .box-flyout {
     background-color: white;
@@ -5328,6 +5333,8 @@
         margin-top: 1em; }
     #converse-embedded-chat .chatbox .chat-body .chat-image,
     #conversejs .chatbox .chat-body .chat-image {
+      height: auto;
+      width: auto;
       max-height: 24em;
       max-width: 100%; }
     #converse-embedded-chat .chatbox .chat-body .chat-action,
@@ -5358,6 +5365,10 @@
       #conversejs .chatbox .chat-body .chat-message .chat-msg-content {
         max-width: 100%;
         word-wrap: break-word; }
+        #converse-embedded-chat .chatbox .chat-body .chat-message .chat-msg-content.spoiler,
+        #conversejs .chatbox .chat-body .chat-message .chat-msg-content.spoiler {
+          border-radius: 4px;
+          padding: 0.5em; }
         #converse-embedded-chat .chatbox .chat-body .chat-message .chat-msg-content .emojione,
         #conversejs .chatbox .chat-body .chat-message .chat-msg-content .emojione {
           margin-bottom: -6px; }
@@ -5391,6 +5402,10 @@
     line-height: 1.3em;
     height: 206px;
     height: calc(100% - 96px); }
+    #converse-embedded-chat .chatbox .chat-content .toggle-spoiler:before,
+    #conversejs .chatbox .chat-content .toggle-spoiler:before {
+      padding-right: 0.25em;
+      whitespace: nowrap; }
   #converse-embedded-chat .chatbox .chat-content-sendbutton,
   #conversejs .chatbox .chat-content-sendbutton {
     height: calc(100% - 128px); }
@@ -5424,6 +5439,9 @@
       #converse-embedded-chat .chatbox .sendXMPPMessage,
       #conversejs .chatbox .sendXMPPMessage {
         width: 100%; } }
+    #converse-embedded-chat .chatbox .sendXMPPMessage .spoiler-hint,
+    #conversejs .chatbox .sendXMPPMessage .spoiler-hint {
+      width: 100%; }
     #converse-embedded-chat .chatbox .sendXMPPMessage .chat-textarea,
     #conversejs .chatbox .sendXMPPMessage .chat-textarea {
       border-top-left-radius: 0;
@@ -5435,6 +5453,9 @@
       width: 100%;
       border: none;
       resize: none; }
+      #converse-embedded-chat .chatbox .sendXMPPMessage .chat-textarea.spoiler,
+      #conversejs .chatbox .sendXMPPMessage .chat-textarea.spoiler {
+        height: 42px; }
     #converse-embedded-chat .chatbox .sendXMPPMessage .send-button,
     #conversejs .chatbox .sendXMPPMessage .send-button {
       position: absolute;
@@ -5450,20 +5471,17 @@
     #conversejs .chatbox .sendXMPPMessage .chat-toolbar {
       box-sizing: border-box;
       margin: 0;
-      padding: 5px;
+      padding: 0.25em;
       height: 25px;
       display: block;
-      background-color: #E7FBF0; }
+      background-color: #50c282;
+      color: white; }
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar a,
       #conversejs .chatbox .sendXMPPMessage .chat-toolbar a {
-        font-size: 14px;
-        color: #777;
+        color: white;
+        font-size: 16px;
         text-decoration: none;
         text-shadow: none; }
-      #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .chat-toolbar-text,
-      #conversejs .chatbox .sendXMPPMessage .chat-toolbar .chat-toolbar-text {
-        font-size: 12px;
-        padding-right: 3px; }
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .unencrypted a,
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .unencrypted,
       #conversejs .chatbox .sendXMPPMessage .chat-toolbar .unencrypted a,
@@ -5554,15 +5572,12 @@
               color: #8f2831; }
         #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu,
         #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu {
-          color: #777; }
-        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley,
-        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley {
-          padding-left: 5px; }
-          #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
-          #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover,
-          #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
-          #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover {
-            background-color: #DCF9F6; }
+          color: white; }
+        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
+        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover,
+        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
+        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover {
+          background-color: #DCF9F6; }
         #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul,
         #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul {
           z-index: 99; }
@@ -5964,7 +5979,8 @@
     padding: 1px;
     float: right; }
   #conversejs #controlbox .controlbox-panes {
-    overflow-y: scroll; }
+    height: 100%;
+    overflow-y: auto; }
   #conversejs #controlbox .controlbox-pane {
     padding: 1em;
     background-color: white;
@@ -6472,7 +6488,7 @@
           background-color: #E77051; }
   #converse-embedded-chat .chatroom .sendXMPPMessage .chat-toolbar,
   #conversejs .chatroom .sendXMPPMessage .chat-toolbar {
-    background-color: #FFECE7; }
+    background-color: #ed957e; }
   #converse-embedded-chat .chatroom .sendXMPPMessage .chat-textarea,
   #conversejs .chatroom .sendXMPPMessage .chat-textarea {
     border-bottom-right-radius: 0; }

+ 292 - 0
css/fonts.css

@@ -0,0 +1,292 @@
+/*
+    Color scheme helpers:
+    https://coolors.co/app/264653-2a9d8f-e9c46a-f4a261-e76f51
+    http://paletton.com/#uid=70a0u0kkNs+b4JOgryLpxqpsbkI
+ */
+@font-face {
+  font-family: 'Converse-js';
+  src: url("../fonticons/fonts/icomoon.eot?wvi0ht");
+  src: url("../fonticons/fonts/icomoon.eot?wvi0ht#iefix") format("embedded-opentype"), url("../fonticons/fonts/icomoon.ttf?wvi0ht") format("truetype"), url("../fonticons/fonts/icomoon.woff?wvi0ht") format("woff"), url("../fonticons/fonts/icomoon.svg?wvi0ht#icomoon") format("svg");
+  font-weight: normal;
+  font-style: normal; }
+.icon-conversejs {
+  padding-right: 0.2em;
+  font-family: 'Converse-js';
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale; }
+
+.icon-conversejs:before {
+  content: "\e600"; }
+
+#converse-embedded-chat .icon-address-book:before,
+#conversejs .icon-address-book:before {
+  content: "\270f"; }
+#converse-embedded-chat .icon-attachment:before,
+#conversejs .icon-attachment:before {
+  content: "\e032"; }
+#converse-embedded-chat .icon-away:before,
+#conversejs .icon-away:before {
+  content: "\25fb"; }
+#converse-embedded-chat .icon-blocked:before,
+#conversejs .icon-blocked:before {
+  content: "\2718"; }
+#converse-embedded-chat .icon-bold:before,
+#conversejs .icon-bold:before {
+  content: "\e04d"; }
+#converse-embedded-chat .icon-bubbles2:before,
+#conversejs .icon-bubbles2:before {
+  content: "\e016"; }
+#converse-embedded-chat .icon-bubbles3:before,
+#conversejs .icon-bubbles3:before {
+  content: "\e017"; }
+#converse-embedded-chat .icon-bubbles:before,
+#conversejs .icon-bubbles:before {
+  content: "\e015"; }
+#converse-embedded-chat .icon-busy:before,
+#conversejs .icon-busy:before {
+  content: "\e004"; }
+#converse-embedded-chat .icon-dnd:before,
+#conversejs .icon-dnd:before {
+  content: "\e004"; }
+#converse-embedded-chat .icon-cancel-circle:before,
+#conversejs .icon-cancel-circle:before {
+  content: "\e058"; }
+#converse-embedded-chat .icon-checkmark:before,
+#conversejs .icon-checkmark:before {
+  content: "\2713"; }
+#converse-embedded-chat .icon-close:before,
+#conversejs .icon-close:before {
+  content: "\2715"; }
+#converse-embedded-chat .icon-closed:before,
+#conversejs .icon-closed:before {
+  content: "\25ba"; }
+#converse-embedded-chat .icon-cog:before,
+#conversejs .icon-cog:before {
+  content: "\e02f"; }
+#converse-embedded-chat .icon-cogs:before,
+#conversejs .icon-cogs:before {
+  content: "\e022"; }
+#converse-embedded-chat .icon-conversejs:before,
+#conversejs .icon-conversejs:before {
+  content: "\e600"; }
+#converse-embedded-chat .icon-database:before,
+#conversejs .icon-database:before {
+  content: "\f1c0"; }
+#converse-embedded-chat .icon-envelope:before,
+#conversejs .icon-envelope:before {
+  content: "\f003"; }
+#converse-embedded-chat .icon-exit:before,
+#conversejs .icon-exit:before {
+  content: "\e601"; }
+#converse-embedded-chat .icon-eye-blocked:before,
+#conversejs .icon-eye-blocked:before {
+  content: "\e031"; }
+#converse-embedded-chat .icon-eye:before,
+#conversejs .icon-eye:before {
+  content: "\e030"; }
+#converse-embedded-chat .icon-github:before,
+#conversejs .icon-github:before {
+  content: "\eab0"; }
+#converse-embedded-chat .icon-globe:before,
+#conversejs .icon-globe:before {
+  content: "\f0ac"; }
+#converse-embedded-chat .icon-google2:before,
+#conversejs .icon-google2:before {
+  content: "\ea89"; }
+#converse-embedded-chat .icon-group:before,
+#conversejs .icon-group:before {
+  content: "\f0c0"; }
+#converse-embedded-chat .icon-happy:before,
+#conversejs .icon-happy:before {
+  content: "\263b"; }
+#converse-embedded-chat .icon-heart2:before,
+#conversejs .icon-heart2:before {
+  content: "\f004"; }
+#converse-embedded-chat .icon-heart:before,
+#conversejs .icon-heart:before {
+  content: "\2764"; }
+#converse-embedded-chat .icon-heart_empty:before,
+#conversejs .icon-heart_empty:before {
+  content: "\f08a"; }
+#converse-embedded-chat .icon-hide-users:before,
+#conversejs .icon-hide-users:before {
+  content: "\e01c"; }
+#converse-embedded-chat .icon-home:before,
+#conversejs .icon-home:before {
+  content: "\e000"; }
+#converse-embedded-chat .icon-idcard-dark:before,
+#conversejs .icon-idcard-dark:before {
+  content: "\f2c2"; }
+#converse-embedded-chat .icon-idcard:before,
+#conversejs .icon-idcard:before {
+  content: "\f2c3"; }
+#converse-embedded-chat .icon-image:before,
+#conversejs .icon-image:before {
+  content: "\2b14"; }
+#converse-embedded-chat .icon-info:before,
+#conversejs .icon-info:before {
+  content: "\2360"; }
+#converse-embedded-chat .icon-italic:before,
+#conversejs .icon-italic:before {
+  content: "\e04f"; }
+#converse-embedded-chat .icon-key:before,
+#conversejs .icon-key:before {
+  content: "\e028"; }
+#converse-embedded-chat .icon-legal:before,
+#conversejs .icon-legal:before {
+  content: "\f0e3"; }
+#converse-embedded-chat .icon-lock-2:before,
+#conversejs .icon-lock-2:before {
+  content: "\e027"; }
+#converse-embedded-chat .icon-minus:before,
+#conversejs .icon-minus:before {
+  content: "\e05a"; }
+#converse-embedded-chat .icon-music:before,
+#conversejs .icon-music:before {
+  content: "\266b"; }
+#converse-embedded-chat .icon-newtab:before,
+#conversejs .icon-newtab:before {
+  content: "\e053"; }
+#converse-embedded-chat .icon-notebook:before,
+#conversejs .icon-notebook:before {
+  content: "\2710"; }
+#converse-embedded-chat .icon-notification:before,
+#conversejs .icon-notification:before {
+  content: "\e01f"; }
+#converse-embedded-chat .icon-offline:before,
+#conversejs .icon-offline:before {
+  content: "\e002"; }
+#converse-embedded-chat .icon-logout:before,
+#conversejs .icon-logout:before {
+  content: "\e002"; }
+#converse-embedded-chat .icon-online:before,
+#conversejs .icon-online:before {
+  content: "\25fc"; }
+#converse-embedded-chat .icon-opened:before,
+#conversejs .icon-opened:before {
+  content: "\25bc"; }
+#converse-embedded-chat .icon-pencil:before,
+#conversejs .icon-pencil:before {
+  content: "\270e"; }
+#converse-embedded-chat .icon-phone-hang-up:before,
+#conversejs .icon-phone-hang-up:before {
+  content: "\260e"; }
+#converse-embedded-chat .icon-phone:before,
+#conversejs .icon-phone:before {
+  content: "\260f"; }
+#converse-embedded-chat .icon-plus:before,
+#conversejs .icon-plus:before {
+  content: "\271a"; }
+#converse-embedded-chat .icon-pushpin:before,
+#conversejs .icon-pushpin:before {
+  content: "\e012"; }
+#converse-embedded-chat .icon-quotes-left:before,
+#conversejs .icon-quotes-left:before {
+  content: "\e01d"; }
+#converse-embedded-chat .icon-reddit:before,
+#conversejs .icon-reddit:before {
+  content: "\eac6"; }
+#converse-embedded-chat .icon-remove:before,
+#conversejs .icon-remove:before {
+  content: "\e02d"; }
+#converse-embedded-chat .icon-room-info:before,
+#conversejs .icon-room-info:before {
+  content: "\e059"; }
+#converse-embedded-chat .icon-save:before,
+#conversejs .icon-save:before {
+  content: "\f0c7"; }
+#converse-embedded-chat .icon-search:before,
+#conversejs .icon-search:before {
+  content: "\e021"; }
+#converse-embedded-chat .icon-show-users:before,
+#conversejs .icon-show-users:before {
+  content: "\e01e"; }
+#converse-embedded-chat .icon-smiley:before,
+#conversejs .icon-smiley:before {
+  content: "\263a"; }
+#converse-embedded-chat .icon-snowflake:before,
+#conversejs .icon-snowflake:before {
+  content: "\f2dc"; }
+#converse-embedded-chat .icon-spell-check:before,
+#conversejs .icon-spell-check:before {
+  content: "\e045"; }
+#converse-embedded-chat .icon-spinner:before,
+#conversejs .icon-spinner:before {
+  content: "\231b"; }
+#converse-embedded-chat .icon-star:before,
+#conversejs .icon-star:before {
+  content: "\f005"; }
+#converse-embedded-chat .icon-star_empty:before,
+#conversejs .icon-star_empty:before {
+  content: "\f006"; }
+#converse-embedded-chat .icon-strikethrough:before,
+#conversejs .icon-strikethrough:before {
+  content: "\e050"; }
+#converse-embedded-chat .icon-twitter:before,
+#conversejs .icon-twitter:before {
+  content: "\ea96"; }
+#converse-embedded-chat .icon-underline:before,
+#conversejs .icon-underline:before {
+  content: "\e04e"; }
+#converse-embedded-chat .icon-unlocked:before,
+#conversejs .icon-unlocked:before {
+  content: "\e025"; }
+#converse-embedded-chat .icon-user:before,
+#conversejs .icon-user:before {
+  content: "\e01a"; }
+#converse-embedded-chat .icon-users:before,
+#conversejs .icon-users:before {
+  content: "\e01b"; }
+#converse-embedded-chat .icon-warning:before,
+#conversejs .icon-warning:before {
+  content: "\26a0"; }
+#converse-embedded-chat .icon-wrench:before,
+#conversejs .icon-wrench:before {
+  content: "\e024"; }
+#converse-embedded-chat .icon-xa:before,
+#conversejs .icon-xa:before {
+  content: "\e602"; }
+#converse-embedded-chat .icon-zoomin:before,
+#conversejs .icon-zoomin:before {
+  content: "\e02b"; }
+#converse-embedded-chat .icon-zoomout:before,
+#conversejs .icon-zoomout:before {
+  content: "\e02a"; }
+#converse-embedded-chat [data-icon]:before,
+#conversejs [data-icon]:before {
+  content: attr(data-icon);
+  font-family: 'Converse-js';
+  font-variant: normal;
+  font-weight: normal;
+  line-height: 1;
+  speak: none;
+  text-transform: none;
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale; }
+#converse-embedded-chat [class^="icon-"]:before, #converse-embedded-chat [class*=" icon-"]:before,
+#conversejs [class^="icon-"]:before,
+#conversejs [class*=" icon-"]:before {
+  background-position: 14px 14px;
+  background-image: none;
+  font-family: 'Converse-js';
+  font-style: normal;
+  font-variant: normal;
+  font-weight: normal;
+  width: auto;
+  height: auto;
+  line-height: 1;
+  speak: none;
+  text-transform: none;
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale; }
+
+/*# sourceMappingURL=fonts.css.map */

+ 35 - 19
css/inverse.css

@@ -4758,6 +4758,8 @@
 #converse-embedded-chat,
 #conversejs {
   margin-left: -0.5em;
+  padding-left: env(safe-area-inset-left);
+  padding-right: env(safe-area-inset-right);
   bottom: 0;
   height: auto;
   width: 100vw;
@@ -5306,6 +5308,9 @@ body {
     #conversejs .chatbox {
       margin: 0;
       width: 100%; } }
+  #converse-embedded-chat .chatbox .spoiler,
+  #conversejs .chatbox .spoiler {
+    background-color: #e7f7ee; }
   #converse-embedded-chat .chatbox .box-flyout,
   #conversejs .chatbox .box-flyout {
     background-color: white;
@@ -5384,6 +5389,8 @@ body {
         margin-top: 1em; }
     #converse-embedded-chat .chatbox .chat-body .chat-image,
     #conversejs .chatbox .chat-body .chat-image {
+      height: auto;
+      width: auto;
       max-height: 24em;
       max-width: 100%; }
     #converse-embedded-chat .chatbox .chat-body .chat-action,
@@ -5414,6 +5421,10 @@ body {
       #conversejs .chatbox .chat-body .chat-message .chat-msg-content {
         max-width: 100%;
         word-wrap: break-word; }
+        #converse-embedded-chat .chatbox .chat-body .chat-message .chat-msg-content.spoiler,
+        #conversejs .chatbox .chat-body .chat-message .chat-msg-content.spoiler {
+          border-radius: 4px;
+          padding: 0.5em; }
         #converse-embedded-chat .chatbox .chat-body .chat-message .chat-msg-content .emojione,
         #conversejs .chatbox .chat-body .chat-message .chat-msg-content .emojione {
           margin-bottom: -6px; }
@@ -5447,6 +5458,10 @@ body {
     line-height: 1.3em;
     height: 206px;
     height: calc(100% - 100px); }
+    #converse-embedded-chat .chatbox .chat-content .toggle-spoiler:before,
+    #conversejs .chatbox .chat-content .toggle-spoiler:before {
+      padding-right: 0.25em;
+      whitespace: nowrap; }
   #converse-embedded-chat .chatbox .chat-content-sendbutton,
   #conversejs .chatbox .chat-content-sendbutton {
     height: calc(100% - 132px); }
@@ -5480,6 +5495,9 @@ body {
       #converse-embedded-chat .chatbox .sendXMPPMessage,
       #conversejs .chatbox .sendXMPPMessage {
         width: 100%; } }
+    #converse-embedded-chat .chatbox .sendXMPPMessage .spoiler-hint,
+    #conversejs .chatbox .sendXMPPMessage .spoiler-hint {
+      width: 100%; }
     #converse-embedded-chat .chatbox .sendXMPPMessage .chat-textarea,
     #conversejs .chatbox .sendXMPPMessage .chat-textarea {
       border-top-left-radius: 0;
@@ -5491,6 +5509,9 @@ body {
       width: 100%;
       border: none;
       resize: none; }
+      #converse-embedded-chat .chatbox .sendXMPPMessage .chat-textarea.spoiler,
+      #conversejs .chatbox .sendXMPPMessage .chat-textarea.spoiler {
+        height: 42px; }
     #converse-embedded-chat .chatbox .sendXMPPMessage .send-button,
     #conversejs .chatbox .sendXMPPMessage .send-button {
       position: absolute;
@@ -5506,20 +5527,17 @@ body {
     #conversejs .chatbox .sendXMPPMessage .chat-toolbar {
       box-sizing: border-box;
       margin: 0;
-      padding: 5px;
+      padding: 0.25em;
       height: 29px;
       display: block;
-      background-color: #E7FBF0; }
+      background-color: #50c282;
+      color: white; }
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar a,
       #conversejs .chatbox .sendXMPPMessage .chat-toolbar a {
-        font-size: 16px;
-        color: #777;
+        color: white;
+        font-size: 18px;
         text-decoration: none;
         text-shadow: none; }
-      #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .chat-toolbar-text,
-      #conversejs .chatbox .sendXMPPMessage .chat-toolbar .chat-toolbar-text {
-        font-size: 12px;
-        padding-right: 3px; }
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .unencrypted a,
       #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar .unencrypted,
       #conversejs .chatbox .sendXMPPMessage .chat-toolbar .unencrypted a,
@@ -5610,15 +5628,12 @@ body {
               color: #8f2831; }
         #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu,
         #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu {
-          color: #777; }
-        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley,
-        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley {
-          padding-left: 5px; }
-          #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
-          #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover,
-          #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
-          #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover {
-            background-color: #DCF9F6; }
+          color: white; }
+        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
+        #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover,
+        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover,
+        #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover {
+          background-color: #DCF9F6; }
         #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul,
         #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul {
           z-index: 99; }
@@ -6036,7 +6051,8 @@ body {
     padding: 1px;
     float: right; }
   #conversejs #controlbox .controlbox-panes {
-    overflow-y: scroll; }
+    height: 100%;
+    overflow-y: auto; }
   #conversejs #controlbox .controlbox-pane {
     padding: 1.2em;
     background-color: white;
@@ -6602,7 +6618,7 @@ body {
           background-color: #E77051; }
   #converse-embedded-chat .chatroom .sendXMPPMessage .chat-toolbar,
   #conversejs .chatroom .sendXMPPMessage .chat-toolbar {
-    background-color: #FFECE7; }
+    background-color: #ed957e; }
   #converse-embedded-chat .chatroom .sendXMPPMessage .chat-textarea,
   #conversejs .chatroom .sendXMPPMessage .chat-textarea {
     border-bottom-right-radius: 0; }

+ 4 - 2
css/mobile.css

@@ -1,11 +1,13 @@
 #conversejs {
     left: 0px;
     right: 0px;
+    padding-left: env(safe-area-inset-left);
+    padding-right: env(safe-area-inset-right);
 }
 .converse-chatroom {
     font-size: 14px;
 }
 .intro {
-  padding: 0;
-  height: 100vh;
+    padding: 0;
+    height: 100vh;
 }

+ 8 - 1
css/theme.css

@@ -285,7 +285,7 @@ a:focus {
   font-size: 26px;
 }
 .banner-social-buttons {
-    padding-top: 7em;
+    padding-top: 5em;
 }
 ::-moz-selection {
   text-shadow: none;
@@ -357,6 +357,13 @@ ul.features {
     margin-left: 0;
     margin-right: 0;
 }
+
+.mastodon {
+    width: 4em;
+    height: 4em;
+    margin-top: 0.9em;
+}
+
 .sponsors {
     clear: both;
     font-size: 1.4em;

+ 2 - 2
demo/anonymous.html

@@ -16,7 +16,7 @@
     <noscript><p><img src="//stats.opkode.com/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
     <![if gte IE 9]>
         <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/3.2.1/css/converse.min.css" />
-	<script src="https://cdn.conversejs.org/3.2.1/dist/converse.min.js"></script>
+        <script src="https://cdn.conversejs.org/3.2.1/dist/converse.min.js"></script>
     <![endif]>
 </head>
 
@@ -40,7 +40,7 @@
             <div class="container">
                 <div class="row">
                     <div class="col-md-8 col-md-offset-2">
-                        <h1 class="brand-heading"><i class="icon-conversejs"></i>Converse.js</h1>
+                        <h1 class="brand-heading"><i class="icon-conversejs"></i>Converse</h1>
                         <p class="intro-text">Anonymous login demo</p>
                     </div>
                 </div>

+ 13 - 38
demo/embedded.html

@@ -5,19 +5,19 @@
     <meta charset="utf-8">
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <meta name="description" content="Converse.js: A free chat client for your website" />
+    <meta name="description" content="Converse.js: An XMPP chat client which can be integrated into any website" />
     <meta name="author" content="JC Brand" />
     <meta name="keywords" content="xmpp chat webchat converse.js" />
     <link rel="shortcut icon" type="image/ico" href="../css/images/favicon.ico"/>
-    <link type="text/css" rel="stylesheet" media="screen" href="../node_modules/bootstrap/dist/css/bootstrap.min.css" />
-    <link type="text/css" rel="stylesheet" media="screen" href="../node_modules/font-awesome/css/font-awesome.min.css" />
+    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/node_modules/bootstrap/dist/css/bootstrap.min.css" />
+    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/node_modules/font-awesome/css/font-awesome.min.css" />
     <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/theme.min.css" />
     <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/converse.min.css" />
     <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/converse-muc-embedded.min.css" />
-    <script type="text/javascript" src="../analytics.js"></script>
+    <script type="text/javascript" src="https://cdn.conversejs.org/analytics.js"></script>
     <noscript><p><img src="//stats.opkode.com/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
-    <![if gte IE 9]>
-	<script src="../dist/converse-muc-embedded.min.js"></script>
+    <![if gte IE 11]>
+        <script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
     <![endif]>
 </head>
 
@@ -41,10 +41,10 @@
             <div class="container">
                 <div class="row">
                     <div class="col-md-8 col-md-offset-2">
-                        <h1 class="brand-heading brand-heading-embedded"><a style="color: white;" href="/"><i class="icon-conversejs"></i>Converse.js</a></h1>
+                        <h1 class="brand-heading brand-heading-embedded"><a style="color: white;" href="/"><i class="icon-conversejs"></i>Converse</a></h1>
                         <p class="intro-text">Embedded MUC chat demo</p>
 
-                        <div id="converse-embedded-chat"></div>
+                        <div id="conversejs"></div>
                     </div>
                 </div>
             </div>
@@ -54,44 +54,19 @@
 
 <script>
     converse.initialize({
-        allow_logout: false, // No point in logging out when we have auto_login as true.
-        allow_muc_invitations: false, // Doesn't make sense to allow because only
-                                        // roster contacts can be invited
-        allow_contact_requests: false, // Contacts from other servers cannot,
-                                        // be added and anonymous users don't
-                                        // know one another's JIDs, so disabling.
-        auto_reconnect: true,
         authentication: 'anonymous',
         auto_login: true,
         auto_join_rooms: [
             'anonymous@conference.nomnom.im',
         ],
-        // Whitelist non-core plugins that we need
-        whitelisted_plugins: ['converse-muc-embedded'],
-        // Blacklist plugins which aren't included in the build file, 
-        // so that other code cannot register their own plugins under
-        // those names.
-        blacklisted_plugins: [
-            "converse-bookmarks",
-            "converse-controlbox",
-            "converse-dragresize",
-            "converse-headline",
-            "converse-minimize",
-            "converse-otr",
-            "converse-register",
-            "converse-vcard",
-        ],
+        bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
+        jid: 'nomnom.im', // XMPP server which allows anonymous login (doesn't
+                          // allow chatting with other XMPP servers).
         notify_all_room_messages: [
             'anonymous@conference.nomnom.im',
         ],
-        bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes
-        jid: 'nomnom.im', // XMPP server which allows anonymous login (doesn't
-                            // allow chatting with other XMPP servers).
-        keepalive: true,
-        hide_muc_server: true, // Federation is disabled, so no use in
-                                // showing the MUC server.
-        play_sounds: true,
-        strict_plugin_dependencies: false
+        locales_url: "../locale/{{{locale}}}/LC_MESSAGES/converse.json",
+        view_mode: 'embedded',
     });
 </script>
 </html>

+ 2 - 2
demo/index.html

@@ -16,7 +16,7 @@
     <script type="text/javascript" src="analytics.js"></script>
     <noscript><p><img src="//stats.opkode.com/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
     <![if gte IE 9]>
-	<script src="/dist/converse.min.js"></script>
+        <script src="/dist/converse.min.js"></script>
     <![endif]>
 </head>
 
@@ -67,7 +67,7 @@
 
     <section class="intro" class="container">
         <div class="row">
-            <h1 class="brand-heading"><i class="icon-conversejs"></i> Converse.js</h1>
+            <h1 class="brand-heading"><i class="icon-conversejs"></i> Converse</h1>
             <div class="col-md-8 col-md-offset-2">
                 <p class="intro-text">Demos:</p>
                 <p class="intro-text">

+ 2 - 2
demo/without_bundled_dependencies.html

@@ -51,7 +51,7 @@
     <script type="text/javascript" src="../node_modules/moment/min/moment-with-locales.js"></script>
 
     <script type="text/javascript" src="../3rdparty/lodash.fp.js"></script>
-	<script src="../dist/converse-no-dependencies.js"></script>
+    <script src="../dist/converse-no-dependencies.js"></script>
 </head>
 <body id="page-top" data-spy="scroll" data-target=".navbar-custom">
     <section class="intro">
@@ -59,7 +59,7 @@
             <div class="container">
                 <div class="row">
                     <div class="col-md-8 col-md-offset-2">
-                        <h1 class="brand-heading"><i class="icon-conversejs"></i>Converse.js</h1>
+                        <h1 class="brand-heading"><i class="icon-conversejs"></i>Converse</h1>
                         <p class="intro-text">An example page where external dependencies are loaded seperately and not within the converse.js bundle.</p>
                         <p class="intro-text">Look at the page source for details.</p>
                         <p class="intro-text">For this page to work, you'll need to

+ 2 - 2
dev.html

@@ -65,8 +65,8 @@
                 'discuss@conference.conversejs.org'
             ],
             auto_reconnect: true,
-            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',
             show_controlbox_by_default: true,
             strict_plugin_dependencies: false,

Datei-Diff unterdrückt, da er zu groß ist
+ 22764 - 0
dist/converse-muc-embedded.js


Datei-Diff unterdrückt, da er zu groß ist
+ 329 - 278
dist/converse-no-dependencies.js


Datei-Diff unterdrückt, da er zu groß ist
+ 1763 - 693
dist/converse.js


+ 1 - 1
docs/source/_static/style.css

@@ -47,7 +47,7 @@ h5 {
 }
 .bs-sidenav .nav .nav .nav > li > a {
     font-size: 90%;
-	color: gray;
+    color: gray;
 }
 
 /*

+ 2 - 2
docs/source/conf.py

@@ -48,9 +48,9 @@ copyright = u'2017, JC Brand'
 # built documents.
 #
 # The short X.Y version.
-version = '3.3.1'
+version = '3.3.3'
 # The full version, including alpha/beta/rc tags.
-release = '3.3.1'
+release = '3.3.3'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

+ 97 - 31
docs/source/configuration.rst

@@ -6,7 +6,7 @@
 Configuration
 =============
 
-The included minified JS and CSS files can be used for demoing or testing, but
+The included minified JavaScript and CSS files can be used for demoing or testing, but
 you'll want to configure *Converse.js* to suit your needs before you deploy it
 on your website.
 
@@ -19,7 +19,7 @@ Please refer to the `Configuration settings`_ section below for info on
 all the available configuration settings.
 
 After you have configured *Converse.js*, you'll have to regenerate the minified
-JS file so that it will include the new settings. Please refer to the
+JavaScript file so that it will include the new settings. Please refer to the
 :ref:`minification` section for more info on how to do this.
 
 .. _`configuration-settings`:
@@ -118,6 +118,8 @@ allow_bookmarks
 Enables/disables chatroom bookmarks functionality.
 This setting is only applicable if the ``converse-bookmarks`` plugin is loaded.
 
+See also: `allow_public_bookmarks`_
+
 allow_chat_pending_contacts
 ---------------------------
 
@@ -131,7 +133,7 @@ allow_contact_removal
 * Default:  ``true``
 
 Allow the user to remove roster contacts by clicking on the delete icon
-(i.e. traschcan) next to a contact's name in the roster.
+(i.e. trashcan) next to a contact's name in the roster.
 
 allow_contact_requests
 ----------------------
@@ -149,7 +151,7 @@ allow_dragresize
 * Default: ``true``
 
 Allow users to resize chats by dragging the edges. The min-height and min-width
-CSS properties set on a chat boxes (specifically on the ``#converse.js .chatbox > .box-flyout`` element)
+CSS properties set on a chatboxes (specifically on the ``#converse.js .chatbox > .box-flyout`` element)
 will be honored, IF they are set in pixels.
 
 allow_muc
@@ -165,9 +167,9 @@ allow_muc_invitations
 
 * Default:  ``true``
 
-Allows users to be invited to join MUC chat rooms. An "Invite" widget will
-appear in the sidebar of the chat room where you can type in the JID of a user
-to invite into the chat room.
+Allows users to be invited to join MUC chatrooms. An "Invite" widget will
+appear in the sidebar of the chatroom where you can type in the JID of a user
+to invite into the chatroom.
 
 .. _`allow_non_roster_messaging`:
 
@@ -190,6 +192,24 @@ allow_otr
 
 Allow Off-the-record encryption of single-user chat messages.
 
+allow_public_bookmarks
+----------------------
+
+* Default: ``false``
+
+Some XMPP servers don't support private PEP/PubSub nodes, as required for
+private bookmarks and outlined in `XEP-0223 <https://xmpp.org/extensions/xep-0223.html>`_.
+
+Even though Converse.js asks for the bookmarks to be kept private (via the
+`<publish-options>` XML node), the server simply ignores the privacy settings
+and publishes the node contents under the default privacy setting, which makes
+the information available to all roster contacts.
+
+If your your XMPP server does not support `XEP-0223`'s ``#publish-options``
+feature and you don't mind that your room bookmarks are visible to all
+contacts, then you can set this setting to ``true``. Otherwise you won't be
+able to have any room bookmarks at all for an account on that XMPP server.
+
 allow_registration
 ------------------
 
@@ -204,7 +224,7 @@ animate
 
 * Default:  ``true``
 
-Show animations, for example when opening and closing chat boxes.
+Show animations, for example when opening and closing chatboxes.
 
 archived_messages_page_size
 ---------------------------
@@ -217,10 +237,10 @@ This feature applies to `XEP-0313: Message Archive Management (MAM) <https://xmp
 and will only take effect if your server supports MAM.
 
 It allows you to specify the maximum amount of archived messages to be returned per query.
-When you open a chat box or room, archived messages will be displayed (if
+When you open a chatbox or room, archived messages will be displayed (if
 available) and the amount returned will be no more than the page size.
 
-You will be able to query for even older messages by scrolling upwards in the chat box or room
+You will be able to query for even older messages by scrolling upwards in the chatbox or room
 (the so-called infinite scrolling pattern).
 
 auto_list_rooms
@@ -231,7 +251,7 @@ auto_list_rooms
 If true, and the XMPP server on which the current user is logged in supports
 multi-user chat, then a list of rooms on that server will be fetched.
 
-Not recommended for servers with lots of chat rooms.
+Not recommended for servers with lots of chatrooms.
 
 For each room on the server a query is made to fetch further details (e.g.
 features, number of occupants etc.), so on servers with many rooms this
@@ -921,15 +941,15 @@ muc_history_max_stanzas
 * Default:  ``undefined``
 
 This option allows you to specify the maximum amount of messages to be shown in a
-chat room when you enter it. By default, the amount specified in the room
+chatroom when you enter it. By default, the amount specified in the room
 configuration or determined by the server will be returned.
 
 Please note, this option is not related to
 `XEP-0313 Message Archive Management <https://xmpp.org/extensions/xep-0313.html>`_,
-which also allows you to show archived chat room messages, but follows a
+which also allows you to show archived chatroom messages, but follows a
 different approach.
 
-If you're using MAM for archiving chat room messages, you might want to set
+If you're using MAM for archiving chatroom messages, you might want to set
 this option to zero.
 
 muc_instant_rooms
@@ -951,9 +971,9 @@ muc_nickname_from_jid
 * Default: ``false``
 
 When set to ``true``, then users will not be prompted to provide nicknames for
-chat rooms. Instead, the node part of a user's JID (i.e. JID = node@domain/resource)
+chatrooms. Instead, the node part of a user's JID (i.e. JID = node@domain/resource)
 will be used. If the user's nickname is already taken by another user in the
-chat room, then an integer will be added to make it unique.
+chatroom, then an integer will be added to make it unique.
 
 So, for example, if john@example.com joins a chatroom, his nickname will
 automatically be "john". If now john@differentdomain.com tries to join the
@@ -965,7 +985,7 @@ muc_show_join_leave
 
 * Default; ``true``
 
-Determines whether Converse.js will show info messages inside a chat room
+Determines whether Converse.js will show info messages inside a chatroom
 whenever a user joins or leaves it.
 
 notify_all_room_messages
@@ -1008,7 +1028,7 @@ play_sounds
 * Default:  ``false``
 
 Plays a notification sound when you receive a personal message or when your
-nickname is mentioned in a chat room.
+nickname is mentioned in a chatroom.
 
 Inside the ``./sounds`` directory of the Converse.js repo you'll see MP3 and Ogg
 formatted sound files. We need both, because neither format is supported by all browsers.
@@ -1071,6 +1091,35 @@ providers_link
 The hyperlink on the registration form which points to a directory of public
 XMPP servers.
 
+root
+----
+
+* Default: ``window.document``
+
+When using converse.js inside a web component's shadow DOM, you will need to set this settings'
+value to the shadow-root of the shadow DOM.
+
+For example:
+
+.. code-block:: javascript
+
+  class CustomChatComponent extends HTMLElement {
+    constructor() {
+      super();
+      const shadowRoot  = this.attachShadow({mode: "open"});
+      this.initConverse(shadowRoot);
+    }
+
+    initConverse(shadowRoot) {
+        window.addEventListener("converse-loaded", function(event) {
+            converse.initialize({
+                root: shadowRoot,
+                // Other settings go here...
+            });
+        });
+      }
+    }
+
 
 roster_groups
 -------------
@@ -1290,7 +1339,7 @@ use_otr_by_default
 * Default:  ``false``
 
 If set to ``true``, Converse.js will automatically try to initiate an OTR (off-the-record)
-encrypted chat session every time you open a chat box.
+encrypted chat session every time you open a chatbox.
 
 use_vcards
 ----------
@@ -1315,7 +1364,7 @@ visible_toolbar_buttons
         toggle_occupants: true
     }
 
-Allows you to show or hide buttons on the chat boxes' toolbars.
+Allows you to show or hide buttons on the chatboxes' toolbars.
 
 * *call*:
     Provides a button with a picture of a telephone on it.
@@ -1329,11 +1378,11 @@ Allows you to show or hide buttons on the chat boxes' toolbars.
             // ... Third-party library code ...
         });
 * *clear*:
-    Provides a button for clearing messages from a chat box.
+    Provides a button for clearing messages from a chatbox.
 * *emoji*:
     Enables rendering of emoji and provides a toolbar button for choosing them.
 * *toggle_occupants*:
-    Shows a button for toggling (i.e. showing/hiding) the list of occupants in a chat room.
+    Shows a button for toggling (i.e. showing/hiding) the list of occupants in a chatroom.
 
 .. _`websocket-url`:
 
@@ -1368,20 +1417,21 @@ view_mode
 ---------
 
 * Default: ``overlayed``
-* Allowed values: ``overlayed``, ``fullscreen``, ``mobile``
+* Allowed values: ``overlayed``, ``fullscreen``, ``mobile``, ``embedded``
 
 The ``view_mode`` setting configures converse.js's mode and resulting behavior.
 
 Before the introduction of this setting (in version 3.3.0), there were there
-different builds, each for the diffent modes.
+different builds, each for the different modes.
 
 These were:
 
-* ``converse.js`` for the ``overlayed`` mode
 * ``converse-mobile.js`` for the ``mobile`` mode
+* ``converse-muc-embedded.js`` for embedding a single MUC room into the page.
+* ``converse.js`` for the ``overlayed`` mode
 * ``inverse.js`` for the ``fullscreen`` mode
 
-Besides having three different builds, certain plugins had to be whitelisted
+Besides having different builds, certain plugins had to be whitelisted
 and blacklisted for the different modes.
 
 ``converse-singleton`` had to be whitelisted for the ``mobile`` and ``fullscreen``
@@ -1391,11 +1441,25 @@ modes, additionally ``converse-inverse`` had to be whitelisted for the
 For both those modes the ``converse-minimize`` and ``converse-dragresize``
 plugins had to be blacklisted.
 
-Since version 3.3.0, the last two builds no longer exist, and instead the
-standard ``converse.js`` build is used, together with the appropraite
-``view_mode`` value.
+When using ``converse-muc-embedded.js`` various plugins also had to manually be
+blacklisted.
+
+Since version 3.3.0 it's no longer necessary to blacklist any plugins (except
+for ``converse-muc-embedded.js``, which is from version 3.3.3).
+
+Blacklisting now happens automatically.
+
+Since version 3.3.0, the ``inverse.js`` and ``converse-mobile.js`` builds no
+longer exist. Instead the standard ``converse.js`` build is used, together with
+the appropriate ``view_mode`` value.
+
+The ``converse-muc-embedded.js`` build is still kept, because it's smaller than
+``converse.js`` due to unused code being removed. It doesn't however contain
+any new code, so the full ``converse.js`` build could be used instead, as long
+as ``view_mode`` is set to ``embedded``.
 
-Furthermore, it's no longer necessary to whitelist or blacklist any plugins.
+Furthermore, it's no longer necessary to whitelist or blacklist any plugins
+when switching view modes.
 
 .. note::
     Although the ``view_mode`` setting has removed the need for different
@@ -1499,7 +1563,9 @@ xhr_user_search
 There are two ways to add users.
 
 * The user inputs a valid JID (Jabber ID), and the user is added as a pending contact.
-* The user inputs some text (for example part of a firstname or lastname), an XHR (Ajax Request) will be made to a remote server, and a list of matches are returned. The user can then choose one of the matches to add as a contact.
+* The user inputs some text (for example part of a first name or last name),
+  an XHR (Ajax Request) will be made to a remote server, and a list of matches are returned.
+  The user can then choose one of the matches to add as a contact.
 
 This setting enables the second mechanism, otherwise by default the first will be used.
 

+ 58 - 25
docs/source/developer_api.rst

@@ -437,6 +437,39 @@ The **disco** grouping
 This grouping collects API functions related to `service discovery
 <https://xmpp.org/extensions/xep-0030.html>`_.
 
+getIdentity
+~~~~~~~~~~~
+
+Paramters:
+
+* (String) category
+* (String) type
+* (String) entity JID
+
+Get the identity (with the given category and type) for a given disco entity.
+
+For example, when determining support for PEP (personal eventing protocol), you
+want to know whether the user's own JID has an identity with
+``category='pubsub'`` and ``type='pep'`` as explained in this section of
+XEP-0163: https://xmpp.org/extensions/xep-0163.html#support
+
+.. code-block:: javascript
+
+    converse.plugins.add('myplugin', {
+        initialize: function () {
+
+            _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then(
+                function (identity) {
+                    if (_.isNil(identity)) {
+                        // The entity DOES NOT have this identity
+                    } else {
+                        // The entity DOES have this identity
+                    }
+                }
+            ).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+        }
+    });
+
 supports
 ~~~~~~~~
 
@@ -450,7 +483,7 @@ Returns a `Promise` which, when resolved, returns a map/object with keys
     converse.plugins.add('myplugin', {
         initialize: function () {
 
-            _converse.api.disco.supports(_converse.bare_jid, Strophe.NS.MAM).then(
+            _converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(
                 function (value) {
                     // `value` is a map with two keys, `supported` and `feature`.
 
@@ -718,35 +751,35 @@ You may also provide the fullname. If not present, we use the jid as fullname:
 The **chats** grouping
 ----------------------
 
-Note, for MUC chat rooms, you need to use the "rooms" grouping instead.
+Note, for MUC chatrooms, you need to use the "rooms" grouping instead.
 
 get
 ~~~
 
-Returns an object representing a chat box. The chat box should already be open.
+Returns an object representing a chatbox. The chatbox should already be open.
 
-To return a single chat box, provide the JID of the contact you're chatting
-with in that chat box:
+To return a single chatbox, provide the JID of the contact you're chatting
+with in that chatbox:
 
 .. code-block:: javascript
 
     _converse.api.chats.get('buddy@example.com')
 
-To return an array of chat boxes, provide an array of JIDs:
+To return an array of chatboxes, provide an array of JIDs:
 
 .. code-block:: javascript
 
     _converse.api.chats.get(['buddy1@example.com', 'buddy2@example.com'])
 
-To return all open chat boxes, call the method without any JIDs::
+To return all open chatboxes, call the method without any JIDs::
 
     _converse.api.chats.get()
 
 open
 ~~~~
 
-Opens a chat box and returns a `Backbone.View <http://backbonejs.org/#View>`_ object
-representing a chat box.
+Opens a chatbox and returns a `Backbone.View <http://backbonejs.org/#View>`_ object
+representing a chatbox.
 
 Note that converse doesn't allow opening chats with users who aren't in your roster
 (unless you have set :ref:`allow_non_roster_messaging` to ``true``).
@@ -754,13 +787,13 @@ Note that converse doesn't allow opening chats with users who aren't in your ros
 Before opening a chat, you should first wait until the roster has been populated.
 This is the :ref:`rosterContactsFetched` event/promise.
 
-Besides that, it's a good idea to also first wait until already opened chat boxes
+Besides that, it's a good idea to also first wait until already opened chatboxes
 (which are cached in sessionStorage) have also been fetched from the cache.
 This is the :ref:`chatBoxesFetched` event/promise.
 
 These two events fire only once per session, so they're also available as promises.
 
-So, to open a single chat box:
+So, to open a single chatbox:
 
 .. code-block:: javascript
 
@@ -777,7 +810,7 @@ So, to open a single chat box:
       }
     });
 
-To return an array of chat boxes, provide an array of JIDs:
+To return an array of chatboxes, provide an array of JIDs:
 
 .. code-block:: javascript
 
@@ -795,14 +828,14 @@ To return an array of chat boxes, provide an array of JIDs:
     });
 
 
-*The returned chat box object contains the following methods:*
+*The returned chatbox object contains the following methods:*
 
 +-------------------+------------------------------------------+
 | Method            | Description                              |
 +===================+==========================================+
-| close             | Close the chat box.                      |
+| close             | Close the chatbox.                      |
 +-------------------+------------------------------------------+
-| focus             | Focuses the chat box textarea            |
+| focus             | Focuses the chatbox textarea            |
 +-------------------+------------------------------------------+
 | model.endOTR      | End an OTR (Off-the-record) session.     |
 +-------------------+------------------------------------------+
@@ -810,13 +843,13 @@ To return an array of chat boxes, provide an array of JIDs:
 +-------------------+------------------------------------------+
 | model.initiateOTR | Start an OTR (off-the-record) session.   |
 +-------------------+------------------------------------------+
-| model.maximize    | Minimize the chat box.                   |
+| model.maximize    | Minimize the chatbox.                   |
 +-------------------+------------------------------------------+
-| model.minimize    | Maximize the chat box.                   |
+| model.minimize    | Maximize the chatbox.                   |
 +-------------------+------------------------------------------+
 | model.set         | Set an attribute (i.e. mutator).         |
 +-------------------+------------------------------------------+
-| show              | Opens/shows the chat box.                |
+| show              | Opens/shows the chatbox.                |
 +-------------------+------------------------------------------+
 
 *The get and set methods can be used to retrieve and change the following attributes:*
@@ -824,9 +857,9 @@ To return an array of chat boxes, provide an array of JIDs:
 +-------------+-----------------------------------------------------+
 | Attribute   | Description                                         |
 +=============+=====================================================+
-| height      | The height of the chat box.                         |
+| height      | The height of the chatbox.                         |
 +-------------+-----------------------------------------------------+
-| url         | The URL of the chat box heading.                    |
+| url         | The URL of the chatbox heading.                    |
 +-------------+-----------------------------------------------------+
 
 .. _`listen-grouping`:
@@ -896,7 +929,7 @@ The **rooms** grouping
 get
 ~~~
 
-Returns an object representing a multi user chat box (room).
+Returns an object representing a multi user chatbox (room).
 It takes 3 parameters:
 
 * the room JID (if not specified, all rooms will be returned).
@@ -928,7 +961,7 @@ It takes 3 parameters:
 open
 ~~~~
 
-Opens a multi user chat box and returns an object representing it.
+Opens a multi user chatbox and returns an object representing it.
 Similar to the ``chats.get`` API.
 
 It takes 2 parameters:
@@ -937,7 +970,7 @@ It takes 2 parameters:
 * A map (object) containing any extra room attributes. For example, if you want
   to specify the nickname, use ``{'nick': 'bloodninja'}``.
 
-To open a single multi user chat box, provide the JID of the room:
+To open a single multi user chatbox, provide the JID of the room:
 
 .. code-block:: javascript
 
@@ -1023,8 +1056,8 @@ For example, opening a room with a specific default configuration:
 close
 ~~~~~
 
-Lets you close open chat rooms. You can call this method without any arguments
-to close all open chat rooms, or you can specify a single JID or an array of
+Lets you close open chatrooms. You can call this method without any arguments
+to close all open chatrooms, or you can specify a single JID or an array of
 JIDs.
 
 .. _`promises-grouping`:

+ 13 - 13
docs/source/events.rst

@@ -50,7 +50,7 @@ Here follows the different events that are emitted:
 afterMessagesFetched
 ~~~~~~~~~~~~~~~~~~~~
 
-Emitted whenever a chat box has fetched its messages from ``sessionStorage`` and
+Emitted whenever a chatbox has fetched its messages from ``sessionStorage`` and
 **NOT** from the server.
 
 This event is listened to by the ``converse-mam`` plugin to know when it can
@@ -83,7 +83,7 @@ See also the `roster`_ event further down.
 callButtonClicked
 ~~~~~~~~~~~~~~~~~
 
-When a call button (i.e. with class .toggle-call) on a chat box has been clicked.
+When a call button (i.e. with class .toggle-call) on a chatbox has been clicked.
 
 ``_converse.on('callButtonClicked', function (connection, model) { ... });``
 
@@ -92,10 +92,10 @@ When a call button (i.e. with class .toggle-call) on a chat box has been clicked
 chatBoxesFetched
 ~~~~~~~~~~~~~~~~
 
-Any open chat boxes (from this current session) has been retrieved from the local cache (`sessionStorage`).
+Any open chatboxes (from this current session) has been retrieved from the local cache (`sessionStorage`).
 
 You should wait for this event or promise before attempting to do things
-related to open chat boxes.
+related to open chatboxes.
 
 ``_converse.on('chatBoxesFetched', function (items) { ... });``
 
@@ -110,42 +110,42 @@ Also available as an `ES2015 Promise <http://es6-features.org/#PromiseUsage>`_:
 chatBoxInitialized
 ~~~~~~~~~~~~~~~~~~
 
-When a chat box has been initialized. Relevant to converse-chatview.js plugin.
+When a chatbox has been initialized. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatBoxInitialized', function (chatbox) { ... });``
 
 chatBoxOpened
 ~~~~~~~~~~~~~
 
-When a chat box has been opened. Relevant to converse-chatview.js plugin.
+When a chatbox has been opened. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatBoxOpened', function (chatbox) { ... });``
 
 chatRoomOpened
 ~~~~~~~~~~~~~~
 
-When a chat room has been opened. Relevant to converse-chatview.js plugin.
+When a chatroom has been opened. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatRoomOpened', function (chatbox) { ... });``
 
 chatBoxClosed
 ~~~~~~~~~~~~~
 
-When a chat box has been closed. Relevant to converse-chatview.js plugin.
+When a chatbox has been closed. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatBoxClosed', function (chatbox) { ... });``
 
 chatBoxFocused
 ~~~~~~~~~~~~~~
 
-When the focus has been moved to a chat box. Relevant to converse-chatview.js plugin.
+When the focus has been moved to a chatbox. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatBoxFocused', function (chatbox) { ... });``
 
 chatBoxToggled
 ~~~~~~~~~~~~~~
 
-When a chat box has been minimized or maximized. Relevant to converse-chatview.js plugin.
+When a chatbox has been minimized or maximized. Relevant to converse-chatview.js plugin.
 
 ``_converse.on('chatBoxToggled', function (chatbox) { ... });``
 
@@ -239,15 +239,15 @@ The user has logged out.
 messageAdded
 ~~~~~~~~~~~~
 
-Once a message has been added to a chat box. The passed in data object contains
-a `chatbox` attribute, referring to the chat box receiving the message, as well
+Once a message has been added to a chatbox. The passed in data object contains
+a `chatbox` attribute, referring to the chatbox receiving the message, as well
 as a `message` attribute which refers to the Message model.
 
 .. code-block:: javascript
 
     _converse.on('messageAdded', function (data) {
         // The message is at `data.message`
-        // The original chat box is at `data.chatbox`.
+        // The original chatbox is at `data.chatbox`.
     });
 
 messageSend

+ 6 - 6
docs/source/features.rst

@@ -69,17 +69,17 @@ Languages increase the size of the Converse.js significantly.
 If you only need one, or a subset of the available languages, it's better to
 make a custom build which includes only those languages that you need.
 
-Moderating chat rooms
-=====================
+Moderating chatrooms
+====================
 
-Here are the different commands that may be used to moderate a chat room:
+Here are the different commands that may be used to moderate a chatroom:
 
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
 | Event Type | When is it triggered?                                                                        | Example (substitue $nickname with an actual user's nickname)  |
 +============+==============================================================================================+===============================================================+
-| **ban**    | Ban a user from the chat room. They will not be able to join again.                          | /ban $nickname                                                |
+| **ban**    | Ban a user from the chatroom. They will not be able to join again.                           | /ban $nickname                                                |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
-| **clear**  | Clear the messages shown in the chat room.                                                   | /clear                                                        |
+| **clear**  | Clear the messages shown in the chatroom.                                                    | /clear                                                        |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
 | **deop**   | Make a moderator a normal occupant.                                                          | /deop $nickname [$reason]                                     |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
@@ -95,7 +95,7 @@ Here are the different commands that may be used to moderate a chat room:
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
 | **op**     | Make a normal occupant a moderator.                                                          | /op $nickname [$reason]                                       |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
-| **topic**  | Set the topic of the chat room.                                                              | /topic ${topic text}                                          |
+| **topic**  | Set the topic of the chatroom.                                                               | /topic ${topic text}                                          |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+
 | **voice**  | Allow a muted user to post messages to the room.                                             | /voice $nickname [$reason]                                    |
 +------------+----------------------------------------------------------------------------------------------+---------------------------------------------------------------+

+ 1 - 1
docs/source/manual.rst

@@ -168,7 +168,7 @@ A normal contact
 
 If the person accepts your contact request, they will get a *chat status
 indicator* in your roster and will also become clickable. Clicking on the name
-of the user will open a chat box in which you can then start chatting with that
+of the user will open a chatbox in which you can then start chatting with that
 user.
 
 .. figure:: images/remove-contact.png

+ 3 - 3
docs/source/other_frameworks.rst

@@ -52,7 +52,7 @@ Below is an example code that wraps converse.js as an angular.js service.
                                      // file src/locales.js to include only those
                                      // translations that you care about.
 
-            "converse-chatview",     // Renders standalone chat boxes for single user chat
+            "converse-chatview",     // Renders standalone chatboxes for single user chat
             "converse-controlbox",   // The control box
             "converse-bookmarks",    // XEP-0048 Bookmarks
             "converse-mam",          // XEP-0313 Message Archive Management
@@ -62,8 +62,8 @@ Below is an example code that wraps converse.js as an angular.js service.
             "converse-register",     // XEP-0077 In-band registration
             "converse-ping",         // XEP-0199 XMPP Ping
             "converse-notification", // HTML5 Notifications
-            "converse-minimize",     // Allows chat boxes to be minimized
-            "converse-dragresize",   // Allows chat boxes to be resized by dragging them
+            "converse-minimize",     // Allows chatboxes to be minimized
+            "converse-dragresize",   // Allows chatboxes to be resized by dragging them
             "converse-headline",     // Support for headline messages
             // END: Removable components
 

+ 1 - 1
docs/source/plugin_development.rst

@@ -340,7 +340,7 @@ method of the plugin:
     });
 
 In this case, the plugin waits for the ``chatBoxOpened`` event, before it then
-calls ``renderMinimizeButton``, which adds a new button to the chat box (which
+calls ``renderMinimizeButton``, which adds a new button to the chatbox (which
 enables you to minimize it).
 
 Finding the right promises and/or events to listen to, can be a bit

+ 3 - 3
docs/source/quickstart.rst

@@ -22,8 +22,8 @@ The latest versions of these files are available at these URLs:
 
 To load a specific version of Converse.js you can put the version in the URL, like so:
 
-* https://cdn.conversejs.org/3.0.3/dist/converse.min.js
-* https://cdn.conversejs.org/3.0.3/css/converse.min.css
+* https://cdn.conversejs.org/3.3.3/dist/converse.min.js
+* https://cdn.conversejs.org/3.3.3/css/converse.min.css
 
 You can include these two URLs inside the *<head>* element of your website
 via the *script* and *link* tags:
@@ -100,7 +100,7 @@ Headless build
 
 There is also the option of making a headless build of converse.js.
 This means a build without any UI but still containing core functionality of
-maintaining a roster, chat boxes and messages.
+maintaining a roster, chatboxes and messages.
 
 The file `src/headless.js <https://github.com/jcbrand/converse.js/blob/master/src/headless.js>`_
 is used to determine which plugins are included in the build.

+ 13 - 15
index.html

@@ -16,8 +16,8 @@
     <noscript><p><img src="//stats.opkode.com/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
     <script src="src/website.js"></script>
     <![if gte IE 11]>
-        <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/3.3.1/css/converse.min.css" />
-        <script src="https://cdn.conversejs.org/3.3.1/dist/converse.min.js"></script>
+        <link type="text/css" rel="stylesheet" media="screen" href="css/converse.css" />
+        <script src="dist/converse.js"></script>
     <![endif]>
 </head>
 
@@ -85,10 +85,9 @@
         <div class="row">
             <div class="col-md-8 col-md-offset-2 banner-social-buttons">
                 <ul class="list-inline">
-                    <li><a href="https://twitter.com/jcopkode" class="btn btn-circle btn-lg" title="Twitter" target="_blank" rel="noopener"><i class="fa fa-twitter"></i></a>
-                    </li>
-                    <li><a href="https://github.com/jcbrand/converse.js" class="btn btn-circle btn-lg" title="GitHub" target="_blank" rel="noopener"><i class="fa fa-github"></i></a>
-                    </li>
+                    <li><a href="https://twitter.com/jcopkode" class="btn btn-circle btn-lg" title="Twitter" target="_blank" rel="noopener"><i class="fa fa-twitter"></i></a></li>
+                    <li><a href="https://mastodon.xyz/@jcbrand" class="btn btn-lg" title="Mastodon" target="_blank" rel="noopener"><img class="mastodon" src="/logo/mastodon.svg"/></a></li>
+                    <li><a href="https://github.com/jcbrand/converse.js" class="btn btn-circle btn-lg" title="GitHub" target="_blank" rel="noopener"><i class="fa fa-github"></i></a></li>
                 </ul>
             </div>
         </div>
@@ -144,11 +143,8 @@
                             <li>Available as overlayed chat boxes or as a fullscreen application.
                                 See <a href="https://inverse.chat" target="_blank" rel="noopener">inverse.chat</a> for the fullscreen version.
                             </li>
-                            <li><a href="https://conversejs.org/docs/html/plugin_development.html">Plugin Architecture</a> based on 
-                                <a href="https://jcbrand.github.io/pluggable.js/" target="_blank" rel="noopener">pluggable.js</a></li>
-                            <li>Presence information (online, busy, away)</li>
-                            <li>Single-user chat</li>
-                            <li>Contacts and groups</li>
+                            <li><a href="https://conversejs.org/docs/html/plugin_development.html">Plugin Architecture</a></li>
+                            <li>Single-user and group chat</li>
                             <li>Multi-user chatrooms (<a href="http://xmpp.org/extensions/xep-0045.html" target="_blank" rel="noopener">XEP 45</a>)</li>
                             <li>Chatroom bookmarks (<a href="http://xmpp.org/extensions/xep-0048.html" target="_blank" rel="noopener">XEP 48</a>)</li>
                             <li>Direct invitations to chat rooms (<a href="http://xmpp.org/extensions/xep-0249.html" target="_blank" rel="noopener">XEP 249</a>)</li>
@@ -163,10 +159,11 @@
                             <li>Third person "/me" messages (<a href="http://xmpp.org/extensions/xep-0245.html" target="_blank" rel="noopener">XEP 245</a>)</li>
                             <li>XMPP Ping (<a href="http://xmpp.org/extensions/xep-0199.html" target="_blank" rel="noopener">XEP 199</a>)</li>
                             <li>Server-side archiving of messages (<a href="http://xmpp.org/extensions/xep-0313.html" target="_blank" rel="noopener">XEP 313</a>)</li>
+                            <li>Hidden messages (aka Spoilers) (<a href="http://xmpp.org/extensions/xep-0382.html" target="_blank" rel="noopener">XEP 382</a>)</li>
                             <li>Client state indication (<a href="http://xmpp.org/extensions/xep-0352.html" target="_blank" rel="noopener">XEP 352</a>)</li>
                             <li>Off-the-record encryption</li>
                             <li>Supports anonymous logins, see the <a href="https://conversejs.org/demo/anonymous.html" target="_blank" rel="noopener">anonymous login demo</a>.</li>
-                            <li>Translated into 16 languages</li>
+                            <li>Translated into 17 languages</li>
                         </ul>
                 </div>
                 <div class="col-lg-4">
@@ -233,15 +230,16 @@
                     <div class="sponsors">
                         <h2>Converse.js is supported by:</h2>
                         <ul>
-                            <li><a href="https://www.keycdn.com/" target="_blank" rel="noopener"><img style="height: 2em" src="/logo/keycdn.svg" alt="KeyCDN"></a></li>
+                            <li><a href="https://www.keycdn.com/" target="_blank" rel="noopener"><img style="height: 3em" src="/logo/keycdn.svg" alt="KeyCDN"></a></li>
+                            <li><a href="http://wikisuite.org" target="_blank" rel="noopener"><img style="height: 4em" src="/logo/wikisuite-white.png" alt="WikiSuite"></a></li>
                             <li><a href="https://diasporing.ch" target="_blank" rel="noopener"><img style="height: 2em" src="/logo/diasporing.svg" alt="Diasporing.ch"></a></li>
                         </ul>
                     </div>
 
-                    <p class="sponsors-text">Converse.js is a software commons; available at no cost to you and every other person on earth.
+                    <p class="sponsors-text">Converse.js is a software commons; available at no cost to you or anyone else.
                        Sponsorships allow us to fund further development and improvements and are greatly appreciated.
                        If you'd like to sponsor this project, please visit <a href="https://www.patreon.com/jcbrand" target="_blank" rel="noopener">Patreon page</a>
-                       or <a href="https://liberapay.com/jcbrand" target="_blank" rel="noopener">Liberapay</a>
+                       or <a href="https://liberapay.com/jcbrand" target="_blank" rel="noopener">Liberapay</a>.
                     </p>
                 </div>
             </div>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/af/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 271 - 243
locale/af/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/ca/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 270 - 243
locale/ca/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 316 - 293
locale/converse.pot


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/de/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 355 - 330
locale/de/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/es/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 321 - 294
locale/es/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/fr/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 321 - 295
locale/fr/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/he/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 321 - 294
locale/he/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/hu/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 320 - 293
locale/hu/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/id/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 323 - 296
locale/id/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/it/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 299 - 272
locale/it/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/ja/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 349 - 320
locale/ja/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/nb/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 328 - 303
locale/nb/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/nl/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 545 - 562
locale/nl/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/pl/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 270 - 243
locale/pl/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/pt_BR/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 502 - 527
locale/pt_BR/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/ru/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 474 - 479
locale/ru/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/uk/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 280 - 253
locale/uk/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/zh_CN/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 321 - 294
locale/zh_CN/LC_MESSAGES/converse.po


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
locale/zh_TW/LC_MESSAGES/converse.json


Datei-Diff unterdrückt, da er zu groß ist
+ 339 - 309
locale/zh_TW/LC_MESSAGES/converse.po


BIN
logo/keycdn.png


Datei-Diff unterdrückt, da er zu groß ist
+ 42 - 0
logo/mastodon.svg


BIN
logo/wikisuite-white.png


BIN
logo/wikisuite.png


+ 2 - 2
mobile.html

@@ -16,8 +16,8 @@
     <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/mobile.min.css" />
     <script type="text/javascript" src="analytics.js"></script>
     <noscript><p><img src="//stats.opkode.com/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
-    <![if gte IE 9]>
-        <script src="https://cdn.conversejs.org/dist/converse-mobile.min.js"></script>
+    <![if gte IE 11]>
+        <script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
     <![endif]>
 </head>
 

Datei-Diff unterdrückt, da er zu groß ist
+ 310 - 381
package-lock.json


+ 5 - 6
package.json

@@ -1,6 +1,6 @@
 {
   "name": "converse.js",
-  "version": "3.3.1",
+  "version": "3.3.3",
   "description": "Browser based XMPP instant messaging client",
   "main": "main.js",
   "directories": {
@@ -37,10 +37,10 @@
     "backbone": "1.3.3",
     "backbone.browserStorage": "0.0.3",
     "backbone.nativeview": "^0.3.3",
-    "backbone.overview": "1.0.0",
-    "backbone.vdomview": "1.0.0",
     "bootstrap": "^4.0.0",
     "bootstrap.native": "^2.0.21",
+    "backbone.overview": "1.0.2",
+    "backbone.vdomview": "1.0.1",
     "bourbon": "^4.3.2",
     "clean-css-cli": "^4.0.10",
     "emojione": "^3.0.3",
@@ -57,15 +57,14 @@
     "lodash": "4.17.4",
     "lodash-template-loader": "^2.0.0",
     "moment": "~2.18.1",
-    "npm": "^4.1.1",
-    "otr": "0.2.16",
+    "otr": "jcbrand/otr",
     "pluggable.js": "2.0.0",
     "po2json": "^0.4.4",
     "requirejs": "2.3.5",
     "run-headless-chromium": "^0.1.1",
     "sinon": "^2.1.0",
     "sizzle": "^2.3.3",
-    "snabbdom": "jcbrand/snabbdom",
+    "snabbdom": "0.7.1",
     "snyk": "^1.21.2",
     "strophe.js": "1.2.14",
     "strophejs-plugin-disco": "0.0.1",

+ 32 - 10
sass/_chatbox.scss

@@ -85,6 +85,11 @@
             margin: 0;
             width: $mobile-chat-width;
         }
+
+        .spoiler {
+            background-color: lighten($chat-head-color, 50%);
+        }
+
         .box-flyout {
             background-color: white;
             box-shadow: 1px 3px 5px 3px rgba(0, 0, 0, 0.4);
@@ -157,6 +162,8 @@
                 }
             }
             .chat-image {
+                height: auto;
+                width: auto;
                 max-height: 24em;
                 max-width: 100%;
             }
@@ -189,6 +196,12 @@
                 .chat-msg-content {
                     max-width: 100%;
                     word-wrap: break-word;
+
+                    &.spoiler {
+                        border-radius: $chatbox-border-radius;
+                        padding: 0.5em;
+                    }
+
                     .emojione {
                         margin-bottom: -6px;
                     }
@@ -226,6 +239,11 @@
             line-height: 1.3em;
             height: 206px;
             height: calc(100% - #{$toolbar-height + $chat-textarea-height +1px});
+
+            .toggle-spoiler:before {
+                padding-right: 0.25em;
+                whitespace: nowrap;
+            }
         }
         .chat-content-sendbutton {
             height: calc(100% - #{$toolbar-height + $chat-textarea-height + $send-button-height + 2*$send-button-margin});
@@ -239,6 +257,7 @@
                 position: relative;
             }
         }
+
         .sendXMPPMessage {
             -moz-background-clip: padding;
             -webkit-background-clip: padding-box;
@@ -257,6 +276,10 @@
                 width: 100%;
             }
 
+            .spoiler-hint {
+                width: 100%;
+            }
+
             .chat-textarea {
                 border-top-left-radius: 0;
                 border-top-right-radius: 0;
@@ -266,6 +289,9 @@
                 width: 100%;
                 border: none;
                 resize: none;
+                &.spoiler {
+                    height: 42px;
+                }
             }
 
             .send-button {
@@ -282,20 +308,17 @@
             .chat-toolbar {
                 box-sizing: border-box;
                 margin: 0;
-                padding: 5px;
+                padding: 0.25em;
                 height: $toolbar-height;
                 display: block;
-                background-color: $toolbar-color;
+                background-color: lighten($chat-head-color, 10%);
+                color: white;
                 a {
-                    font-size: $font-size;
-                    color: $text-color;
+                    color: white;
+                    font-size: $font-size-large;
                     text-decoration: none;
                     text-shadow: none;
                 }
-                .chat-toolbar-text {
-                    font-size: 12px;
-                    padding-right: 3px;
-                }
                 .unencrypted a,
                 .unencrypted {
                     color: $text-color;
@@ -384,10 +407,9 @@
                         }
                     }
                     &.toggle-toolbar-menu {
-                        color: $text-color;
+                        color: white;
                     }
                     &.toggle-smiley {
-                        padding-left: 5px;
                         .emoji-toolbar {
                             .emoji-category-picker,
                             .emoji-skintone-picker {

+ 4 - 4
sass/_chatrooms.scss

@@ -185,10 +185,10 @@
                             &.occupant {
                                 cursor: pointer;
                                 .occupant-status {
-									display: inline-block;
+                                    display: inline-block;
                                     margin-right: 0.5em;
-									width: 0.5em;
-									height: 0.5em;
+                                    width: 0.5em;
+                                    height: 0.5em;
                                     &.occupant-online,
                                     &.occupant-chat {
                                         background-color: #1A9707;
@@ -249,7 +249,7 @@
 
         .sendXMPPMessage {
             .chat-toolbar {
-                background-color: $chatroom-toolbar-color;
+                background-color: lighten($chatroom-head-color, 10%);
             }
             .chat-textarea {
                 border-bottom-right-radius: 0;

+ 2 - 1
sass/_controlbox.scss

@@ -404,7 +404,8 @@
         }
 
         .controlbox-panes {
-            overflow-y: scroll;
+            height: 100%;
+            overflow-y: auto;
         }
 
         .controlbox-pane {

+ 27 - 24
sass/_core.scss

@@ -9,6 +9,8 @@
 #converse-embedded-chat,
 #conversejs {
     margin-left: -$chat-gutter; // Mystery why this is necessary
+    padding-left: env(safe-area-inset-left);
+    padding-right: env(safe-area-inset-right);
     bottom: 0;
     height: auto;
     width: 100vw;
@@ -21,17 +23,18 @@
     z-index: 1031; // One more than bootstrap navbar
 
     ::-webkit-input-placeholder { /* Chrome/Opera/Safari */
-    color: $subdued-color;
+        color: $subdued-color;
     }
     ::-moz-placeholder { /* Firefox 19+ */
-    color: $subdued-color;
+        color: $subdued-color;
     }
     :-ms-input-placeholder { /* IE 10+ */
-    color: $subdued-color;
+        color: $subdued-color;
     }
     :-moz-placeholder { /* Firefox 18- */
-    color: $subdued-color;
+        color: $subdued-color;
     }
+
     ::placeholder {
         color: $subdued-color;
     }
@@ -105,33 +108,33 @@
     }
 
     @keyframes colorchange-chatmessage {
-          0%   {background-color: rgba(141, 216, 174, 1);}
-          25%  {background-color: rgba(141, 216, 174, 0.75);}
-          50%  {background-color: rgba(141, 216, 174, 0.5);}
-          75%  {background-color: rgba(141, 216, 174, 0.25);}
-          100% {background-color: transparent;}
+        0%   {background-color: rgba(141, 216, 174, 1);}
+        25%  {background-color: rgba(141, 216, 174, 0.75);}
+        50%  {background-color: rgba(141, 216, 174, 0.5);}
+        75%  {background-color: rgba(141, 216, 174, 0.25);}
+        100% {background-color: transparent;}
     }
     @-webkit-keyframes colorchange-chatmessage {
-          0%   {background-color: rgba(141, 216, 174, 1);}
-          25%  {background-color: rgba(141, 216, 174, 0.75);}
-          50%  {background-color: rgba(141, 216, 174, 0.5);}
-          75%  {background-color: rgba(141, 216, 174, 0.25);}
-          100% {background-color: transparent;}
+        0%   {background-color: rgba(141, 216, 174, 1);}
+        25%  {background-color: rgba(141, 216, 174, 0.75);}
+        50%  {background-color: rgba(141, 216, 174, 0.5);}
+        75%  {background-color: rgba(141, 216, 174, 0.25);}
+        100% {background-color: transparent;}
     }
 
     @keyframes colorchange-chatmessage-muc {
-          0%   {background-color: rgba(255, 181, 162, 1);}
-          25%  {background-color: rgba(255, 181, 162, 0.75);}
-          50%  {background-color: rgba(255, 181, 162, 0.5);}
-          75%  {background-color: rgba(255, 181, 162, 0.25);}
-          100% {background-color: transparent;}
+        0%   {background-color: rgba(255, 181, 162, 1);}
+        25%  {background-color: rgba(255, 181, 162, 0.75);}
+        50%  {background-color: rgba(255, 181, 162, 0.5);}
+        75%  {background-color: rgba(255, 181, 162, 0.25);}
+        100% {background-color: transparent;}
     }
     @-webkit-keyframes colorchange-chatmessage-muc {
-          0%   {background-color: rgba(255, 181, 162, 1);}
-          25%  {background-color: rgba(255, 181, 162, 0.75);}
-          50%  {background-color: rgba(255, 181, 162, 0.5);}
-          75%  {background-color: rgba(255, 181, 162, 0.25);}
-          100% {background-color: transparent;}
+        0%   {background-color: rgba(255, 181, 162, 1);}
+        25%  {background-color: rgba(255, 181, 162, 0.75);}
+        50%  {background-color: rgba(255, 181, 162, 0.5);}
+        75%  {background-color: rgba(255, 181, 162, 0.25);}
+        100% {background-color: transparent;}
     }
 
 

+ 78 - 78
sass/_fonts.scss

@@ -29,84 +29,84 @@
 
 #converse-embedded-chat,
 #conversejs {
-    .icon-address-book:before 	{ content: "\270f"; }
-    .icon-attachment:before 	{ content: "\e032"; }
-    .icon-away:before 		    { content: "\25fb"; }
-    .icon-blocked:before 		{ content: "\2718"; }
-    .icon-bold:before 		    { content: "\e04d"; }
-    .icon-bubbles2:before 		{ content: "\e016"; }
-    .icon-bubbles3:before 		{ content: "\e017"; }
-    .icon-bubbles:before 		{ content: "\e015"; }
-    .icon-busy:before 		    { content: "\e004"; }
-    .icon-dnd:before 		    { content: "\e004"; }
-    .icon-cancel-circle:before 	{ content: "\e058"; }
-    .icon-checkmark:before 		{ content: "\2713"; }
-    .icon-close:before 		    { content: "\2715"; }
-    .icon-closed:before 		{ content: "\25ba"; }
-    .icon-cog:before 		    { content: "\e02f"; }
-    .icon-cogs:before 		    { content: "\e022"; }
-    .icon-conversejs:before 	{ content: "\e600"; }
-    .icon-database:before 		{ content: "\f1c0"; }
-    .icon-envelope:before 		{ content: "\f003"; }
-    .icon-exit:before 		    { content: "\e601"; }
-    .icon-eye-blocked:before 	{ content: "\e031"; }
-    .icon-eye:before 		    { content: "\e030"; }
-    .icon-github:before 		{ content: "\eab0"; }
-    .icon-globe:before 		    { content: "\f0ac"; }
-    .icon-google2:before 		{ content: "\ea89"; }
-    .icon-group:before 		    { content: "\f0c0"; }
-    .icon-happy:before 		    { content: "\263b"; }
-    .icon-heart2:before 		{ content: "\f004"; }
-    .icon-heart:before 		    { content: "\2764"; }
-    .icon-heart_empty:before 	{ content: "\f08a"; }
-    .icon-hide-users:before 	{ content: "\e01c"; }
-    .icon-home:before 		    { content: "\e000"; }
-    .icon-idcard-dark:before 	{ content: "\f2c2"; }
-    .icon-idcard:before 		{ content: "\f2c3"; }
-    .icon-image:before 		    { content: "\2b14"; }
-    .icon-info:before 		    { content: "\2360"; }
-    .icon-italic:before 		{ content: "\e04f"; }
-    .icon-key:before 		    { content: "\e028"; }
-    .icon-legal:before 		    { content: "\f0e3"; }
-    .icon-lock-2:before 		{ content: "\e027"; }
-    .icon-minus:before 		    { content: "\e05a"; }
-    .icon-music:before 		    { content: "\266b"; }
-    .icon-newtab:before 		{ content: "\e053"; }
-    .icon-notebook:before 		{ content: "\2710"; }
-    .icon-notification:before 	{ content: "\e01f"; }
-    .icon-offline:before 		{ content: "\e002"; }
-    .icon-logout:before	        { content: "\e002"; }
-    .icon-online:before 		{ content: "\25fc"; }
-    .icon-opened:before 		{ content: "\25bc"; }
-    .icon-pencil:before 		{ content: "\270e"; }
-    .icon-phone-hang-up:before 	{ content: "\260e"; }
-    .icon-phone:before 		    { content: "\260f"; }
-    .icon-plus:before 		    { content: "\271a"; }
-    .icon-pushpin:before 		{ content: "\e012"; }
-    .icon-quotes-left:before 	{ content: "\e01d"; }
-    .icon-reddit:before 		{ content: "\eac6"; }
-    .icon-remove:before 		{ content: "\e02d"; }
-    .icon-room-info:before 		{ content: "\e059"; }
-    .icon-save:before 		    { content: "\f0c7"; }
-    .icon-search:before 		{ content: "\e021"; }
-    .icon-show-users:before 	{ content: "\e01e"; }
-    .icon-smiley:before 		{ content: "\263a"; }
-    .icon-snowflake:before 		{ content: "\f2dc"; }
-    .icon-spell-check:before 	{ content: "\e045"; }
-    .icon-spinner:before 		{ content: "\231b"; }
-    .icon-star:before 		    { content: "\f005"; }
-    .icon-star_empty:before 	{ content: "\f006"; }
-    .icon-strikethrough:before 	{ content: "\e050"; }
-    .icon-twitter:before 		{ content: "\ea96"; }
-    .icon-underline:before 		{ content: "\e04e"; }
-    .icon-unlocked:before 		{ content: "\e025"; }
-    .icon-user:before 		    { content: "\e01a"; }
-    .icon-users:before 		    { content: "\e01b"; }
-    .icon-warning:before 		{ content: "\26a0"; }
-    .icon-wrench:before 		{ content: "\e024"; }
-    .icon-xa:before 		    { content: "\e602"; }
-    .icon-zoomin:before 		{ content: "\e02b"; }
-    .icon-zoomout:before 		{ content: "\e02a"; }
+    .icon-address-book:before   { content: "\270f"; }
+    .icon-attachment:before     { content: "\e032"; }
+    .icon-away:before           { content: "\25fb"; }
+    .icon-blocked:before        { content: "\2718"; }
+    .icon-bold:before           { content: "\e04d"; }
+    .icon-bubbles2:before       { content: "\e016"; }
+    .icon-bubbles3:before       { content: "\e017"; }
+    .icon-bubbles:before        { content: "\e015"; }
+    .icon-busy:before           { content: "\e004"; }
+    .icon-dnd:before            { content: "\e004"; }
+    .icon-cancel-circle:before  { content: "\e058"; }
+    .icon-checkmark:before      { content: "\2713"; }
+    .icon-close:before          { content: "\2715"; }
+    .icon-closed:before         { content: "\25ba"; }
+    .icon-cog:before            { content: "\e02f"; }
+    .icon-cogs:before           { content: "\e022"; }
+    .icon-conversejs:before     { content: "\e600"; }
+    .icon-database:before       { content: "\f1c0"; }
+    .icon-envelope:before       { content: "\f003"; }
+    .icon-exit:before           { content: "\e601"; }
+    .icon-eye-blocked:before    { content: "\e031"; }
+    .icon-eye:before            { content: "\e030"; }
+    .icon-github:before         { content: "\eab0"; }
+    .icon-globe:before          { content: "\f0ac"; }
+    .icon-google2:before        { content: "\ea89"; }
+    .icon-group:before          { content: "\f0c0"; }
+    .icon-happy:before          { content: "\263b"; }
+    .icon-heart2:before         { content: "\f004"; }
+    .icon-heart:before          { content: "\2764"; }
+    .icon-heart_empty:before    { content: "\f08a"; }
+    .icon-hide-users:before     { content: "\e01c"; }
+    .icon-home:before           { content: "\e000"; }
+    .icon-idcard-dark:before    { content: "\f2c2"; }
+    .icon-idcard:before         { content: "\f2c3"; }
+    .icon-image:before          { content: "\2b14"; }
+    .icon-info:before           { content: "\2360"; }
+    .icon-italic:before         { content: "\e04f"; }
+    .icon-key:before            { content: "\e028"; }
+    .icon-legal:before          { content: "\f0e3"; }
+    .icon-lock-2:before         { content: "\e027"; }
+    .icon-minus:before          { content: "\e05a"; }
+    .icon-music:before          { content: "\266b"; }
+    .icon-newtab:before         { content: "\e053"; }
+    .icon-notebook:before       { content: "\2710"; }
+    .icon-notification:before   { content: "\e01f"; }
+    .icon-offline:before        { content: "\e002"; }
+    .icon-logout:before         { content: "\e002"; }
+    .icon-online:before         { content: "\25fc"; }
+    .icon-opened:before         { content: "\25bc"; }
+    .icon-pencil:before         { content: "\270e"; }
+    .icon-phone-hang-up:before  { content: "\260e"; }
+    .icon-phone:before          { content: "\260f"; }
+    .icon-plus:before           { content: "\271a"; }
+    .icon-pushpin:before        { content: "\e012"; }
+    .icon-quotes-left:before    { content: "\e01d"; }
+    .icon-reddit:before         { content: "\eac6"; }
+    .icon-remove:before         { content: "\e02d"; }
+    .icon-room-info:before      { content: "\e059"; }
+    .icon-save:before           { content: "\f0c7"; }
+    .icon-search:before         { content: "\e021"; }
+    .icon-show-users:before     { content: "\e01e"; }
+    .icon-smiley:before         { content: "\263a"; }
+    .icon-snowflake:before      { content: "\f2dc"; }
+    .icon-spell-check:before    { content: "\e045"; }
+    .icon-spinner:before        { content: "\231b"; }
+    .icon-star:before           { content: "\f005"; }
+    .icon-star_empty:before     { content: "\f006"; }
+    .icon-strikethrough:before  { content: "\e050"; }
+    .icon-twitter:before        { content: "\ea96"; }
+    .icon-underline:before      { content: "\e04e"; }
+    .icon-unlocked:before       { content: "\e025"; }
+    .icon-user:before           { content: "\e01a"; }
+    .icon-users:before          { content: "\e01b"; }
+    .icon-warning:before        { content: "\26a0"; }
+    .icon-wrench:before         { content: "\e024"; }
+    .icon-xa:before             { content: "\e602"; }
+    .icon-zoomin:before         { content: "\e02b"; }
+    .icon-zoomout:before        { content: "\e02a"; }
 
     [data-icon]:before {
         content: attr(data-icon);

+ 1 - 1
sass/_muc_embedded.scss

@@ -1,6 +1,6 @@
 @import "bourbon";
 @import "converse/variables";
-#converse-embedded-chat {
+#conversejs.converse-embedded {
 
     @include box-sizing(border-box);
     *, *:before, *:after {

+ 3 - 0
sass/only-fonts.scss

@@ -0,0 +1,3 @@
+@import "bourbon";
+@import "converse/variables";
+@import "fonts";

+ 527 - 438
spec/bookmarks.js

@@ -12,6 +12,8 @@
 } (this, function (jasmine, $, converse, utils, mock, test_utils) {
     "use strict";
     var $iq = converse.env.$iq,
+        $msg = converse.env.$msg,
+        Backbone = converse.env.Backbone,
         Strophe = converse.env.Strophe,
         _ = converse.env._,
         u = converse.env.utils;
@@ -20,138 +22,154 @@
 
         it("can be bookmarked", mock.initConverseWithPromises(
             null, ['rosterGroupsFetched'], {}, function (done, _converse) {
+                
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.bare_jid,
+                [{'category': 'pubsub', 'type': 'pep'}],
+                ['http://jabber.org/protocol/pubsub#publish-options']
+            ).then(function () {
+                var sent_stanza, IQ_id;
+                var sendIQ = _converse.connection.sendIQ;
+                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                    sent_stanza = iq;
+                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                });
+                spyOn(_converse.connection, 'getUniqueId').and.callThrough();
 
-            var sent_stanza, IQ_id;
-            var sendIQ = _converse.connection.sendIQ;
-            spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                sent_stanza = iq;
-                IQ_id = sendIQ.bind(this)(iq, callback, errback);
-            });
-            spyOn(_converse.connection, 'getUniqueId').and.callThrough();
-
-            test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
-            var jid = 'theplay@conference.shakespeare.lit';
-            var view = _converse.chatboxviews.get(jid);
-            spyOn(view, 'renderBookmarkForm').and.callThrough();
-            spyOn(view, 'closeForm').and.callThrough();
-
-            var $bookmark = $(view.el).find('.icon-pushpin');
-            $bookmark[0].click();
-            expect(view.renderBookmarkForm).toHaveBeenCalled();
-
-            view.el.querySelector('.button-cancel').click();
-            expect(view.closeForm).toHaveBeenCalled();
-            expect($bookmark.hasClass('on-button'), false);
-
-            $bookmark[0].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>
-             */
-            expect(view.model.get('bookmarked')).toBeFalsy();
-            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');
-            view.el.querySelector('.button-primary').click();
-
-            expect(view.model.get('bookmarked')).toBeTruthy();
-            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>"+
-                        "<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>"
-            );
-
-            /* 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
+                test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
+                var jid = 'theplay@conference.shakespeare.lit';
+                var view = _converse.chatboxviews.get(jid);
+                spyOn(view, 'renderBookmarkForm').and.callThrough();
+                spyOn(view, 'closeForm').and.callThrough();
+
+                test_utils.waitUntil(function () {
+                    return !_.isNull(view.el.querySelector('.toggle-bookmark'));
+                }, 300).then(function () {
+                    var $bookmark = $(view.el).find('.icon-pushpin');
+                    $bookmark[0].click();
+                    expect(view.renderBookmarkForm).toHaveBeenCalled();
+
+                    view.el.querySelector('.button-cancel').click();
+                    expect(view.closeForm).toHaveBeenCalled();
+                    expect($bookmark.hasClass('on-button'), false);
+
+                    $bookmark[0].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>
+                    */
+                    expect(view.model.get('bookmarked')).toBeFalsy();
+                    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');
+                    view.el.querySelector('.button-primary').click();
+
+                    expect(view.model.get('bookmarked')).toBeTruthy();
+                    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>"+
+                                "<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>"
+                    );
+
+                    /* 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));
+                    // We ignore this IQ stanza... (unless it's an error stanza), so
+                    // nothing to test for here.
+                    done();
+                });
             });
-            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            // We ignore this IQ stanza... (unless it's an error stanza), so
-            // nothing to test for here.
-            done();
         }));
 
         it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverseWithPromises(
             null, ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-            var jid = 'lounge@localhost';
-            _converse.bookmarks.create({
-                'jid': jid,
-                'autojoin': false,
-                'name':  'The Lounge',
-                'nick': ' Othello'
-            });
-            expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeTruthy();
-
-            jid = 'theplay@conference.shakespeare.lit';
-            _converse.bookmarks.create({
-                'jid': jid,
-                'autojoin': true,
-                'name':  'The Play',
-                'nick': ' Othello'
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.bare_jid,
+                [{'category': 'pubsub', 'type': 'pep'}],
+                ['http://jabber.org/protocol/pubsub#publish-options']
+            ).then(function () {
+                var jid = 'lounge@localhost';
+                _converse.bookmarks.create({
+                    'jid': jid,
+                    'autojoin': false,
+                    'name':  'The Lounge',
+                    'nick': ' Othello'
+                });
+                expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeTruthy();
+
+                jid = 'theplay@conference.shakespeare.lit';
+                _converse.bookmarks.create({
+                    'jid': jid,
+                    'autojoin': true,
+                    'name':  'The Play',
+                    'nick': ' Othello'
+                });
+                expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy();
+                done();
             });
-            expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy();
-            done();
         }));
 
         describe("when bookmarked", function () {
@@ -159,237 +177,217 @@
             it("displays that it's bookmarked through its bookmark icon", mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-                test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
-                var view = _converse.chatboxviews.get('lounge@localhost');
-                var $bookmark_icon = $(view.el.querySelector('.icon-pushpin'));
-                expect($bookmark_icon.hasClass('button-on')).toBeFalsy();
-                view.model.set('bookmarked', true);
-                expect($bookmark_icon.hasClass('button-on')).toBeTruthy();
-                view.model.set('bookmarked', false);
-                expect($bookmark_icon.hasClass('button-on')).toBeFalsy();
-                done();
+                test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                ).then(function () {
+                    test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
+                    var view = _converse.chatboxviews.get('lounge@localhost');
+
+                    test_utils.waitUntil(function () {
+                        return !_.isNull(view.el.querySelector('.toggle-bookmark'));
+                    }, 300).then(function () {
+                        var bookmark_icon = view.el.querySelector('.icon-pushpin');
+                        expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
+                        view.model.set('bookmarked', true);
+                        expect(_.includes(bookmark_icon.classList, 'button-on')).toBeTruthy();
+                        view.model.set('bookmarked', false);
+                        expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
+                        done();
+                    });
+                });
             }));
 
             it("can be unbookmarked", mock.initConverseWithPromises(
                 null, ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-                var sent_stanza, IQ_id;
-                var sendIQ = _converse.connection.sendIQ;
-                test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
-                var jid = 'theplay@conference.shakespeare.lit';
-                var view = _converse.chatboxviews.get(jid);
-                spyOn(view, 'toggleBookmark').and.callThrough();
-                spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough();
-                view.delegateEvents();
-                _converse.bookmarks.create({
-                    'jid': view.model.get('jid'),
-                    'autojoin': false,
-                    'name':  'The Play',
-                    'nick': ' Othello'
-                });
-                expect(_converse.bookmarks.length).toBe(1);
-                expect(view.model.get('bookmarked')).toBeTruthy();
-                var $bookmark_icon = $(view.el.querySelector('.icon-pushpin'));
-                expect($bookmark_icon.hasClass('button-on')).toBeTruthy();
-
-                spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
-                    sent_stanza = iq;
-                    IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                ).then(function () {
+                    var sent_stanza, IQ_id;
+                    var sendIQ = _converse.connection.sendIQ;
+
+                    test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
+                    var jid = 'theplay@conference.shakespeare.lit';
+                    var view = _converse.chatboxviews.get(jid);
+
+                    test_utils.waitUntil(function () {
+                        return !_.isNull(view.el.querySelector('.toggle-bookmark'));
+                    }, 300).then(function () {
+                        spyOn(view, 'toggleBookmark').and.callThrough();
+                        spyOn(_converse.bookmarks, 'sendBookmarkStanza').and.callThrough();
+                        view.delegateEvents();
+
+                        _converse.bookmarks.create({
+                            'jid': view.model.get('jid'),
+                            'autojoin': false,
+                            'name':  'The Play',
+                            'nick': ' Othello'
+                        });
+                        expect(_converse.bookmarks.length).toBe(1);
+                        expect(view.model.get('bookmarked')).toBeTruthy();
+                        var $bookmark_icon = $(view.el.querySelector('.icon-pushpin'));
+                        expect($bookmark_icon.hasClass('button-on')).toBeTruthy();
+
+                        spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
+                            sent_stanza = iq;
+                            IQ_id = sendIQ.bind(this)(iq, callback, errback);
+                        });
+                        spyOn(_converse.connection, 'getUniqueId').and.callThrough();
+                        $bookmark_icon[0].click();
+                        expect(view.toggleBookmark).toHaveBeenCalled();
+                        expect($bookmark_icon.hasClass('button-on')).toBeFalsy();
+                        expect(_converse.bookmarks.length).toBe(0);
+
+                        // Check that an IQ stanza is sent out, containing no
+                        // conferences to bookmark (since we removed the one and
+                        // only bookmark).
+                        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'/>"+
+                                        "</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>"
+                        );
+                        done();
+                    });
                 });
-                spyOn(_converse.connection, 'getUniqueId').and.callThrough();
-                $bookmark_icon[0].click();
-                expect(view.toggleBookmark).toHaveBeenCalled();
-                expect($bookmark_icon.hasClass('button-on')).toBeFalsy();
-                expect(_converse.bookmarks.length).toBe(0);
-
-                // Check that an IQ stanza is sent out, containing no
-                // conferences to bookmark (since we removed the one and
-                // only bookmark).
-                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'/>"+
-                                "</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>"
-                );
-                done();
             }));
         });
 
         describe("and when autojoin is set", function () {
 
-            it("will be be opened and joined automatically upon login", mock.initConverse(function (_converse) {
-                spyOn(_converse.api.rooms, 'open');
-                var jid = 'theplay@conference.shakespeare.lit';
-                var model = _converse.bookmarks.create({
-                    'jid': jid,
-                    'autojoin': false,
-                    'name':  'The Play',
-                    'nick': ''
-                });
-                expect(_converse.api.rooms.open).not.toHaveBeenCalled();
-                _converse.bookmarks.remove(model);
+            it("will be be opened and joined automatically upon login", mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-                _converse.bookmarks.create({
-                    'jid': jid,
-                    'autojoin': true,
-                    'name':  'Hamlet',
-                    'nick': ''
+                test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                ).then(function () {
+                    spyOn(_converse.api.rooms, 'open');
+                    var jid = 'theplay@conference.shakespeare.lit';
+                    var model = _converse.bookmarks.create({
+                        'jid': jid,
+                        'autojoin': false,
+                        'name':  'The Play',
+                        'nick': ''
+                    });
+                    expect(_converse.api.rooms.open).not.toHaveBeenCalled();
+                    _converse.bookmarks.remove(model);
+
+                    _converse.bookmarks.create({
+                        'jid': jid,
+                        'autojoin': true,
+                        'name':  'Hamlet',
+                        'nick': ''
+                    });
+                    expect(_converse.api.rooms.open).toHaveBeenCalled();
+                    done();
                 });
-                expect(_converse.api.rooms.open).toHaveBeenCalled();
             }));
         });
     });
 
     describe("Bookmarks", function () {
 
-        xit("can be pushed from the XMPP server", mock.initConverse(function (_converse) {
-            // 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 pushed from the XMPP server", mock.initConverseWithPromises(
+            ['send'], ['rosterGroupsFetched', 'connected'], {},
+            function (done, _converse) {
 
-        it("can be retrieved from the XMPP server", mock.initConverseWithPromises(
-            ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
-
-            /* 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>
-             */
-            var IQ_id;
-            expect(_.filter(_converse.connection.send.calls.all(), function (call) {
-                var stanza = call.args[0];
-                if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
-                    return;
-                }
-                // XXX: Wrapping in a div is a workaround for PhantomJS
-                var div = document.createElement('div');
-                div.appendChild(stanza);
-                if (div.innerHTML ===
-                    '<iq from="dummy@localhost/resource" type="get" '+
-                         'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
-                    '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
-                        '<items node="storage:bookmarks"></items>'+
-                    '</pubsub>'+
-                    '</iq>') {
-                    IQ_id = stanza.getAttribute('id');
-                    return true;
-                }
-            }).length).toBe(1);
-
-            /*
-             * 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').up().up()
-                                .c('conference', {
-                                    'name': 'Another room',
-                                    'autojoin': 'false',
-                                    'jid': 'another@conference.shakespeare.lit'
-                                }).c('nick').t('JC').up().up();
-            _converse.connection._dataRecv(test_utils.createRequest(stanza));
-            expect(_converse.bookmarks.models.length).toBe(2);
-            expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true);
-            expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
-            done();
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.bare_jid,
+                [{'category': 'pubsub', 'type': 'pep'}],
+                ['http://jabber.org/protocol/pubsub#publish-options']
+            ).then(function () {
+                test_utils.openControlBox().openRoomsPanel(_converse);
+                test_utils.waitUntil(function () {
+                    return _converse.bookmarks;
+                }, 300).then(function () {
+                    /* 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>
+                    */
+                    var stanza = $msg({
+                        'from': 'dummy@localhost',
+                        'to': 'dummy@localhost/resource',
+                        'type': 'headline',
+                        'id': 'rnfoo1'
+                    }).c('event', {'xmlns': 'http://jabber.org/protocol/pubsub#event'})
+                        .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.length).toBe(1);
+                    expect(_converse.chatboxviews.get('theplay@conference.shakespeare.lit')).not.toBeUndefined();
+                    done();
+                });
+            });
         }));
 
-        describe("The rooms panel", function () {
-
-            it("shows a list of bookmarks", mock.initConverseWithPromises(
-                ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
+        it("can be retrieved from the XMPP server", mock.initConverseWithPromises(
+            ['send'], ['chatBoxesFetched', 'roomsPanelRendered', 'rosterGroupsFetched'], {},
+            function (done, _converse) {
 
-                test_utils.openControlBox().openRoomsPanel(_converse);
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.bare_jid,
+                [{'category': 'pubsub', 'type': 'pep'}],
+                ['http://jabber.org/protocol/pubsub#publish-options']
+            ).then(function () {
+                /* 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>
+                */
                 var IQ_id;
                 expect(_.filter(_converse.connection.send.calls.all(), function (call) {
                     var stanza = call.args[0];
@@ -411,6 +409,28 @@
                     }
                 }).length).toBe(1);
 
+                /*
+                * 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'})
@@ -418,97 +438,160 @@
                                 .c('storage', {'xmlns': 'storage:bookmarks'})
                                     .c('conference', {
                                         'name': 'The Play&apos;s the Thing',
-                                        'autojoin': 'false',
+                                        'autojoin': 'true',
                                         'jid': 'theplay@conference.shakespeare.lit'
                                     }).c('nick').t('JC').up().up()
-                                    .c('conference', {
-                                        'name': '1st Bookmark',
-                                        'autojoin': 'false',
-                                        'jid': 'first@conference.shakespeare.lit'
-                                    }).c('nick').t('JC').up().up()
-                                    .c('conference', {
-                                        'name': 'Bookmark with a very very long name that will be shortened',
-                                        'autojoin': 'false',
-                                        'jid': 'longname@conference.shakespeare.lit'
-                                    }).c('nick').t('JC').up().up()
                                     .c('conference', {
                                         'name': 'Another room',
                                         'autojoin': 'false',
                                         'jid': 'another@conference.shakespeare.lit'
                                     }).c('nick').t('JC').up().up();
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                expect(_converse.bookmarks.models.length).toBe(2);
+                expect(_converse.bookmarks.findWhere({'jid': 'theplay@conference.shakespeare.lit'}).get('autojoin')).toBe(true);
+                expect(_converse.bookmarks.findWhere({'jid': 'another@conference.shakespeare.lit'}).get('autojoin')).toBe(false);
+                done();
+            });
+        }));
 
-                test_utils.waitUntil(function () {
-                    return $('#chatrooms dl.bookmarks dd').length;
-                }, 300).then(function () {
-                    expect($('#chatrooms dl.bookmarks dd').length).toBe(4);
-                    expect($('#chatrooms dl.bookmarks dd a').text().trim()).toBe(
-                        "1st Bookmark  Another room  Bookmark with a very very long name that will be shortened  The Play&apos;s the Thing")
-
-                    spyOn(window, 'confirm').and.returnValue(true);
-                    $('#chatrooms dl.bookmarks dd:nth-child(2) a:nth-child(2)')[0].click();
-                    expect(window.confirm).toHaveBeenCalled();
-
-                    return test_utils.waitUntil(function () {
-                        return $('#chatrooms dl.bookmarks dd a').text().trim() ===
-                            "1st Bookmark  Bookmark with a very very long name that will be shortened  The Play&apos;s the Thing";
-                    }, 300)
-                }).then(done);
-            }));
+        describe("The rooms panel", function () {
 
-            it("remembers the toggle state of the bookmarks list", mock.initConverseWithPromises(
+            it("shows a list of bookmarks", mock.initConverseWithPromises(
                 ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
 
-                var IQ_id;
-                expect(_.filter(_converse.connection.send.calls.all(), function (call) {
-                    var stanza = call.args[0];
-                    if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
-                        return;
-                    }
-                    // XXX: Wrapping in a div is a workaround for PhantomJS
-                    var div = document.createElement('div');
-                    div.appendChild(stanza);
-                    if (div.innerHTML ===
-                        '<iq from="dummy@localhost/resource" type="get" '+
-                            'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
-                        '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
-                            '<items node="storage:bookmarks"></items>'+
-                        '</pubsub>'+
-                        '</iq>') {
-                        IQ_id = stanza.getAttribute('id');
-                        return true;
-                    }
-                }).length).toBe(1);
-
-                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'});
-                _converse.connection._dataRecv(test_utils.createRequest(stanza));
-
-                _converse.bookmarks.create({
-                    'jid': 'theplay@conference.shakespeare.lit',
-                    'autojoin': false,
-                    'name':  'The Play',
-                    'nick': ''
+                test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                ).then(function () {
+
+                    test_utils.openControlBox().openRoomsPanel(_converse);
+                    var IQ_id;
+                    expect(_.filter(_converse.connection.send.calls.all(), function (call) {
+                        var stanza = call.args[0];
+                        if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
+                            return;
+                        }
+                        // XXX: Wrapping in a div is a workaround for PhantomJS
+                        var div = document.createElement('div');
+                        div.appendChild(stanza);
+                        if (div.innerHTML ===
+                            '<iq from="dummy@localhost/resource" type="get" '+
+                                'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
+                            '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
+                                '<items node="storage:bookmarks"></items>'+
+                            '</pubsub>'+
+                            '</iq>') {
+                            IQ_id = stanza.getAttribute('id');
+                            return true;
+                        }
+                    }).length).toBe(1);
+
+                    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': 'false',
+                                            'jid': 'theplay@conference.shakespeare.lit'
+                                        }).c('nick').t('JC').up().up()
+                                        .c('conference', {
+                                            'name': '1st Bookmark',
+                                            'autojoin': 'false',
+                                            'jid': 'first@conference.shakespeare.lit'
+                                        }).c('nick').t('JC').up().up()
+                                        .c('conference', {
+                                            'name': 'Bookmark with a very very long name that will be shortened',
+                                            'autojoin': 'false',
+                                            'jid': 'longname@conference.shakespeare.lit'
+                                        }).c('nick').t('JC').up().up()
+                                        .c('conference', {
+                                            'name': 'Another room',
+                                            'autojoin': 'false',
+                                            'jid': 'another@conference.shakespeare.lit'
+                                        }).c('nick').t('JC').up().up();
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                    test_utils.waitUntil(function () {
+                        return $('#chatrooms dl.bookmarks dd').length;
+                    }, 300).then(function () {
+                        expect($('#chatrooms dl.bookmarks dd').length).toBe(4);
+                        expect($('#chatrooms dl.bookmarks dd a').text().trim()).toBe(
+                            "1st Bookmark  Another room  Bookmark with a very very long name that will be shortened  The Play&apos;s the Thing")
+
+                        spyOn(window, 'confirm').and.returnValue(true);
+                        $('#chatrooms dl.bookmarks dd:nth-child(2) a:nth-child(2)')[0].click();
+                        expect(window.confirm).toHaveBeenCalled();
+
+                        return test_utils.waitUntil(function () {
+                            return $('#chatrooms dl.bookmarks dd a').text().trim() ===
+                                "1st Bookmark  Bookmark with a very very long name that will be shortened  The Play&apos;s the Thing";
+                        }, 300)
+                    }).then(done);
                 });
-                test_utils.openControlBox().openRoomsPanel(_converse);
+            }));
 
-                test_utils.waitUntil(function () {
-                    return $('#chatrooms dl.bookmarks dd:visible').length;
-                }, 300).then(function () {
-                    expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeFalsy();
-                    expect($('#chatrooms dl.bookmarks dd:visible').length).toBe(1);
-                    expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
-                    $('#chatrooms .bookmarks-toggle')[0].click();
-                    expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeTruthy();
-                    expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED);
-                    $('#chatrooms .bookmarks-toggle')[0].click();
-                    expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeFalsy();
-                    expect($('#chatrooms dl.bookmarks dd:visible').length).toBe(1);
-                    expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
-                    done();
+            it("remembers the toggle state of the bookmarks list", mock.initConverseWithPromises(
+                ['send'], ['rosterGroupsFetched'], {}, function (done, _converse) {
+
+                test_utils.waitUntilDiscoConfirmed(
+                    _converse, _converse.bare_jid,
+                    [{'category': 'pubsub', 'type': 'pep'}],
+                    ['http://jabber.org/protocol/pubsub#publish-options']
+                ).then(function () {
+                    var IQ_id;
+                    expect(_.filter(_converse.connection.send.calls.all(), function (call) {
+                        var stanza = call.args[0];
+                        if (!(stanza instanceof Element) || stanza.nodeName !== 'iq') {
+                            return;
+                        }
+                        // XXX: Wrapping in a div is a workaround for PhantomJS
+                        var div = document.createElement('div');
+                        div.appendChild(stanza);
+                        if (div.innerHTML ===
+                            '<iq from="dummy@localhost/resource" type="get" '+
+                                'xmlns="jabber:client" id="'+stanza.getAttribute('id')+'">'+
+                            '<pubsub xmlns="http://jabber.org/protocol/pubsub">'+
+                                '<items node="storage:bookmarks"></items>'+
+                            '</pubsub>'+
+                            '</iq>') {
+                            IQ_id = stanza.getAttribute('id');
+                            return true;
+                        }
+                    }).length).toBe(1);
+
+                    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'});
+                    _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                    _converse.bookmarks.create({
+                        'jid': 'theplay@conference.shakespeare.lit',
+                        'autojoin': false,
+                        'name':  'The Play',
+                        'nick': ''
+                    });
+                    test_utils.openControlBox().openRoomsPanel(_converse);
+
+                    test_utils.waitUntil(function () {
+                        return $('#chatrooms dl.bookmarks dd:visible').length;
+                    }, 300).then(function () {
+                        expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeFalsy();
+                        expect($('#chatrooms dl.bookmarks dd:visible').length).toBe(1);
+                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
+                        $('#chatrooms .bookmarks-toggle')[0].click();
+                        expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeTruthy();
+                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.CLOSED);
+                        $('#chatrooms .bookmarks-toggle')[0].click();
+                        expect($('#chatrooms dl.bookmarks').hasClass('collapsed')).toBeFalsy();
+                        expect($('#chatrooms dl.bookmarks dd:visible').length).toBe(1);
+                        expect(_converse.bookmarksview.list_model.get('toggle-state')).toBe(_converse.OPENED);
+                        done();
+                    });
                 });
             }));
         });
@@ -521,36 +604,42 @@
             { hide_open_bookmarks: true },
             function (done, _converse) {
 
-            test_utils.openControlBox().openRoomsPanel(_converse);
-            // XXX Create bookmarks view here, otherwise we need to mock stanza
-            // traffic for it to get created.
-            _converse.bookmarksview = new _converse.BookmarksView(
-                {'model': _converse.bookmarks}
-            );
-            _converse.emit('bookmarksInitialized');
-
-            // Check that it's there
-            var jid = 'room@conference.example.org';
-            _converse.bookmarks.create({
-                'jid': jid,
-                'autojoin': false,
-                'name':  'The Play',
-                'nick': ' Othello'
-            });
+            test_utils.waitUntilDiscoConfirmed(
+                _converse, _converse.bare_jid,
+                [{'category': 'pubsub', 'type': 'pep'}],
+                ['http://jabber.org/protocol/pubsub#publish-options']
+            ).then(function () {
+                test_utils.openControlBox().openRoomsPanel(_converse);
+                // XXX Create bookmarks view here, otherwise we need to mock stanza
+                // traffic for it to get created.
+                _converse.bookmarksview = new _converse.BookmarksView(
+                    {'model': _converse.bookmarks}
+                );
+                _converse.emit('bookmarksInitialized');
+
+                // Check that it's there
+                var jid = 'room@conference.example.org';
+                _converse.bookmarks.create({
+                    'jid': jid,
+                    'autojoin': false,
+                    'name':  'The Play',
+                    'nick': ' Othello'
+                });
 
-            expect(_converse.bookmarks.length).toBe(1);
-            var room_els = _converse.bookmarksview.el.querySelectorAll(".open-room");
-            expect(room_els.length).toBe(1);
-
-            // Check that it disappears once the room is opened
-            var bookmark = _converse.bookmarksview.el.querySelector(".open-room");
-            bookmark.click();
-            expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy();
-            // Check that it reappears once the room is closed
-            var view = _converse.chatboxviews.get(jid);
-            view.close();
-            expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeFalsy();
-            done();
+                expect(_converse.bookmarks.length).toBe(1);
+                var room_els = _converse.bookmarksview.el.querySelectorAll(".open-room");
+                expect(room_els.length).toBe(1);
+
+                // Check that it disappears once the room is opened
+                var bookmark = _converse.bookmarksview.el.querySelector(".open-room");
+                bookmark.click();
+                expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeTruthy();
+                // Check that it reappears once the room is closed
+                var view = _converse.chatboxviews.get(jid);
+                view.close();
+                expect(u.hasClass('hidden', _converse.bookmarksview.el.querySelector(".available-chatroom"))).toBeFalsy();
+                done();
+            });
         }));
     });
 }));

+ 5 - 5
spec/chatbox.js

@@ -17,7 +17,7 @@
     var moment = converse.env.moment;
     var u = converse.env.utils;
 
-    return describe("Chatboxes", function() {
+    return describe("Chatboxes", function () {
         describe("A Chatbox", function () {
 
             it("has a /help command to show the available commands",
@@ -57,7 +57,7 @@
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
 
-                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                 .then(function () {
                     return test_utils.waitUntil(function () {
                         return _converse.xmppstatus.get('fullname');
@@ -1228,7 +1228,7 @@
                         function (done, _converse) {
 
                     var contact, sent_stanza, IQ_id, stanza;
-                    test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                    test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                     .then(function () {
                         return test_utils.waitUntil(function () {
                             return _converse.xmppstatus.get('fullname');
@@ -1842,7 +1842,7 @@
                             function (done, _converse) {
 
                         var contact, sent_stanza, IQ_id, stanza;
-                        test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                        test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                         .then(function () {
                             return test_utils.waitUntil(function () {
                                 return _converse.xmppstatus.get('fullname');
@@ -1989,7 +1989,7 @@
                             function (done, _converse) {
 
                         var contact, sent_stanza, IQ_id, stanza;
-                        test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                        test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                         .then(function () {
                             return test_utils.waitUntil(function () {
                                 return _converse.xmppstatus.get('fullname');

+ 189 - 183
spec/chatroom.js

@@ -762,7 +762,6 @@
                 });
                 var view = _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
 
-                spyOn(view, 'generateHeadingHTML').and.callThrough();
                 var features_stanza = $iq({
                         from: 'coven@chat.shakespeare.lit',
                         'id': IQ_id,
@@ -791,10 +790,12 @@
                             .c('field', {'type':'text-single', 'var':'muc#roominfo_occupants', 'label':'Number of occupants'})
                                 .c('value').t(0);
                 _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
-
-                expect(view.generateHeadingHTML).toHaveBeenCalled();
-                expect($(view.el.querySelector('.chatroom-description')).text()).toBe('This is the description');
-                done();
+                test_utils.waitUntil(function () {
+                    return _.get(view.el.querySelector('.chatroom-description'), 'textContent');
+                }).then(function () {
+                    expect($(view.el.querySelector('.chatroom-description')).text()).toBe('This is the description');
+                    done();
+                });
             }));
 
             it("will specially mark messages in which you are mentioned",
@@ -825,8 +826,7 @@
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
 
-
-                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                 .then(function () {
                     return test_utils.waitUntil(function () {
                         return _converse.xmppstatus.get('fullname');
@@ -890,6 +890,7 @@
 
                 _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
                 view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
+
                 spyOn(view, 'saveAffiliationAndRole').and.callThrough();
 
                 // We pretend this is a new room, so no disco info is returned.
@@ -903,13 +904,13 @@
                 _converse.connection._dataRecv(test_utils.createRequest(features_stanza));
 
                 /* <presence to="dummy@localhost/_converse.js-29092160"
-                    *           from="coven@chat.shakespeare.lit/some1">
-                    *      <x xmlns="http://jabber.org/protocol/muc#user">
-                    *          <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
-                    *          <status code="110"/>
-                    *      </x>
-                    *  </presence></body>
-                    */
+                *           from="coven@chat.shakespeare.lit/some1">
+                *      <x xmlns="http://jabber.org/protocol/muc#user">
+                *          <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
+                *          <status code="110"/>
+                *      </x>
+                *  </presence></body>
+                */
                 var presence = $pres({
                         to: 'dummy@localhost/_converse.js-29092160',
                         from: 'coven@chat.shakespeare.lit/some1'
@@ -922,179 +923,184 @@
                     .c('status', {code: '110'});
                 _converse.connection._dataRecv(test_utils.createRequest(presence));
                 expect(view.saveAffiliationAndRole).toHaveBeenCalled();
-                expect($(view.el.querySelector('.configure-chatroom-button')).is(':visible')).toBeTruthy();
                 expect($(view.el.querySelector('.toggle-chatbox-button')).is(':visible')).toBeTruthy();
-                expect($(view.el.querySelector('.toggle-bookmark')).is(':visible')).toBeTruthy();
-                view.el.querySelector('.configure-chatroom-button').click();
-
-                /* Check that an IQ is sent out, asking for the
-                 * configuration form.
-                 * See: // http://xmpp.org/extensions/xep-0045.html#example-163
-                 *
-                 *  <iq from='crone1@shakespeare.lit/desktop'
-                 *      id='config1'
-                 *      to='coven@chat.shakespeare.lit'
-                 *      type='get'>
-                 *  <query xmlns='http://jabber.org/protocol/muc#owner'/>
-                 *  </iq>
-                 */
-                expect(sent_IQ.toLocaleString()).toBe(
-                    "<iq to='coven@chat.shakespeare.lit' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
-                        "<query xmlns='http://jabber.org/protocol/muc#owner'/>"+
-                    "</iq>");
-
-                /* Server responds with the configuration form.
-                 * See: // http://xmpp.org/extensions/xep-0045.html#example-165
-                 */
-                var config_stanza = $iq({from: 'coven@chat.shakespeare.lit',
-                    'id': IQ_id,
-                    'to': 'dummy@localhost/desktop',
-                    'type': 'result'})
-                .c('query', { 'xmlns': 'http://jabber.org/protocol/muc#owner'})
-                    .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form'})
-                        .c('title').t('Configuration for "coven" Room').up()
-                        .c('instructions').t('Complete this form to modify the configuration of your room.').up()
-                        .c('field', {'type': 'hidden', 'var': 'FORM_TYPE'})
-                            .c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up()
-                        .c('field', {
-                            'label': 'Natural-Language Room Name',
-                            'type': 'text-single',
-                            'var': 'muc#roomconfig_roomname'})
-                            .c('value').t('A Dark Cave').up().up()
-                        .c('field', {
-                            'label': 'Short Description of Room',
-                            'type': 'text-single',
-                            'var': 'muc#roomconfig_roomdesc'})
-                            .c('value').t('The place for all good witches!').up().up()
-                        .c('field', {
-                            'label': 'Enable Public Logging?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_enablelogging'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Allow Occupants to Change Subject?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_changesubject'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Allow Occupants to Invite Others?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_allowinvites'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Who Can Send Private Messages?',
-                            'type': 'list-single',
-                            'var': 'muc#roomconfig_allowpm'})
-                            .c('value').t('anyone').up()
-                            .c('option', {'label': 'Anyone'})
-                                .c('value').t('anyone').up().up()
-                            .c('option', {'label': 'Anyone with Voice'})
-                                .c('value').t('participants').up().up()
-                            .c('option', {'label': 'Moderators Only'})
-                                .c('value').t('moderators').up().up()
-                            .c('option', {'label': 'Nobody'})
-                                .c('value').t('none').up().up().up()
-                        .c('field', {
-                            'label': 'Roles for which Presence is Broadcasted',
-                            'type': 'list-multi',
-                            'var': 'muc#roomconfig_presencebroadcast'})
-                            .c('value').t('moderator').up()
-                            .c('value').t('participant').up()
-                            .c('value').t('visitor').up()
-                            .c('option', {'label': 'Moderator'})
-                                .c('value').t('moderator').up().up()
-                            .c('option', {'label': 'Participant'})
-                                .c('value').t('participant').up().up()
-                            .c('option', {'label': 'Visitor'})
-                                .c('value').t('visitor').up().up().up()
-                        .c('field', {
-                            'label': 'Roles and Affiliations that May Retrieve Member List',
-                            'type': 'list-multi',
-                            'var': 'muc#roomconfig_getmemberlist'})
-                            .c('value').t('moderator').up()
-                            .c('value').t('participant').up()
-                            .c('value').t('visitor').up()
-                            .c('option', {'label': 'Moderator'})
-                                .c('value').t('moderator').up().up()
-                            .c('option', {'label': 'Participant'})
-                                .c('value').t('participant').up().up()
-                            .c('option', {'label': 'Visitor'})
-                                .c('value').t('visitor').up().up().up()
-                        .c('field', {
-                            'label': 'Make Room Publicly Searchable?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_publicroom'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Make Room Publicly Searchable?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_publicroom'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Make Room Persistent?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_persistentroom'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Make Room Moderated?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_moderatedroom'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Make Room Members Only?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_membersonly'})
-                            .c('value').t(0).up().up()
-                        .c('field', {
-                            'label': 'Password Required for Entry?',
-                            'type': 'boolean',
-                            'var': 'muc#roomconfig_passwordprotectedroom'})
-                            .c('value').t(1).up().up()
-                        .c('field', {'type': 'fixed'})
-                            .c('value').t('If a password is required to enter this room,'+
-                                        'you must specify the password below.').up().up()
-                        .c('field', {
-                            'label': 'Password',
-                            'type': 'text-private',
-                            'var': 'muc#roomconfig_roomsecret'})
-                            .c('value').t('cauldronburn');
-                _converse.connection._dataRecv(test_utils.createRequest(config_stanza));
 
                 test_utils.waitUntil(function () {
-                    return $(view.el.querySelector('form.chatroom-form')).length;
+                    return !_.isNull(view.el.querySelector('.configure-chatroom-button'));
                 }, 300).then(function () {
-                    expect($(view.el.querySelector('form.chatroom-form')).length).toBe(1);
-                    expect(view.el.querySelectorAll('form.chatroom-form fieldset').length).toBe(2);
-                    var $membersonly = $(view.el.querySelector('input[name="muc#roomconfig_membersonly"]'));
-                    expect($membersonly.length).toBe(1);
-                    expect($membersonly.attr('type')).toBe('checkbox');
-                    $membersonly.prop('checked', true);
-
-                    var $moderated = $(view.el.querySelector('input[name="muc#roomconfig_moderatedroom"]'));
-                    expect($moderated.length).toBe(1);
-                    expect($moderated.attr('type')).toBe('checkbox');
-                    $moderated.prop('checked', true);
-
-                    var $password = $(view.el.querySelector('input[name="muc#roomconfig_roomsecret"]'));
-                    expect($password.length).toBe(1);
-                    expect($password.attr('type')).toBe('password');
-
-                    var $allowpm = $(view.el.querySelector('select[name="muc#roomconfig_allowpm"]'));
-                    expect($allowpm.length).toBe(1);
-                    $allowpm.val('moderators');
-
-                    var $presencebroadcast = $(view.el.querySelector('select[name="muc#roomconfig_presencebroadcast"]'));
-                    expect($presencebroadcast.length).toBe(1);
-                    $presencebroadcast.val(['moderator']);
-
-                    view.el.querySelector('input[type="submit"]').click();
-
-                    var $sent_stanza = $(sent_IQ.toLocaleString());
-                    expect($sent_stanza.find('field[var="muc#roomconfig_membersonly"] value').text()).toBe('1');
-                    expect($sent_stanza.find('field[var="muc#roomconfig_moderatedroom"] value').text()).toBe('1');
-                    expect($sent_stanza.find('field[var="muc#roomconfig_allowpm"] value').text()).toBe('moderators');
-                    expect($sent_stanza.find('field[var="muc#roomconfig_presencebroadcast"] value').text()).toBe('moderator');
-                    done();
+                    expect($(view.el.querySelector('.configure-chatroom-button')).is(':visible')).toBeTruthy();
+
+                    view.el.querySelector('.configure-chatroom-button').click();
+
+                    /* Check that an IQ is sent out, asking for the
+                    * configuration form.
+                    * See: // http://xmpp.org/extensions/xep-0045.html#example-163
+                    *
+                    *  <iq from='crone1@shakespeare.lit/desktop'
+                    *      id='config1'
+                    *      to='coven@chat.shakespeare.lit'
+                    *      type='get'>
+                    *  <query xmlns='http://jabber.org/protocol/muc#owner'/>
+                    *  </iq>
+                    */
+                    expect(sent_IQ.toLocaleString()).toBe(
+                        "<iq to='coven@chat.shakespeare.lit' type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
+                            "<query xmlns='http://jabber.org/protocol/muc#owner'/>"+
+                        "</iq>");
+
+                    /* Server responds with the configuration form.
+                    * See: // http://xmpp.org/extensions/xep-0045.html#example-165
+                    */
+                    var config_stanza = $iq({from: 'coven@chat.shakespeare.lit',
+                        'id': IQ_id,
+                        'to': 'dummy@localhost/desktop',
+                        'type': 'result'})
+                    .c('query', { 'xmlns': 'http://jabber.org/protocol/muc#owner'})
+                        .c('x', { 'xmlns': 'jabber:x:data', 'type': 'form'})
+                            .c('title').t('Configuration for "coven" Room').up()
+                            .c('instructions').t('Complete this form to modify the configuration of your room.').up()
+                            .c('field', {'type': 'hidden', 'var': 'FORM_TYPE'})
+                                .c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up()
+                            .c('field', {
+                                'label': 'Natural-Language Room Name',
+                                'type': 'text-single',
+                                'var': 'muc#roomconfig_roomname'})
+                                .c('value').t('A Dark Cave').up().up()
+                            .c('field', {
+                                'label': 'Short Description of Room',
+                                'type': 'text-single',
+                                'var': 'muc#roomconfig_roomdesc'})
+                                .c('value').t('The place for all good witches!').up().up()
+                            .c('field', {
+                                'label': 'Enable Public Logging?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_enablelogging'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Allow Occupants to Change Subject?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_changesubject'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Allow Occupants to Invite Others?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_allowinvites'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Who Can Send Private Messages?',
+                                'type': 'list-single',
+                                'var': 'muc#roomconfig_allowpm'})
+                                .c('value').t('anyone').up()
+                                .c('option', {'label': 'Anyone'})
+                                    .c('value').t('anyone').up().up()
+                                .c('option', {'label': 'Anyone with Voice'})
+                                    .c('value').t('participants').up().up()
+                                .c('option', {'label': 'Moderators Only'})
+                                    .c('value').t('moderators').up().up()
+                                .c('option', {'label': 'Nobody'})
+                                    .c('value').t('none').up().up().up()
+                            .c('field', {
+                                'label': 'Roles for which Presence is Broadcasted',
+                                'type': 'list-multi',
+                                'var': 'muc#roomconfig_presencebroadcast'})
+                                .c('value').t('moderator').up()
+                                .c('value').t('participant').up()
+                                .c('value').t('visitor').up()
+                                .c('option', {'label': 'Moderator'})
+                                    .c('value').t('moderator').up().up()
+                                .c('option', {'label': 'Participant'})
+                                    .c('value').t('participant').up().up()
+                                .c('option', {'label': 'Visitor'})
+                                    .c('value').t('visitor').up().up().up()
+                            .c('field', {
+                                'label': 'Roles and Affiliations that May Retrieve Member List',
+                                'type': 'list-multi',
+                                'var': 'muc#roomconfig_getmemberlist'})
+                                .c('value').t('moderator').up()
+                                .c('value').t('participant').up()
+                                .c('value').t('visitor').up()
+                                .c('option', {'label': 'Moderator'})
+                                    .c('value').t('moderator').up().up()
+                                .c('option', {'label': 'Participant'})
+                                    .c('value').t('participant').up().up()
+                                .c('option', {'label': 'Visitor'})
+                                    .c('value').t('visitor').up().up().up()
+                            .c('field', {
+                                'label': 'Make Room Publicly Searchable?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_publicroom'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Make Room Publicly Searchable?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_publicroom'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Make Room Persistent?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_persistentroom'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Make Room Moderated?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_moderatedroom'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Make Room Members Only?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_membersonly'})
+                                .c('value').t(0).up().up()
+                            .c('field', {
+                                'label': 'Password Required for Entry?',
+                                'type': 'boolean',
+                                'var': 'muc#roomconfig_passwordprotectedroom'})
+                                .c('value').t(1).up().up()
+                            .c('field', {'type': 'fixed'})
+                                .c('value').t('If a password is required to enter this room,'+
+                                            'you must specify the password below.').up().up()
+                            .c('field', {
+                                'label': 'Password',
+                                'type': 'text-private',
+                                'var': 'muc#roomconfig_roomsecret'})
+                                .c('value').t('cauldronburn');
+                    _converse.connection._dataRecv(test_utils.createRequest(config_stanza));
+
+                    test_utils.waitUntil(function () {
+                        return $(view.el.querySelector('form.chatroom-form')).length;
+                    }, 300).then(function () {
+                        expect($(view.el.querySelector('form.chatroom-form')).length).toBe(1);
+                        expect(view.el.querySelectorAll('form.chatroom-form fieldset').length).toBe(2);
+                        var $membersonly = $(view.el.querySelector('input[name="muc#roomconfig_membersonly"]'));
+                        expect($membersonly.length).toBe(1);
+                        expect($membersonly.attr('type')).toBe('checkbox');
+                        $membersonly.prop('checked', true);
+
+                        var $moderated = $(view.el.querySelector('input[name="muc#roomconfig_moderatedroom"]'));
+                        expect($moderated.length).toBe(1);
+                        expect($moderated.attr('type')).toBe('checkbox');
+                        $moderated.prop('checked', true);
+
+                        var $password = $(view.el.querySelector('input[name="muc#roomconfig_roomsecret"]'));
+                        expect($password.length).toBe(1);
+                        expect($password.attr('type')).toBe('password');
+
+                        var $allowpm = $(view.el.querySelector('select[name="muc#roomconfig_allowpm"]'));
+                        expect($allowpm.length).toBe(1);
+                        $allowpm.val('moderators');
+
+                        var $presencebroadcast = $(view.el.querySelector('select[name="muc#roomconfig_presencebroadcast"]'));
+                        expect($presencebroadcast.length).toBe(1);
+                        $presencebroadcast.val(['moderator']);
+
+                        view.el.querySelector('input[type="submit"]').click();
+
+                        var $sent_stanza = $(sent_IQ.toLocaleString());
+                        expect($sent_stanza.find('field[var="muc#roomconfig_membersonly"] value').text()).toBe('1');
+                        expect($sent_stanza.find('field[var="muc#roomconfig_moderatedroom"] value').text()).toBe('1');
+                        expect($sent_stanza.find('field[var="muc#roomconfig_allowpm"] value').text()).toBe('moderators');
+                        expect($sent_stanza.find('field[var="muc#roomconfig_presencebroadcast"] value').text()).toBe('moderator');
+                        done();
+                    });
                 }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             }));
 

+ 2 - 2
spec/disco.js

@@ -79,7 +79,7 @@
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
                     var entities = _converse.disco_entities;
-                    expect(entities.length).toBe(1);
+                    expect(entities.length).toBe(2); // We have an extra entity, which is the user's JID
                     expect(entities.get(_converse.domain).features.length).toBe(5);
                     expect(entities.get(_converse.domain).identities.length).toBe(3);
                     expect(entities.get('localhost').features.where({'var': 'jabber:iq:version'}).length).toBe(1);
@@ -159,7 +159,7 @@
                     _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
                     entities = _converse.disco_entities;
-                    expect(entities.length).toBe(4);
+                    expect(entities.length).toBe(5); // We have an extra entity, which is the user's JID
                     expect(entities.get(_converse.domain).identities.where({'category': 'conference'}).length).toBe(1);
                     expect(entities.get(_converse.domain).identities.where({'category': 'directory'}).length).toBe(1);
                     done();

+ 66 - 1
spec/otr.js

@@ -3,8 +3,73 @@
 } (this, function ($, jasmine, mock, converse, test_utils) {
     var Strophe = converse.env.Strophe;
     var b64_sha1 = converse.env.b64_sha1;
+    var $pres = converse.env.$pres;
+    var _ = converse.env._;
 
-    return describe("The OTR module", function() {
+    describe("A chatbox with an active OTR session", function() {
+
+        it("will not show the spoiler toolbar button",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+            // XXX: We need to send a presence from the contact, so that we
+            // have a resource, that resource is then queried to see
+            // whether Strophe.NS.SPOILER is supported, in which case
+            // the spoiler button will appear.
+            var presence = $pres({
+                'from': contact_jid+'/phone',
+                'to': 'dummy@localhost'
+            });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+            test_utils.openChatBoxFor(_converse, contact_jid);
+
+            test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]).then(function () {
+                var spoiler_toggle;
+                var view = _converse.chatboxviews.get(contact_jid);
+                spyOn(view, 'addSpoilerButton').and.callThrough();
+                view.model.set('otr_status', 1);
+
+                test_utils.waitUntil(function () {
+                    return _.isNull(view.el.querySelector('.toggle-compose-spoiler'));
+                }).then(function () {
+                    spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                    expect(spoiler_toggle).toBe(null);
+
+                    view.model.set('otr_status', 3);
+
+                    return test_utils.waitUntil(function () {
+                        return !_.isNull(view.el.querySelector('.toggle-compose-spoiler'));
+                    });
+                }).then(function () {
+                    spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                    expect(spoiler_toggle).not.toBe(null);
+
+                    view.model.set('otr_status', 2);
+                    return test_utils.waitUntil(function () {
+                        return _.isNull(view.el.querySelector('.toggle-compose-spoiler'));
+                    });
+                }).then(function () {
+                    spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                    expect(spoiler_toggle).toBe(null);
+
+                    view.model.set('otr_status', 4);
+                    return test_utils.waitUntil(function () {
+                        return !_.isNull(view.el.querySelector('.toggle-compose-spoiler'));
+                    });
+                }).then(function () {
+                    spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                    expect(spoiler_toggle).not.toBe(null);
+                    done();
+                });
+            });
+        }));
+    });
+
+    describe("The OTR module", function() {
 
         it("will add processing hints to sent out encrypted <message> stanzas",
             mock.initConverseWithPromises(

+ 1 - 1
spec/protocol.js

@@ -55,7 +55,7 @@
                     function (done, _converse) {
 
                 var contact, sent_stanza, IQ_id, stanza;
-                test_utils.waitUntilFeatureSupportConfirmed(_converse, 'vcard-temp')
+                test_utils.waitUntilDiscoConfirmed(_converse, 'localhost', [], ['vcard-temp'])
                 .then(function () {
                     return test_utils.waitUntil(function () {
                         return _converse.xmppstatus.get('fullname');

+ 240 - 0
spec/spoilers.js

@@ -0,0 +1,240 @@
+(function (root, factory) {
+    define([
+        "jasmine",
+        "utils",
+        "mock",
+        "converse-core",
+        "test-utils"
+        ], factory);
+} (this, function (jasmine, utils, mock, converse, test_utils) {
+
+    var _ = converse.env._;
+    var Strophe = converse.env.Strophe;
+    var $msg = converse.env.$msg;
+    var $pres = converse.env.$pres;
+    var u = converse.env.utils;
+
+    return describe("A spoiler message", function () {
+
+        it("can be received with a hint",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+            /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
+             *      <body>And at the end of the story, both of them die! It is so tragic!</body>
+             *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
+             *  </message>
+             */
+            var spoiler_hint = "Love story end"
+            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            var msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'
+                }).c('body').t(spoiler).up()
+                  .c('spoiler', {
+                      'xmlns': 'urn:xmpp:spoiler:0',
+                    }).t(spoiler_hint)
+                .tree();
+            _converse.chatboxes.onMessage(msg);
+
+            var view = _converse.chatboxviews.get(sender_jid);
+            var message_content = view.el.querySelector('.chat-msg-content');
+            expect(message_content.textContent).toBe(spoiler);
+
+            var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe(spoiler_hint);
+            done();
+        }));
+
+        it("can be received without a hint",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            var sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+            /* <message to='romeo@montague.net/orchard' from='juliet@capulet.net/balcony' id='spoiler2'>
+             *      <body>And at the end of the story, both of them die! It is so tragic!</body>
+             *      <spoiler xmlns='urn:xmpp:spoiler:0'>Love story end</spoiler>
+             *  </message>
+             */
+            var spoiler = "And at the end of the story, both of them die! It is so tragic!";
+            var msg = $msg({
+                    'xmlns': 'jabber:client',
+                    'to': _converse.bare_jid,
+                    'from': sender_jid,
+                    'type': 'chat'
+                }).c('body').t(spoiler).up()
+                  .c('spoiler', {
+                      'xmlns': 'urn:xmpp:spoiler:0',
+                    }).tree();
+            _converse.chatboxes.onMessage(msg);
+
+            var view = _converse.chatboxviews.get(sender_jid);
+            var message_content = view.el.querySelector('.chat-msg-content');
+            expect(message_content.textContent).toBe(spoiler);
+
+            var spoiler_hint_el = view.el.querySelector('.spoiler-hint');
+            expect(spoiler_hint_el.textContent).toBe('');
+            done();
+        }));
+
+        it("can be sent without a hint",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
+            test_utils.openContactsPanel(_converse);
+            var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+            // XXX: We need to send a presence from the contact, so that we
+            // have a resource, that resource is then queried to see
+            // whether Strophe.NS.SPOILER is supported, in which case
+            // the spoiler button will appear.
+            var presence = $pres({
+                'from': contact_jid+'/phone',
+                'to': 'dummy@localhost'
+            });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+            test_utils.openChatBoxFor(_converse, contact_jid);
+
+            test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]).then(function () {
+                var view = _converse.chatboxviews.get(contact_jid);
+                spyOn(view, 'onMessageSubmitted').and.callThrough();
+                spyOn(_converse.connection, 'send');
+
+                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                spoiler_toggle.click();
+
+                var textarea = view.el.querySelector('.chat-textarea');
+                textarea.value = 'This is the spoiler';
+                view.keyPressed({
+                    target: textarea,
+                    preventDefault: _.noop,
+                    keyCode: 13
+                });
+                expect(view.onMessageSubmitted).toHaveBeenCalled();
+
+                /* Test the XML stanza 
+                *
+                * <message from="dummy@localhost/resource"
+                *          to="max.frankfurter@localhost"
+                *          type="chat"
+                *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+                *          xmlns="jabber:client">
+                *    <body>This is the spoiler</body>
+                *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+                *    <spoiler xmlns="urn:xmpp:spoiler:0"/>
+                * </message>"
+                */
+                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+                expect(_.isNull(spoiler_el)).toBeFalsy();
+                expect(spoiler_el.textContent).toBe('');
+
+                var body_el = stanza.querySelector('body');
+                expect(body_el.textContent).toBe('This is the spoiler');
+
+                /* Test the HTML spoiler message */
+                var spoiler_msg_el = view.el.querySelector('.chat-msg-content.spoiler');
+                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+                spoiler_toggle = view.el.querySelector('.toggle-spoiler');
+                expect(spoiler_toggle.textContent).toBe('Show hidden message');
+                spoiler_toggle.click();
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+                expect(spoiler_toggle.textContent).toBe('Hide hidden message');
+                spoiler_toggle.click();
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+                done();
+            });
+        }));
+
+        it("can be sent with a hint",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            test_utils.createContacts(_converse, 'current');
+            test_utils.openControlBox();
+            test_utils.openContactsPanel(_converse);
+            var contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+
+            // XXX: We need to send a presence from the contact, so that we
+            // have a resource, that resource is then queried to see
+            // whether Strophe.NS.SPOILER is supported, in which case
+            // the spoiler button will appear.
+            var presence = $pres({
+                'from': contact_jid+'/phone',
+                'to': 'dummy@localhost'
+            });
+            _converse.connection._dataRecv(test_utils.createRequest(presence));
+            test_utils.openChatBoxFor(_converse, contact_jid);
+
+            test_utils.waitUntilDiscoConfirmed(_converse, contact_jid+'/phone', [], [Strophe.NS.SPOILER]).then(function () {
+                var view = _converse.chatboxviews.get(contact_jid);
+                var spoiler_toggle = view.el.querySelector('.toggle-compose-spoiler');
+                spoiler_toggle.click();
+
+                spyOn(view, 'onMessageSubmitted').and.callThrough();
+                spyOn(_converse.connection, 'send');
+
+                var textarea = view.el.querySelector('.chat-textarea');
+                textarea.value = 'This is the spoiler';
+                var hint_input = view.el.querySelector('.spoiler-hint');
+                hint_input.value = 'This is the hint';
+
+                view.keyPressed({
+                    target: textarea,
+                    preventDefault: _.noop,
+                    keyCode: 13
+                });
+                expect(view.onMessageSubmitted).toHaveBeenCalled();
+
+                /* Test the XML stanza 
+                *
+                * <message from="dummy@localhost/resource"
+                *          to="max.frankfurter@localhost"
+                *          type="chat"
+                *          id="4547c38b-d98b-45a5-8f44-b4004dbc335e"
+                *          xmlns="jabber:client">
+                *    <body>This is the spoiler</body>
+                *    <active xmlns="http://jabber.org/protocol/chatstates"/>
+                *    <spoiler xmlns="urn:xmpp:spoiler:0">This is the hint</spoiler>
+                * </message>"
+                */
+                var stanza = _converse.connection.send.calls.argsFor(0)[0].tree();
+                var spoiler_el = stanza.querySelector('spoiler[xmlns="urn:xmpp:spoiler:0"]');
+                expect(_.isNull(spoiler_el)).toBeFalsy();
+                expect(spoiler_el.textContent).toBe('This is the hint');
+
+                var body_el = stanza.querySelector('body');
+                expect(body_el.textContent).toBe('This is the spoiler');
+
+                /* Test the HTML spoiler message */
+                var spoiler_msg_el = view.el.querySelector('.chat-msg-content.spoiler');
+                expect(spoiler_msg_el.textContent).toBe('This is the spoiler');
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+
+                spoiler_toggle = view.el.querySelector('.toggle-spoiler');
+                expect(spoiler_toggle.textContent).toBe('Show hidden message');
+                spoiler_toggle.click();
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeFalsy();
+                expect(spoiler_toggle.textContent).toBe('Hide hidden message');
+                spoiler_toggle.click();
+                expect(_.includes(spoiler_msg_el.classList, 'collapsed')).toBeTruthy();
+                done();
+            });
+        }));
+    });
+}));

+ 69 - 31
src/converse-bookmarks.js

@@ -71,26 +71,29 @@
                     this.setBookmarkState();
                 },
 
-                generateHeadingHTML () {
+                renderBookmarkToggle () {
                     const { _converse } = this.__super__,
-                        { __ } = _converse,
-                        html = this.__super__.generateHeadingHTML.apply(this, arguments);
+                          { __ } = _converse;
+                    const bookmark_button = tpl_chatroom_bookmark_toggle(
+                        _.assignIn(this.model.toJSON(), {
+                            info_toggle_bookmark: __('Bookmark this room'),
+                            bookmarked: this.model.get('bookmarked')
+                        }));
+                    const close_button = this.el.querySelector('.close-chatbox-button');
+                    close_button.insertAdjacentHTML('afterend', bookmark_button);
+                },
+
+                renderHeading () {
+                    this.__super__.renderHeading.apply(this, arguments);
+                    const { _converse } = this.__super__;
                     if (_converse.allow_bookmarks) {
-                        const div = document.createElement('div');
-                        div.innerHTML = html;
-                        const bookmark_button = tpl_chatroom_bookmark_toggle(
-                            _.assignIn(
-                                this.model.toJSON(),
-                                {
-                                    info_toggle_bookmark: __('Bookmark this room'),
-                                    bookmarked: this.model.get('bookmarked')
-                                }
-                            ));
-                        const close_button = div.querySelector('.close-chatbox-button');
-                        close_button.insertAdjacentHTML('afterend', bookmark_button);
-                        return div.innerHTML;
+                        _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid).then((identity) => {
+                            if (_.isNil(identity)) {
+                                return;
+                            }
+                            this.renderBookmarkToggle();
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     }
-                    return html;
                 },
 
                 checkForReservedNick () {
@@ -112,6 +115,9 @@
 
                 onBookmarked () {
                     const icon = this.el.querySelector('.icon-pushpin');
+                    if (_.isNull(icon)) {
+                        return;
+                    }
                     if (this.model.get('bookmarked')) {
                         icon.classList.add('button-on');
                     } else {
@@ -145,7 +151,7 @@
                     _.each(body.querySelectorAll('.chatroom-form-container'), u.removeElement);
 
                     body.insertAdjacentHTML(
-                        'beforeend', 
+                        'beforeend',
                         tpl_chatroom_bookmark_form({
                             heading: __('Bookmark this room'),
                             label_name: __('The name for this bookmark:'),
@@ -212,7 +218,8 @@
             // configuration settings.
             _converse.api.settings.update({
                 allow_bookmarks: true,
-                hide_open_bookmarks: true 
+                allow_public_bookmarks: false,
+                hide_open_bookmarks: true
             });
             // Promises exposed by this plugin
             _converse.api.promises.add('bookmarksInitialized');
@@ -360,10 +367,13 @@
                     }
                 },
 
-                onBookmarksReceived (deferred, iq) {
+                createBookmarksFromStanza (stanza) {
                     const bookmarks = sizzle(
-                        'items[node="storage:bookmarks"] item[id="current"] storage conference',
-                        iq
+                        'items[node="storage:bookmarks"] '+
+                        'item#current '+
+                        'storage[xmlns="storage:bookmarks"] '+
+                        'conference',
+                        stanza
                     )
                     _.forEach(bookmarks, (bookmark) => {
                         this.create({
@@ -373,6 +383,10 @@
                             'nick': bookmark.querySelector('nick').textContent
                         });
                     });
+                },
+
+                onBookmarksReceived (deferred, iq) {
+                    this.createBookmarksFromStanza(iq);
                     if (!_.isUndefined(deferred)) {
                         return deferred.resolve();
                     }
@@ -464,7 +478,7 @@
                 insertIntoControlBox () {
                     const controlboxview = _converse.chatboxviews.get('controlbox');
                     if (!_.isUndefined(controlboxview) &&
-                            !document.body.contains(this.el)) {
+                            !_converse.root.contains(this.el)) {
                         const container = controlboxview.el.querySelector('#chatrooms');
                         if (!_.isNull(container)) {
                             container.insertBefore(this.el, container.firstChild);
@@ -520,14 +534,28 @@
                 if (!_converse.allow_bookmarks) {
                     return;
                 }
-                _converse.bookmarks = new _converse.Bookmarks();
-                _converse.bookmarks.fetchBookmarks().then(() => {
-                    _converse.bookmarksview = new _converse.BookmarksView(
-                        {'model': _converse.bookmarks}
-                    );
-                })
-                .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR))
-                .then(() => {
+                Promise.all([
+                    _converse.api.disco.getIdentity('pubsub', 'pep', _converse.bare_jid),
+                    _converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid)
+                ]).then((args) => {
+                    const identity = args[0],
+                          options_support = args[1];
+
+                    if (_.isNil(identity) || (!options_support.supported && !_converse.allow_public_bookmarks)) {
+                        _converse.emit('bookmarksInitialized');
+                        return;
+                    }
+                    _converse.bookmarks = new _converse.Bookmarks();
+                    _converse.bookmarks.fetchBookmarks().then(() => {
+                        _converse.bookmarksview = new _converse.BookmarksView(
+                            {'model': _converse.bookmarks}
+                        );
+                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR))
+                      .then(() => {
+                          _converse.emit('bookmarksInitialized');
+                      });
+                }).catch((e) => {
+                    _converse.log(e, Strophe.LogLevel.ERROR);
                     _converse.emit('bookmarksInitialized');
                 });
             };
@@ -538,6 +566,16 @@
             ]).then(initBookmarks)
               .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
 
+            _converse.on('connected', () => {
+                // Add a handler for bookmarks pushed from other connected clients
+                // (from the same user obviously)
+                _converse.connection.addHandler((message) => {
+                    if (message.querySelector('event[xmlns="'+Strophe.NS.PUBSUB+'#event"]')) {
+                        _converse.bookmarks.createBookmarksFromStanza(message);
+                    }
+                }, null, 'message', 'headline', null, _converse.bare_jid);
+            });
+
             const afterReconnection = function () {
                 if (!_converse.allow_bookmarks) {
                     return;

+ 46 - 16
src/converse-chatboxes.js

@@ -7,7 +7,7 @@
 /*global define */
 
 (function (root, factory) {
-    define(["converse-core"], factory);
+    define(["converse-core", "backbone.overview"], factory);
 }(this, function (converse) {
     "use strict";
     const { Backbone, Promise, Strophe, b64_sha1, moment, utils, _ } = converse.env;
@@ -117,6 +117,20 @@
                 },
 
                 getMessageAttributes (message, delay, original_stanza) {
+                    /* Parses a passed in message stanza and returns an object
+                     * of attributes.
+                     *
+                     * Parameters:
+                     *    (XMLElement) message - The message stanza
+                     *    (XMLElement) delay - The <delay> node from the
+                     *      stanza, if there was one.
+                     *    (XMLElement) original_stanza - The original stanza,
+                     *      that contains the message stanza, if it was
+                     *      contained, otherwise it's the message stanza itself.
+                     */
+                    const { _converse } = this.__super__,
+                          { __ } = _converse;
+
                     delay = delay || message.querySelector('delay');
                     const type = message.getAttribute('type'),
                         body = this.getMessageBody(message);
@@ -144,7 +158,8 @@
                         sender = 'them';
                         fullname = this.get('fullname') || from;
                     }
-                    return {
+                    const spoiler = message.querySelector(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`);
+                    const attrs = {
                         'type': type,
                         'chat_state': chat_state,
                         'delayed': delayed,
@@ -152,18 +167,26 @@
                         'message': body || undefined,
                         'msgid': message.getAttribute('id'),
                         'sender': sender,
-                        'time': time
+                        'time': time,
+                        'is_spoiler': !_.isNull(spoiler)
                     };
+                    if (spoiler) {
+                        attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
+                    }
+                    return attrs;
                 },
 
                 createMessage (message, delay, original_stanza) {
+                    /* Create a Backbone.Message object inside this chat box
+                     * based on the identified message stanza.
+                     */
                     return this.messages.create(this.getMessageAttributes.apply(this, arguments));
                 },
 
                 newMessageWillBeHidden () {
                     /* Returns a boolean to indicate whether a newly received
-                    * message will be visible to the user or not.
-                    */
+                     * message will be visible to the user or not.
+                     */
                     return this.get('hidden') ||
                         this.get('minimized') ||
                         this.isScrolledUp() ||
@@ -172,8 +195,8 @@
 
                 incrementUnreadMsgCounter (stanza) {
                     /* Given a newly received message, update the unread counter if
-                    * necessary.
-                    */
+                     * necessary.
+                     */
                     if (_.isNull(stanza.querySelector('body'))) {
                         return; // The message has no text
                     }
@@ -256,8 +279,11 @@
 
                 onMessage (message) {
                     /* Handler method for all incoming single-user chat "message"
-                    * stanzas.
-                    */
+                     * stanzas.
+                     *
+                     * Parameters:
+                     *    (XMLElement) message - The incoming message stanza
+                     */
                     let contact_jid, delay, resource,
                         from_jid = message.getAttribute('from'),
                         to_jid = message.getAttribute('to');
@@ -272,7 +298,7 @@
                             Strophe.LogLevel.INFO
                         );
                         return true;
-                    } else if (utils.isHeadlineMessage(message)) {
+                    } else if (utils.isHeadlineMessage(_converse, message)) {
                         // XXX: Ideally we wouldn't have to check for headline
                         // messages, but Prosody sends headline messages with the
                         // wrong type ('chat'), so we need to filter them out here.
@@ -392,16 +418,20 @@
                     * If the #conversejs element doesn't exist, create it.
                     */
                     if (!this.el) {
-                        let el = document.querySelector('#conversejs');
+                        let el = _converse.root.querySelector('#conversejs');
                         if (_.isNull(el)) {
                             el = document.createElement('div');
                             el.setAttribute('id', 'conversejs');
-                            if (_.includes(['mobile', 'fullscreen'], _converse.view_mode)) {
-                                el.classList.add('fullscreen');
+                            const body = _converse.root.querySelector('body');
+                            if (body) {
+                                body.appendChild(el);
+                            } else {
+                                // Perhaps inside a web component?
+                                _converse.root.appendChild(el);
                             }
-                            // Converse.js expects a <body> tag to be present.
-                            document.querySelector('body').appendChild(el);
-
+                        }
+                        if (_.includes(['mobile', 'fullscreen'], _converse.view_mode)) {
+                            el.classList.add('fullscreen');
                         }
                         el.innerHTML = '';
                         this.setElement(el, false);

+ 257 - 94
src/converse-chatview.js

@@ -15,12 +15,15 @@
             "tpl!action",
             "tpl!chatbox",
             "tpl!chatbox_head",
+            "tpl!chatbox_message_form",
             "tpl!emojis",
             "tpl!help_message",
             "tpl!info",
             "tpl!message",
             "tpl!new_day",
             "tpl!spinner",
+            "tpl!spoiler_button",
+            "tpl!spoiler_message",
             "tpl!toolbar"
     ], factory);
 }(this, function (
@@ -31,16 +34,19 @@
             tpl_action,
             tpl_chatbox,
             tpl_chatbox_head,
+            tpl_chatbox_message_form,
             tpl_emojis,
             tpl_help_message,
             tpl_info,
             tpl_message,
             tpl_new_day,
             tpl_spinner,
+            tpl_spoiler_button,
+            tpl_spoiler_message,
             tpl_toolbar
     ) {
     "use strict";
-    const { $msg, Backbone, Strophe, _, b64_sha1, sizzle, moment } = converse.env;
+    const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env;
     const u = converse.env.utils;
     const KEY = {
         ENTER: 13,
@@ -58,7 +64,7 @@
          *
          * NB: These plugins need to have already been loaded via require.js.
          */
-        dependencies: ["converse-chatboxes"],
+        dependencies: ["converse-chatboxes", "converse-disco"],
 
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
@@ -68,15 +74,16 @@
             // New functions which don't exist yet can also be added.
             //
             registerGlobalEventHandlers: function () {
+                const { _converse } = this.__super__;
                 this.__super__.registerGlobalEventHandlers();
-                document.addEventListener(
+                _converse.root.addEventListener(
                     'click', function (ev) {
                         if (_.includes(ev.target.classList, 'toggle-toolbar-menu') ||
                             _.includes(ev.target.classList, 'insert-emoji')) {
                             return;
                         }
                         u.slideInAllElements(
-                            document.querySelectorAll('.toolbar-menu')
+                            _converse.root.querySelectorAll('.toolbar-menu')
                         )
                     }
                 );
@@ -113,9 +120,10 @@
                 'show_message_load_animation': false,
                 'time_format': 'HH:mm',
                 'visible_toolbar_buttons': {
-                    'emoji': true,
                     'call': false,
-                    'clear': true
+                    'clear': true,
+                    'emoji': true,
+                    'spoiler': true
                 },
             });
             emojione.imagePathPNG = _converse.emojione_image_path;
@@ -201,7 +209,7 @@
                 },
 
                 setScrollPosition (ev) {
-                    this.model.save('scroll_position', ev.target.scrollTop);
+                    this.model.save('scroll_position', this.content);
                 },
 
                 chooseSkinTone (ev) {
@@ -258,6 +266,7 @@
                 }
             });
 
+
             _converse.ChatBoxView = Backbone.NativeView.extend({
                 length: 200,
                 className: 'chatbox hidden',
@@ -265,13 +274,15 @@
 
                 events: {
                     'click .close-chatbox-button': 'close',
-                    'keypress .chat-textarea': 'keyPressed',
+                    'click .new-msgs-indicator': 'viewUnreadMessages',
                     'click .send-button': 'onFormSubmitted',
-                    'click .toggle-smiley': 'toggleEmojiMenu',
-                    'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
-                    'click .toggle-clear': 'clearMessages',
                     'click .toggle-call': 'toggleCall',
-                    'click .new-msgs-indicator': 'viewUnreadMessages'
+                    'click .toggle-clear': 'clearMessages',
+                    'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
+                    'click .toggle-smiley': 'toggleEmojiMenu',
+                    'click .toggle-spoiler': 'toggleSpoilerMessage',
+                    'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
+                    'keypress .chat-textarea': 'keyPressed'
                 },
 
                 initialize () {
@@ -286,8 +297,8 @@
                     this.model.on('change:chat_status', this.onChatStatusChanged, this);
                     this.model.on('showHelpMessages', this.showHelpMessages, this);
                     this.model.on('sendMessage', this.sendMessage, this);
-
-                    this.render().renderToolbar().insertHeading().fetchMessages();
+                    this.render();
+                    this.fetchMessages();
                     _converse.emit('chatBoxOpened', this);
                     _converse.emit('chatBoxInitialized', this);
                 },
@@ -296,18 +307,80 @@
                     this.el.setAttribute('id', this.model.get('box_id'));
                     this.el.innerHTML = tpl_chatbox(
                         _.extend(this.model.toJSON(), {
-                                label_personal_message: __('Personal message'),
-                                label_send: __('Send'),
-                                show_send_button: _converse.show_send_button,
-                                show_textarea: true,
-                                show_toolbar: _converse.show_toolbar,
                                 unread_msgs: __('You have unread messages')
                             }
                         ));
                     this.content = this.el.querySelector('.chat-content');
+                    this.renderMessageForm();
+                    this.insertHeading();
+                    return this;
+                },
+
+                renderToolbar (toolbar, options) {
+                    if (!_converse.show_toolbar) {
+                        return this;
+                    }
+                    toolbar = toolbar || tpl_toolbar;
+                    options = _.assign(
+                        this.model.toJSON(),
+                        this.getToolbarOptions(options || {})
+                    );
+                    this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
+                    this.addSpoilerButton(options);
+                    this.insertEmojiPicker();
                     return this;
                 },
 
+                renderMessageForm () {
+                    let placeholder;
+                    if (this.model.get('composing_spoiler')) {
+                        placeholder = __('Hidden message');
+                    } else {
+                        placeholder = __('Personal message');
+                    }
+                    const form_container = this.el.querySelector('.message-form-container');
+                    form_container.innerHTML = tpl_chatbox_message_form(
+                        _.extend(this.model.toJSON(), {
+                            'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
+                            'label_personal_message': placeholder,
+                            'label_send': __('Send'),
+                            'label_spoiler_hint': __('Optional hint'),
+                            'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value'),
+                            'show_send_button': _converse.show_send_button,
+                            'show_toolbar': _converse.show_toolbar,
+                            'unread_msgs': __('You have unread messages')
+                        }));
+                    this.renderToolbar();
+                },
+
+                addSpoilerButton (options) {
+                    /* Asynchronously adds a button for writing spoiler
+                     * messages, based on whether the contact's client supports
+                     * it.
+                     */
+                    if (!options.show_spoiler_button || this.model.get('type') === 'chatroom') {
+                        return;
+                    }
+                    const contact_jid = this.model.get('jid');
+                    const resources = this.model.get('resources');
+                    if (_.isEmpty(resources)) {
+                        return;
+                    }
+                    Promise.all(_.map(_.keys(resources), (resource) =>
+                        _converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
+                    )).then((results) => {
+                        const supported = _.every(f.map(f.get('supported'))(results));
+                        if (supported) {
+                            const html = tpl_spoiler_button(this.model.toJSON());
+                            if (_converse.visible_toolbar_buttons.emoji) {
+                                this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
+                            } else {
+                                this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
+                            }
+                        }
+                    });
+                },
+
                 insertHeading () {
                     this.heading = new _converse.ChatBoxHeading({'model': this.model});
                     this.heading.render();
@@ -318,13 +391,22 @@
                     return this;
                 },
 
-                createEmojiPicker () {
-                    if (_.isUndefined(_converse.emojipicker)) {
-                        _converse.emojipicker = new _converse.EmojiPicker();
-                        _converse.emojipicker.fetch();
+                getToolbarOptions (options) {
+                    let label_toggle_spoiler;
+                    if (this.model.get('composing_spoiler')) {
+                        label_toggle_spoiler = __('Click to write as a normal (non-spoiler) message');
+                    } else {
+                        label_toggle_spoiler = __('Click to write your message as a spoiler');
                     }
-                    this.emoji_picker_view = new _converse.EmojiPickerView({
-                        'model': _converse.emojipicker
+                    return _.extend(options || {}, {
+                        'label_clear': __('Clear all messages'),
+                        'label_insert_smiley': __('Insert a smiley'),
+                        'label_start_call': __('Start a call'),
+                        'label_toggle_spoiler': label_toggle_spoiler,
+                        'show_call_button': _converse.visible_toolbar_buttons.call,
+                        'show_clear_button': _converse.visible_toolbar_buttons.clear,
+                        'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler,
+                        'use_emoji': _converse.visible_toolbar_buttons.emoji,
                     });
                 },
 
@@ -349,7 +431,7 @@
                      * as well as src/converse-muc.js (if those plugins are
                      * enabled).
                      */
-                    const container = document.querySelector('#conversejs');
+                    const container = _converse.root.querySelector('#conversejs');
                     if (this.el.parentNode !== container) {
                         container.insertBefore(this.el, container.firstChild);
                     }
@@ -510,14 +592,18 @@
                     }
                 },
 
-                getExtraMessageTemplateAttributes () {
+                getExtraMessageTemplateAttributes (attrs) {
                     /* Provides a hook for sending more attributes to the
                      * message template.
                      *
                      * Parameters:
                      *  (Object) attrs: An object containing message attributes.
                      */
-                    return {};
+                    if (attrs.is_spoiler) {
+                        return {'label_show': __('Show hidden message')};
+                    } else {
+                        return {}
+                    }
                 },
 
                 getExtraMessageClasses (attrs) {
@@ -528,6 +614,17 @@
                     }
                 },
 
+                renderSpoilerMessage (msg, attrs) {
+                    /* Render a "spoiler" message, as defined in XEP-0382
+                     *
+                     * Parameters:
+                     *  (HTMLElement) msg: The chat message DOM element
+                     *  (Object) attrs: An object containing the message attributes.
+                     */
+                    const hint = msg.querySelector('.spoiler-hint');
+                    hint.appendChild(document.createTextNode(attrs.spoiler_hint || ''));
+                },
+
                 renderMessage (attrs) {
                     /* Renders a chat message based on the passed in attributes.
                      *
@@ -551,6 +648,8 @@
                         } else {
                             username = attrs.fullname;
                         }
+                    } else if (attrs.is_spoiler) {
+                        template = tpl_spoiler_message;
                     } else  {
                         template = tpl_message;
                         username = attrs.sender === 'me' && __('me') || fullname;
@@ -568,12 +667,16 @@
                         })
                     ));
                     if (_converse.show_message_load_animation) {
-                        window.setTimeout(_.partial(u.removeClass, 'onload', msg), 2000);
+                        window.setTimeout(
+                            _.partial(u.removeClass, 'onload', msg), 2000);
                     }
                     const msg_content = msg.querySelector('.chat-msg-content');
                     msg_content.innerHTML = u.addEmoji(
                         _converse, emojione, u.addHyperlinks(xss.filterXSS(text, {'whiteList': {}}))
                     );
+                    if (attrs.is_spoiler) {
+                        this.renderSpoilerMessage(msg, attrs)
+                    }
                     u.renderImageURLs(msg_content).then(this.scrollDown.bind(this));
                     return msg;
                 },
@@ -688,13 +791,22 @@
                 },
 
                 createMessageStanza (message) {
-                    return $msg({
-                                from: _converse.connection.jid,
-                                to: this.model.get('jid'),
-                                type: 'chat',
-                                id: message.get('msgid')
+                    const stanza = $msg({
+                            'from': _converse.connection.jid,
+                            'to': this.model.get('jid'),
+                            'type': 'chat',
+                            'id': message.get('msgid')
                         }).c('body').t(message.get('message')).up()
-                            .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
+                          .c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
+
+                    if (message.get('is_spoiler')) {
+                        if (message.get('spoiler_hint')) {
+                            stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint'));
+                        } else {
+                            stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER });
+                        }
+                    }
+                    return stanza;
                 },
 
                 sendMessage (message) {
@@ -714,31 +826,19 @@
                             .c('forwarded', {'xmlns': Strophe.NS.FORWARD})
                             .c('delay', {
                                 'xmns': Strophe.NS.DELAY,
-                                'stamp': moment.format()
+                                'stamp': moment().format()
                             }).up()
                             .cnode(messageStanza.tree())
                         );
                     }
                 },
 
-                onMessageSubmitted (text) {
-                    /* This method gets called once the user has typed a message
-                     * and then pressed enter in a chat box.
-                     *
-                     *  Parameters:
-                     *    (string) text - The chat message text.
-                     */
-                    if (!_converse.connection.authenticated) {
-                        return this.showHelpMessages(
-                            ['Sorry, the connection has been lost, '+
-                                'and your message could not be sent'],
-                            'error'
-                        );
-                    }
+                parseMessageForCommands (text) {
                     const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
                     if (match) {
                         if (match[1] === "clear") {
-                            return this.clearMessages();
+                            this.clearMessages();
+                            return true;
                         }
                         else if (match[1] === "help") {
                             const msgs = [
@@ -747,21 +847,54 @@
                                 `<strong>/help</strong>: ${__('Show this menu')}`
                                 ];
                             this.showHelpMessages(msgs);
-                            return;
+                            return true;
                         }
                     }
-                    let fullname = _converse.xmppstatus.get('fullname');
-                    fullname = _.isEmpty(fullname)? _converse.bare_jid: fullname;
+                },
 
-                    const message = this.model.messages.create({
-                        fullname,
-                        sender: 'me',
-                        time: moment().format(),
-                        message: emojione.shortnameToUnicode(text)
-                    });
+                onMessageSubmitted (text, spoiler_hint) {
+                    /* This method gets called once the user has typed a message
+                     * and then pressed enter in a chat box.
+                     *
+                     *  Parameters:
+                     *    (String) text - The chat message text.
+                     *    (String) spoiler_hint - A hint in case the message
+                     *      text is a hidden/spoiler message. See XEP-0382
+                     */
+                    if (!_converse.connection.authenticated) {
+                        return this.showHelpMessages(
+                            ['Sorry, the connection has been lost, '+
+                                'and your message could not be sent'],
+                            'error'
+                        );
+                    }
+                    if (this.parseMessageForCommands(text)) {
+                        return;
+                    }
+                    const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint)
+                    const message = this.model.messages.create(attrs);
                     this.sendMessage(message);
                 },
 
+                getOutgoingMessageAttributes (text, spoiler_hint) {
+                    /* Overridable method which returns the attributes to be
+                     * passed to Backbone.Message's constructor.
+                     */
+                    const fullname = _converse.xmppstatus.get('fullname'),
+                        is_spoiler = this.model.get('composing_spoiler'),
+                        attrs = {
+                            'fullname': _.isEmpty(fullname) ? _converse.bare_jid : fullname,
+                            'sender': 'me',
+                            'time': moment().format(),
+                            'message': emojione.shortnameToUnicode(text),
+                            'is_spoiler': is_spoiler
+                        };
+                    if (is_spoiler) {
+                        attrs.spoiler_hint = spoiler_hint;
+                    }
+                    return attrs;
+                },
+
                 sendChatState () {
                     /* Sends a message with the status of the user in this chat session
                      * as taken from the 'chat_state' attribute of the chat box.
@@ -814,10 +947,17 @@
                     ev.preventDefault();
                     const textarea = this.el.querySelector('.chat-textarea'),
                           message = textarea.value;
+
+                    let spoiler_hint;
+                    if (this.model.get('composing_spoiler')) {
+                        const hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
+                        spoiler_hint = hint_el.value;
+                        hint_el.value = '';
+                    }
                     textarea.value = '';
                     textarea.focus();
                     if (message !== '') {
-                        this.onMessageSubmitted(message);
+                        this.onMessageSubmitted(message, spoiler_hint);
                         _converse.emit('messageSend', message);
                     }
                     this.setChatState(_converse.ACTIVE);
@@ -856,6 +996,16 @@
                     textbox_el.focus()
                 },
 
+                createEmojiPicker () {
+                    if (_.isUndefined(_converse.emojipicker)) {
+                        _converse.emojipicker = new _converse.EmojiPicker();
+                        _converse.emojipicker.fetch();
+                    }
+                    this.emoji_picker_view = new _converse.EmojiPickerView({
+                        'model': _converse.emojipicker
+                    });
+                },
+
                 insertEmoji (ev) {
                     ev.stopPropagation();
                     const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
@@ -875,7 +1025,7 @@
                         }
                     }
                     const elements = _.difference(
-                        document.querySelectorAll('.toolbar-menu'),
+                        _converse.root.querySelectorAll('.toolbar-menu'),
                         [this.emoji_picker_view.el]
                     );
                     u.slideInAllElements(elements)
@@ -893,6 +1043,33 @@
                     });
                 },
 
+                toggleComposeSpoilerMessage () {
+                    this.model.set('composing_spoiler', !this.model.get('composing_spoiler'));
+                    this.renderMessageForm();
+                    this.focus();
+                },
+
+                toggleSpoilerMessage (ev) {
+                    if (ev && ev.preventDefault) {
+                        ev.preventDefault();
+                    }
+                    const toggle_el = ev.target;
+                    u.slideToggleElement(
+                        toggle_el.parentElement.querySelector('.spoiler')
+                    );
+                    if (toggle_el.getAttribute("data-toggle-state") == "closed") {
+                        toggle_el.textContent = __('Hide hidden message');
+                        toggle_el.classList.remove("icon-eye");
+                        toggle_el.classList.add("icon-eye-blocked");
+                        toggle_el.setAttribute("data-toggle-state", "open");
+                    } else {
+                        toggle_el.textContent = __('Show hidden message');
+                        toggle_el.classList.remove("icon-eye-blocked");
+                        toggle_el.classList.add("icon-eye");
+                        toggle_el.setAttribute("data-toggle-state", "closed");
+                    }
+                },
+
                 onChatStatusChanged (item) {
                     const chat_status = item.get('chat_status');
                     let fullname = item.get('fullname');
@@ -931,33 +1108,16 @@
                     return this;
                 },
 
-                getToolbarOptions (options) {
-                    return _.extend(options || {}, {
-                        'label_clear': __('Clear all messages'),
-                        'label_insert_smiley': __('Insert a smiley'),
-                        'label_start_call': __('Start a call'),
-                        'show_call_button': _converse.visible_toolbar_buttons.call,
-                        'show_clear_button': _converse.visible_toolbar_buttons.clear,
-                        'use_emoji': _converse.visible_toolbar_buttons.emoji,
-                    });
-                },
-
-                renderToolbar (toolbar, options) {
-                    if (!_converse.show_toolbar) { return; }
-                    toolbar = toolbar || tpl_toolbar;
-                    options = _.assign(
-                        this.model.toJSON(),
-                        this.getToolbarOptions(options || {})
-                    );
-                    this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
-
-                    return this;
+                renderEmojiPicker () {
+                    this.emoji_picker_view.render();
                 },
 
-                renderEmojiPicker () {
-                    var toggle = this.el.querySelector('.toggle-smiley');
-                    toggle.innerHTML = '';
-                    toggle.appendChild(this.emoji_picker_view.render().el);
+                insertEmojiPicker () {
+                    var picker_el = this.el.querySelector('.emoji-picker');
+                    if (!_.isNull(picker_el)) {
+                        picker_el.innerHTML = '';
+                        picker_el.appendChild(this.emoji_picker_view.el);
+                    }
                 },
 
                 focus () {
@@ -976,11 +1136,12 @@
 
                 afterShown (focus) {
                     if (u.isPersistableModel(this.model)) {
+                        this.model.clearUnreadMsgCounter();
                         this.model.save();
                     }
                     this.setChatState(_converse.ACTIVE);
-                    this.renderEmojiPicker();
                     this.scrollDown();
+                    this.renderEmojiPicker();
                     if (focus) {
                         this.focus();
                     }
@@ -992,11 +1153,7 @@
                         if (focus) { this.focus(); }
                         return;
                     }
-                    const that = this;
-                    u.fadeIn(this.el, function () {
-                        that.afterShown();
-                        if (focus) { that.focus(); }
-                    });
+                    u.fadeIn(this.el, _.bind(this.afterShown, this, focus));
                 },
 
                 show (focus) {
@@ -1063,7 +1220,7 @@
                     }
                 },
 
-                onScrolledDown() {
+                onScrolledDown () {
                     this.hideNewMessagesIndicator();
                     if (_converse.windowState !== 'hidden') {
                         this.model.clearUnreadMsgCounter();
@@ -1077,6 +1234,12 @@
                     }
                 }
             });
+
+
+            _converse.on('connected', () => {
+                // Advertise that we support XEP-0382 Message Spoilers
+                _converse.connection.disco.addFeature(Strophe.NS.SPOILER);
+            });
         }
     });
 

+ 18 - 10
src/converse-controlbox.js

@@ -115,11 +115,14 @@
 
             clearSession () {
                 this.__super__.clearSession.apply(this, arguments);
-                const controlbox = this.chatboxes.get('controlbox');
-                if (controlbox &&
-                        controlbox.collection &&
-                        controlbox.collection.browserStorage) {
-                    controlbox.save({'connected': false});
+                const chatboxes = _.get(this, 'chatboxes', null);
+                if (!_.isNil(chatboxes)) {
+                    const controlbox = chatboxes.get('controlbox');
+                    if (controlbox &&
+                            controlbox.collection &&
+                            controlbox.collection.browserStorage) {
+                        controlbox.save({'connected': false});
+                    }
                 }
             },
 
@@ -423,8 +426,8 @@
                     const tab = ev.target,
                         sibling_li = tab.parentNode.nextElementSibling || tab.parentNode.previousElementSibling,
                         sibling = sibling_li.firstChild,
-                        sibling_panel = document.querySelector(sibling.getAttribute('href')),
-                        tab_panel = document.querySelector(tab.getAttribute('href'));
+                        sibling_panel = _converse.root.querySelector(sibling.getAttribute('href')),
+                        tab_panel = _converse.root.querySelector(tab.getAttribute('href'));
 
                     u.hideElement(sibling_panel);
                     u.removeClass('current', sibling);
@@ -448,6 +451,8 @@
 
             _converse.LoginPanelModel = Backbone.Model.extend({
                 defaults: {
+                    // Passed-by-reference. Fine in this case because there's
+                    // only one such model.
                     'errors': [],
                 }
             });
@@ -594,6 +599,9 @@
 
                 renderTab () {
                     const controlbox = _converse.chatboxes.get('controlbox');
+                    if (_.isNil(controlbox)) {
+                        return;
+                    }
                     const chats = fp.filter(_.partial(u.isOfType, CHATBOX_TYPE), _converse.chatboxes.models);
                     this.tab_el.innerHTML = tpl_contacts_tab({
                         'label_contacts': LABEL_CONTACTS,
@@ -648,7 +656,7 @@
                     xhr.onload = function () {
                         if (xhr.status >= 200 && xhr.status < 400) {
                             const data = JSON.parse(xhr.responseText),
-                                  ul = document.querySelector('.search-xmpp ul');
+                                  ul = _converse.root.querySelector('.search-xmpp ul');
                             u.removeElement(ul.querySelector('li.found-user'));
                             u.removeElement(ul.querySelector('li.chat-info'));
                             if (!data.length) {
@@ -682,7 +690,7 @@
                     ev.preventDefault();
                     const input = ev.target.querySelector('input');
                     const jid = input.value;
-                    if (!jid || _.filter(jid.split('@')).length < 2) {
+                    if (!jid || _.compact(jid.split('@')).length < 2) {
                         this.el.querySelector('.search-xmpp div').innerHTML =
                             this.generateAddContactHTML({
                                 error_message: __('Please enter a valid XMPP address'),
@@ -761,7 +769,7 @@
 
                 onClick (e) {
                     e.preventDefault();
-                    if (u.isVisible(document.querySelector("#controlbox"))) {
+                    if (u.isVisible(_converse.root.querySelector("#controlbox"))) {
                         const controlbox = _converse.chatboxes.get('controlbox');
                         if (_converse.connection.connected) {
                             controlbox.save({closed: true});

+ 54 - 35
src/converse-core.js

@@ -4,11 +4,12 @@
 // Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 //
-/*global Backbone, define, window, document, JSON */
+/*global Backbone, define, window, JSON */
 (function (root, factory) {
     define(["sizzle",
             "es6-promise",
             "lodash.noconflict",
+            "lodash.fp",
             "polyfill",
             "i18n",
             "utils",
@@ -19,7 +20,7 @@
             "backbone.nativeview",
             "backbone.browserStorage"
     ], factory);
-}(this, function (sizzle, Promise, _, polyfill, i18n, utils, moment, Strophe, pluggable, Backbone) {
+}(this, function (sizzle, Promise, _, f, polyfill, i18n, utils, moment, Strophe, pluggable, Backbone) {
 
     /* Cannot use this due to Safari bug.
      * See https://github.com/jcbrand/converse.js/issues/196
@@ -44,6 +45,7 @@
     Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
     Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
     Strophe.addNamespace('SID', 'urn:xmpp:sid:0');
+    Strophe.addNamespace('SPOILER', 'urn:xmpp:spoiler:0');
     Strophe.addNamespace('XFORM', 'jabber:x:data');
 
     // Use Mustache style syntax for variable interpolation
@@ -77,6 +79,7 @@
         'converse-mam',
         'converse-minimize',
         'converse-muc',
+        'converse-muc-embedded',
         'converse-notification',
         'converse-otr',
         'converse-ping',
@@ -85,6 +88,7 @@
         'converse-roomslist',
         'converse-rosterview',
         'converse-singleton',
+        'converse-spoilers',
         'converse-vcard'
     ];
 
@@ -163,21 +167,13 @@
                 'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
             }, console);
         if (level === Strophe.LogLevel.ERROR) {
-            if (_converse.debug) {
-                logger.trace(`${prefix} ${moment().format()} ERROR: ${message}`, style);
-            } else {
-                logger.error(`${prefix} ERROR: ${message}`, style);
-            }
+            logger.error(`${prefix} ERROR: ${message}`, style);
         } else if (level === Strophe.LogLevel.WARN) {
             if (_converse.debug) {
                 logger.warn(`${prefix} ${moment().format()} WARNING: ${message}`, style);
             }
         } else if (level === Strophe.LogLevel.FATAL) {
-            if (_converse.debug) {
-                logger.trace(`${prefix} ${moment().format()} FATAL: ${message}`, style);
-            } else {
-                logger.error(`${prefix} FATAL: ${message}`, style);
-            }
+            logger.error(`${prefix} FATAL: ${message}`, style);
         } else if (_converse.debug) {
             if (level === Strophe.LogLevel.DEBUG) {
                 logger.debug(`${prefix} ${moment().format()} DEBUG: ${message}`, style);
@@ -293,7 +289,7 @@
             authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
             auto_away: 0, // Seconds after which user status is set to 'away'
             auto_login: false, // Currently only used in connection with anonymous login
-            auto_reconnect: false,
+            auto_reconnect: true,
             auto_subscribe: false,
             auto_xa: 0, // Seconds after which user status is set to 'xa'
             blacklisted_plugins: [],
@@ -310,7 +306,7 @@
             include_offline_state: false,
             jid: undefined,
             keepalive: true,
-            locales_url: '/locale/{{{locale}}}/LC_MESSAGES/converse.json',
+            locales_url: 'locale/{{{locale}}}/LC_MESSAGES/converse.json',
             locales: [
                 'af', 'ca', 'de', 'es', 'en', 'fr', 'he',
                 'hu', 'id', 'it', 'ja', 'nb', 'nl',
@@ -323,6 +319,7 @@
             priority: 0,
             registration_domain: '',
             rid: undefined,
+            root: window.document,
             roster_groups: true,
             show_only_online_users: false,
             show_send_button: false,
@@ -373,7 +370,7 @@
         // Module-level functions
         // ----------------------
 
-        this.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749825).toString()}`;
+        this.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
 
         this.sendCSI = function (stat) {
             /* Send out a Chat Status Notification (XEP-0352)
@@ -596,19 +593,25 @@
         this.incrementMsgCounter = function () {
             this.msg_counter += 1;
             const unreadMsgCount = this.msg_counter;
-            if (document.title.search(/^Messages \(\d+\) /) === -1) {
-                document.title = `Messages (${unreadMsgCount}) ${document.title}`;
+            let title = document.title;
+            if (_.isNil(title)) {
+                return;
+            }
+            if (title.search(/^Messages \(\d+\) /) === -1) {
+                title = `Messages (${unreadMsgCount}) ${title}`;
             } else {
-                document.title = document.title.replace(
-                    /^Messages \(\d+\) /, `Messages (${unreadMsgCount}) `
-                );
+                title = title.replace(/^Messages \(\d+\) /, `Messages (${unreadMsgCount})`);
             }
         };
 
         this.clearMsgCounter = function () {
             this.msg_counter = 0;
-            if (document.title.search(/^Messages \(\d+\) /) !== -1) {
-                document.title = document.title.replace(/^Messages \(\d+\) /, "");
+            let title = document.title;
+            if (_.isNil(title)) {
+                return;
+            }
+            if (title.search(/^Messages \(\d+\) /) !== -1) {
+                title = title.replace(/^Messages \(\d+\) /, "");
             }
         };
 
@@ -863,10 +866,8 @@
         this.RosterContact = Backbone.Model.extend({
 
             defaults: {
-                'bookmarked': false,
                 'chat_state': undefined,
                 'chat_status': 'offline',
-                'groups': [],
                 'image': _converse.DEFAULT_IMAGE,
                 'image_type': _converse.DEFAULT_IMAGE_TYPE,
                 'num_unread': 0,
@@ -879,11 +880,12 @@
                 const resource = Strophe.getResourceFromJid(jid);
                 attributes.jid = bare_jid;
                 this.set(_.assignIn({
+                    'fullname': bare_jid,
+                    'groups': [],
                     'id': bare_jid,
                     'jid': bare_jid,
-                    'fullname': bare_jid,
-                    'user_id': Strophe.getNodeFromJid(jid),
-                    'resources': resource ? {resource :0} : {},
+                    'resources': {},
+                    'user_id': Strophe.getNodeFromJid(jid)
                 }, attributes));
 
                 this.on('destroy', () => { this.removeFromRoster(); });
@@ -979,6 +981,7 @@
 
                 const resources = _.isObject(this.get('resources')) ? this.get('resources') : {};
                 resources[resource] = {
+                    'name': resource,
                     'priority': priority,
                     'status': chat_status,
                     'timestamp': timestamp
@@ -1479,6 +1482,7 @@
         this.connfeedback = new this.ConnectionFeedback();
 
         this.XMPPStatus = Backbone.Model.extend({
+
             initialize () {
                 this.set({
                     'status' : this.getStatus()
@@ -1799,6 +1803,21 @@
             const whitelist = _converse.core_plugins.concat(
                 _converse.whitelisted_plugins);
 
+            if (_converse.view_mode === 'embedded') {
+                _.forEach([ // eslint-disable-line lodash/prefer-map
+                    "converse-bookmarks",
+                    "converse-controlbox",
+                    "converse-dragresize",
+                    "converse-headline",
+                    "converse-minimize",
+                    "converse-otr",
+                    "converse-register",
+                    "converse-vcard",
+                ], (name) => {
+                    _converse.blacklisted_plugins.push(name)
+                });
+            }
+
             _converse.pluggable.initializePlugins({
                 'updateSettings' () {
                     _converse.log(
@@ -1843,13 +1862,10 @@
             i18n.fetchTranslations(
                 _converse.locale,
                 _converse.locales,
-                _.template(_converse.locales_url)({'locale': _converse.locale})
-            ).then(() => {
-                finishInitialization();
-            }).catch((reason) => {
-                finishInitialization();
-                _converse.log(reason, Strophe.LogLevel.ERROR);
-            });
+                _.template(_converse.locales_url)({'locale': _converse.locale}))
+            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
+            .then(finishInitialization)
+            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
         }
         return init_promise;
     };
@@ -2000,7 +2016,7 @@
     };
 
     // The public API
-    return {
+    window.converse = {
         'initialize' (settings, callback) {
             return _converse.initialize(settings, callback);
         },
@@ -2025,10 +2041,13 @@
             'Promise': Promise,
             'Strophe': Strophe,
             '_': _,
+            'f': f,
             'b64_sha1':  b64_sha1,
             'moment': moment,
             'sizzle': sizzle,
             'utils': utils
         }
     };
+    window.dispatchEvent(new Event('converse-loaded'));
+    return window.converse;
 }));

+ 48 - 4
src/converse-disco.js

@@ -7,7 +7,7 @@
 
 /* This is a Converse.js plugin which add support for XEP-0030: Service Discovery */
 
-/*global Backbone, define, window, document */
+/*global Backbone, define, window */
 (function (root, factory) {
     define(["converse-core", "sizzle", "strophe.disco"], factory);
 }(this, function (converse, sizzle) {
@@ -65,6 +65,29 @@
 
                 },
 
+                getIdentity (category, type) {
+                    /* Returns a Promise which resolves with a map indicating
+                     * whether a given identity is provided.
+                     *
+                     * Parameters:
+                     *    (String) category - The identity category
+                     *    (String) type - The identity type
+                     */
+                    const entity = this;
+                    return new Promise((resolve, reject) => {
+                        function fulfillPromise () {
+                            const model = entity.identities.findWhere({
+                                'category': category,
+                                'type': type
+                            });
+                            resolve(model);
+                        }
+                        entity.waitUntilFeaturesDiscovered
+                            .then(fulfillPromise)
+                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    });
+                },
+
                 hasFeature (feature) {
                     /* Returns a Promise which resolves with a map indicating
                      * whether a given feature is supported.
@@ -130,8 +153,8 @@
                     _.forEach(stanza.querySelectorAll('identity'), (identity) => {
                         this.identities.create({
                             'category': identity.getAttribute('category'),
-                            'type': stanza.getAttribute('type'),
-                            'name': stanza.getAttribute('name')
+                            'type': identity.getAttribute('type'),
+                            'name': identity.getAttribute('name')
                         });
                     });
                     if (stanza.querySelector('feature[var="'+Strophe.NS.DISCO_ITEMS+'"]')) {
@@ -245,7 +268,28 @@
                         return _converse.api.waitUntil('discoInitialized').then(() => {
                             const entity = _converse.api.disco.entities.get(entity_jid, true);
                             return entity.hasFeature(feature);
-                        });
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+                    },
+
+                    'getIdentity' (category, type, entity_jid) {
+                        /* Returns a Promise which resolves with a map indicating
+                         * whether an identity with a given type is provided by
+                         * the entity.
+                         *
+                         * Parameters:
+                         *    (String) category - The identity category.
+                         *          In the XML stanza, this is the `category`
+                         *          attribute of the `<identity>` element.
+                         *          For example: 'pubsub'
+                         *    (String) type - The identity type.
+                         *          In the XML stanza, this is the `type`
+                         *          attribute of the `<identity>` element.
+                         *          For example: 'pep'
+                         */
+                        return _converse.api.waitUntil('discoInitialized').then(() => {
+                            const entity = _converse.api.disco.entities.get(entity_jid, true);
+                            return entity.getIdentity(category, type);
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     }
                 }
             });

+ 9 - 4
src/converse-dragresize.js

@@ -4,7 +4,7 @@
 // Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 //
-/*global define, window */
+/*global define, window, document */
 
 (function (root, factory) {
     define(["converse-core",
@@ -175,7 +175,10 @@
                     } else {
                         height = "";
                     }
-                    this.el.querySelector('.box-flyout').style.height = height;
+                    const flyout_el = this.el.querySelector('.box-flyout');
+                    if (!_.isNull(flyout_el)) {
+                        flyout_el.style.height = height;
+                    }
                 },
 
                 setChatBoxWidth (width) {
@@ -186,10 +189,12 @@
                         width = "";
                     }
                     this.el.style.width = width;
-                    this.el.querySelector('.box-flyout').style.width = width;
+                    const flyout_el = this.el.querySelector('.box-flyout');
+                    if (!_.isNull(flyout_el)) {
+                        flyout_el.style.width = width;
+                    }
                 },
 
-
                 adjustToViewport () {
                     /* Event handler called when viewport gets resized. We remove
                      * custom width/height from chat boxes.

+ 3 - 2
src/converse-fullscreen.js

@@ -21,7 +21,7 @@
     converse.plugins.add('converse-fullscreen', {
 
         enabled (_converse) {
-            return _.includes(['mobile', 'fullscreen'], _converse.view_mode);
+            return _.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode);
         },
 
         overrides: {
@@ -37,7 +37,8 @@
                 },
 
                 insertBrandHeading () {
-                    const el = document.getElementById('converse-login-panel');
+                    const { _converse } = this.__super__;
+                    const el = _converse.root.getElementById('converse-login-panel');
                     el.parentNode.insertAdjacentHTML(
                         'afterbegin',
                         this.createBrandHeadingHTML()

+ 4 - 5
src/converse-headline.js

@@ -113,7 +113,6 @@
                                 info_close: '',
                                 label_personal_message: '',
                                 show_send_button: false,
-                                show_textarea: false,
                                 show_toolbar: false,
                                 unread_msgs: ''
                             }
@@ -122,14 +121,15 @@
                     return this;
                 },
 
-                // Override to avoid the method in converse-chatview.js
+                // Override to avoid the methods in converse-chatview.js
+                'renderMessageForm': _.noop,
                 'afterShown': _.noop
             });
 
             function onHeadlineMessage (message) {
                 /* Handler method for all incoming messages of type "headline". */
                 const from_jid = message.getAttribute('from');
-                if (utils.isHeadlineMessage(message)) {
+                if (utils.isHeadlineMessage(_converse, message)) {
                     if (_.includes(from_jid, '@') && !_converse.allow_non_roster_messaging) {
                         return;
                     }
@@ -146,8 +146,7 @@
             }
 
             function registerHeadlineHandler () {
-                _converse.connection.addHandler(
-                        onHeadlineMessage, null, 'message');
+                _converse.connection.addHandler(onHeadlineMessage, null, 'message');
             }
             _converse.on('connected', registerHeadlineHandler);
             _converse.on('reconnected', registerHeadlineHandler);

+ 4 - 6
src/converse-mam.js

@@ -83,11 +83,9 @@
         }
 
         const messages = [];
-        const message_handler = _converse.connection.addHandler(function (message) {
-            if (options.groupchat) {
-                if (message.getAttribute('from') !== options['with']) { // eslint-disable-line dot-notation
-                    return true;
-                }
+        const message_handler = _converse.connection.addHandler((message) => {
+            if (options.groupchat && message.getAttribute('from') !== options['with']) { // eslint-disable-line dot-notation
+                return true;
             }
             const result = message.querySelector('result');
             if (!_.isNull(result) && result.getAttribute('queryid') === queryid) {
@@ -255,7 +253,7 @@
 
                 onScroll (ev) {
                     const { _converse } = this.__super__;
-                    if (ev.target.scrollTop === 0 && this.model.messages.length) {
+                    if (this.content.scrollTop === 0 && this.model.messages.length) {
                         const oldest_message = this.model.messages.at(0);
                         const archive_id = oldest_message.get('archive_id');
                         if (archive_id) {

+ 1 - 1
src/converse-minimize.js

@@ -4,7 +4,7 @@
 // Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 //
-/*global define, window */
+/*global define, window, document */
 
 (function (root, factory) {
     define(["converse-core",

+ 16 - 27
src/converse-muc-embedded.js

@@ -11,6 +11,11 @@
     const { Backbone, _ } = converse.env;
 
     converse.plugins.add('converse-muc-embedded', {
+
+        enabled (_converse) {
+            return _converse.view_mode === 'embedded';
+        },
+
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
             // plugin architecture they will replace existing methods on the
@@ -18,33 +23,10 @@
             //
             // New functions which don't exist yet can also be added.
 
-            ChatBoxes: {
-                onConnected () {
-                    // Override to avoid storing or fetching chat boxes from session
-                    // storage.
-                    const { _converse } = this.__super__;
-                    this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
-                        converse.env.b64_sha1(`converse.chatboxes-${_converse.bare_jid}`));
-                    this.registerMessageHandler();
-                    /* This is disabled:
-                     *
-                     * this.fetch({
-                     *      add: true,
-                     *      success: this.onChatBoxesFetched.bind(this)
-                     *  });
-                     */
-                     this.onChatBoxesFetched(new Backbone.Collection());
-                }
-            },
-
-            ChatRoomView: {
-                insertIntoDOM () {
-                    if (!document.body.contains(this.el)) {
-                        const container = document.querySelector('#converse-embedded-chat');
-                        container.innerHTML = '';
-                        container.appendChild(this.el);
-                    }
-                    return this;
+            ChatBoxViews: {
+                initialize () {
+                    this.__super__.initialize.apply(this, arguments);
+                    this.el.classList.add('converse-embedded');
                 }
             }
         },
@@ -53,6 +35,13 @@
             /* The initialize function gets called as soon as the plugin is
              * loaded by converse.js's plugin machinery.
              */
+            this._converse.api.settings.update({
+                'allow_logout': false, // No point in logging out when we have auto_login as true.
+                'allow_muc_invitations': false, // Doesn't make sense to allow because only
+                                                // roster contacts can be invited
+                'hide_muc_server': true, // Federation is disabled, so no use in
+                                         // showing the MUC server.
+            });
             const { _converse } = this;
             if (!_.isArray(_converse.auto_join_rooms)) {
                 throw new Error("converse-muc-embedded: auto_join_rooms must be an Array");

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.