Bläddra i källkod

Merge branch 'master' of github.com:jcbrand/converse.js

JC Brand 7 år sedan
förälder
incheckning
6786d1b77d
70 ändrade filer med 7096 tillägg och 8690 borttagningar
  1. 1 1
      .eslintrc.json
  2. 10 0
      CHANGES.md
  3. 1 1
      COPYRIGHT
  4. 10 11
      Makefile
  5. 19 3
      css/converse.css
  6. 28 9
      css/inverse.css
  7. 461 1490
      dist/converse-mobile.js
  8. 461 1490
      dist/converse.js
  9. 461 1490
      dist/inverse.js
  10. 0 0
      dist/locales.js
  11. 2 2
      docs/source/conf.py
  12. 7 1
      docs/source/configuration.rst
  13. 2 2
      index.html
  14. 207 193
      locale/af/LC_MESSAGES/converse.po
  15. 207 193
      locale/ca/LC_MESSAGES/converse.po
  16. 207 193
      locale/converse.pot
  17. 83 39
      locale/de/LC_MESSAGES/converse.json
  18. 225 215
      locale/de/LC_MESSAGES/converse.po
  19. 206 192
      locale/es/LC_MESSAGES/converse.po
  20. 101 13
      locale/fr/LC_MESSAGES/converse.json
  21. 232 231
      locale/fr/LC_MESSAGES/converse.po
  22. 207 193
      locale/he/LC_MESSAGES/converse.po
  23. 207 193
      locale/hu/LC_MESSAGES/converse.po
  24. 206 192
      locale/id/LC_MESSAGES/converse.po
  25. 207 193
      locale/it/LC_MESSAGES/converse.po
  26. 206 192
      locale/ja/LC_MESSAGES/converse.po
  27. 207 193
      locale/nb/LC_MESSAGES/converse.po
  28. 206 192
      locale/nl/LC_MESSAGES/converse.po
  29. 207 193
      locale/pl/LC_MESSAGES/converse.po
  30. 206 192
      locale/pt_BR/LC_MESSAGES/converse.po
  31. 207 193
      locale/ru/LC_MESSAGES/converse.po
  32. 207 193
      locale/uk/LC_MESSAGES/converse.po
  33. 206 192
      locale/zh/LC_MESSAGES/converse.po
  34. 552 124
      package-lock.json
  35. 1 1
      package.json
  36. 5 1
      sass/_controlbox.scss
  37. 11 1
      sass/_core.scss
  38. 6 2
      sass/inverse/_controlbox.scss
  39. 4 5
      sass/inverse/_core.scss
  40. 2 1
      spec/bookmarks.js
  41. 21 11
      spec/chatbox.js
  42. 7 12
      spec/chatroom.js
  43. 11 0
      spec/controlbox.js
  44. 90 25
      spec/register.js
  45. 2 0
      src/build-inverse.js
  46. 4 1
      src/build.js
  47. 4 1
      src/config.js
  48. 26 10
      src/converse-bookmarks.js
  49. 384 0
      src/converse-chatboxes.js
  50. 7 4
      src/converse-chatview.js
  51. 44 32
      src/converse-controlbox.js
  52. 2 6
      src/converse-core.js
  53. 5 5
      src/converse-inverse.js
  54. 90 58
      src/converse-minimize.js
  55. 23 31
      src/converse-muc.js
  56. 4 3
      src/converse-otr.js
  57. 3 1
      src/converse-ping.js
  58. 4 2
      src/converse-register.js
  59. 5 2
      src/converse-rosterview.js
  60. 26 22
      src/converse-vcard.js
  61. 160 0
      src/form-utils.js
  62. 4 2
      src/lodash.fp.js
  63. 14 0
      src/polyfill.js
  64. 1 1
      src/start.frag
  65. 2 1
      src/templates/field.html
  66. 2 2
      src/templates/form_checkbox.html
  67. 3 5
      src/templates/form_input.html
  68. 4 2
      src/templates/form_select.html
  69. 146 229
      src/utils.js
  70. 7 7
      tests/mock.js

+ 1 - 1
.eslintrc.json

@@ -190,7 +190,7 @@
         "no-use-before-define": "off",
         "no-useless-call": "error",
         "no-useless-computed-key": "error",
-        "no-useless-concat": "error",
+        "no-useless-concat": "off",
         "no-useless-constructor": "error",
         "no-useless-escape": "off",
         "no-useless-rename": "error",

+ 10 - 0
CHANGES.md

@@ -1,5 +1,15 @@
 # Changelog
 
+## 3.2.1 (2017-08-29)
+
+### Bugfixes
+- Various IE11 fixes.
+- #907 Unnecessary login validation error when `default_domain` or `locked_domain` are set.
+- #908 Login form for inVerse is only 200px when `allow_registration` is set to `false`.
+- #909 Translations written as template literals [aren't parsed properly by xgettext](https://savannah.gnu.org/bugs/?50920).
+- #911 Use `getDefaultNickName` consistently to allow better overrides via plugins.
+- #912 `maximize` method in `converse-minimize` fails if the `controlbox` is not there.
+
 ## 3.2.0 (2017-08-09)
 
 ### New Plugins

+ 1 - 1
COPYRIGHT

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

+ 10 - 11
Makefile

@@ -61,11 +61,11 @@ serve_bg: dev
 ########################################################################
 ## Translation machinery
 
-GETTEXT = xgettext --keyword=__ --keyword=___ --from-code=UTF-8 --output=locale/converse.pot src/*.js --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=3.2.0-rc -c
+GETTEXT = xgettext --language="JavaScript" --keyword=__ --keyword=___ --from-code=UTF-8 --output=locale/converse.pot src/*.js --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=3.2.1 -c
 
 .PHONY: pot
 pot:
-	$(GETTEXT) --language="javascript" 2>&1 > /dev/null; test $$? -eq 0 && exit 0 || $(GETTEXT) --language="python" && exit $$?;
+	$(GETTEXT) 2>&1 > /dev/null; exit $$?;
 
 .PHONY: po
 po:
@@ -83,7 +83,6 @@ release:
 	$(SED) -ri s/Version:\ [0-9]\+\.[0-9]\+\.[0-9]\+/Version:\ $(VERSION)/ COPYRIGHT
 	$(SED) -ri s/Version:\ [0-9]\+\.[0-9]\+\.[0-9]\+/Version:\ $(VERSION)/ src/start.frag
 	$(SED) -ri s/Project-Id-Version:\ Converse\.js\ [0-9]\+\.[0-9]\+\.[0-9]\+/Project-Id-Version:\ Converse.js\ $(VERSION)/ locale/converse.pot
-	$(SED) -ri s/\"version\":\ \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/\"version\":\ \"$(VERSION)\"/ bower.json
 	$(SED) -ri s/\"version\":\ \"[0-9]\+\.[0-9]\+\.[0-9]\+\"/\"version\":\ \"$(VERSION)\"/ package.json
 	$(SED) -ri s/--package-version=[0-9]\+\.[0-9]\+\.[0-9]\+/--package-version=$(VERSION)/ Makefile
 	$(SED) -ri s/v[0-9]\+\.[0-9]\+\.[0-9]\+\.zip/v$(VERSION)\.zip/ index.html
@@ -176,31 +175,31 @@ BUILDS = dist/converse.js \
 dist/converse.js: transpile src locale node_modules *.js
 	$(RJS) -o src/build.js include=converse out=dist/converse.js optimize=none 
 dist/converse.min.js: src locale node_modules *.js
-	$(RJS) -o src/build.js include=converse out=dist/converse.js
+	$(RJS) -o src/build.js include=converse out=dist/converse.min.js
 dist/converse-esnext.js: src locale node_modules *.js transpile
 	$(RJS) -o src/build-esnext.js include=converse out=dist/converse-esnext.js optimize=none 
 dist/converse-esnext.min.js: src locale node_modules *.js transpile
-	$(RJS) -o src/build-esnext.js include=converse out=dist/converse-esnext.js
+	$(RJS) -o src/build-esnext.js include=converse out=dist/converse-esnext.min.js
 dist/inverse.js: transpile src locale node_modules *.js
 	$(RJS) -o src/build-inverse.js include=inverse out=dist/inverse.js optimize=none 
 dist/inverse.min.js: src locale node_modules *.js
-	$(RJS) -o src/build-inverse.js include=inverse out=dist/inverse.js
+	$(RJS) -o src/build-inverse.js include=inverse out=dist/inverse.min.js
 dist/converse-no-jquery.js: transpile src locale node_modules *.js
 	$(RJS) -o src/build.js include=converse wrap.endFile=end-no-jquery.frag exclude=jquery exclude=jquery.noconflict out=dist/converse-no-jquery.js optimize=none 
 dist/converse-no-jquery.min.js: src locale node_modules *.js transpile
-	$(RJS) -o src/build.js include=converse wrap.endFile=end-no-jquery.frag exclude=jquery exclude=jquery.noconflict out=dist/converse-no-jquery.js
+	$(RJS) -o src/build.js include=converse wrap.endFile=end-no-jquery.frag exclude=jquery exclude=jquery.noconflict out=dist/converse-no-jquery.min.js
 dist/converse-no-dependencies.js: transpile src locale node_modules *.js
-	$(RJS) -o src/build-no-dependencies.js optimize=none out=dist/converse-no-dependencies.js
+	$(RJS) -o src/build-no-dependencies.js optimize=none out=dist/converse-no-dependencies.min.js
 dist/converse-no-dependencies.min.js: src locale node_modules *.js
-	$(RJS) -o src/build-no-dependencies.js out=dist/converse-no-dependencies.js
+	$(RJS) -o src/build-no-dependencies.js out=dist/converse-no-dependencies.min.js
 dist/converse-mobile.js: transpile src locale node_modules *.js
 	$(RJS) -o src/build.js paths.converse=src/converse-mobile include=converse out=dist/converse-mobile.js optimize=none 
 dist/converse-mobile.min.js: src locale node_modules *.js
-	$(RJS) -o src/build.js paths.converse=src/converse-mobile include=converse out=dist/converse-mobile.js
+	$(RJS) -o src/build.js paths.converse=src/converse-mobile include=converse out=dist/converse-mobile.min.js
 dist/converse-muc-embedded.js: transpile src locale node_modules *.js
 	$(RJS) -o src/build.js paths.converse=src/converse-embedded include=converse out=dist/converse-muc-embedded.js optimize=none 
 dist/converse-muc-embedded.min.js: src locale node_modules *.js
-	$(RJS) -o src/build.js paths.converse=src/converse-embedded include=converse out=dist/converse-muc-embedded.js
+	$(RJS) -o src/build.js paths.converse=src/converse-embedded include=converse out=dist/converse-muc-embedded.min.js
 
 .PHONY: jsmin
 jsmin: $(BUILDS)

+ 19 - 3
css/converse.css

@@ -1360,10 +1360,23 @@
     padding: 1em; }
     #converse-embedded-chat form.pure-form.converse-form legend,
     #conversejs form.pure-form.converse-form legend {
-      color: #777; }
+      color: #777;
+      font-size: 125%; }
+    #converse-embedded-chat form.pure-form.converse-form input[type=checkbox],
+    #conversejs form.pure-form.converse-form input[type=checkbox] {
+      display: block; }
+    #converse-embedded-chat form.pure-form.converse-form select,
+    #converse-embedded-chat form.pure-form.converse-form input[type=password],
+    #converse-embedded-chat form.pure-form.converse-form input[type=number],
+    #converse-embedded-chat form.pure-form.converse-form input[type=text],
+    #conversejs form.pure-form.converse-form select,
+    #conversejs form.pure-form.converse-form input[type=password],
+    #conversejs form.pure-form.converse-form input[type=number],
+    #conversejs form.pure-form.converse-form input[type=text] {
+      min-width: 50%; }
     #converse-embedded-chat form.pure-form.converse-form label,
     #conversejs form.pure-form.converse-form label {
-      margin-top: 1em;
+      margin: 1em 0;
       font-size: 16px; }
     #converse-embedded-chat form.pure-form.converse-form input[type=text],
     #converse-embedded-chat form.pure-form.converse-form input[type=password],
@@ -1944,10 +1957,13 @@
       color: red;
       display: none; }
     #conversejs #controlbox #converse-register .provider-title {
-      font-size: 22px; }
+      font-size: 20px;
+      margin: 0; }
     #conversejs #controlbox #converse-register .provider-score {
       width: 178px;
       margin-bottom: 8px; }
+    #conversejs #controlbox #converse-register .title, #conversejs #controlbox #converse-register .instructions, #conversejs #controlbox #converse-register label {
+      margin: 0.5em 0 0 0; }
     #conversejs #controlbox #converse-register .form-help .url {
       font-weight: bold;
       color: #578EA9; }

+ 28 - 9
css/inverse.css

@@ -1360,10 +1360,23 @@
     padding: 1em; }
     #converse-embedded-chat form.pure-form.converse-form legend,
     #conversejs form.pure-form.converse-form legend {
-      color: #777; }
+      color: #777;
+      font-size: 125%; }
+    #converse-embedded-chat form.pure-form.converse-form input[type=checkbox],
+    #conversejs form.pure-form.converse-form input[type=checkbox] {
+      display: block; }
+    #converse-embedded-chat form.pure-form.converse-form select,
+    #converse-embedded-chat form.pure-form.converse-form input[type=password],
+    #converse-embedded-chat form.pure-form.converse-form input[type=number],
+    #converse-embedded-chat form.pure-form.converse-form input[type=text],
+    #conversejs form.pure-form.converse-form select,
+    #conversejs form.pure-form.converse-form input[type=password],
+    #conversejs form.pure-form.converse-form input[type=number],
+    #conversejs form.pure-form.converse-form input[type=text] {
+      min-width: 50%; }
     #converse-embedded-chat form.pure-form.converse-form label,
     #conversejs form.pure-form.converse-form label {
-      margin-top: 1em;
+      margin: 1em 0;
       font-size: 18px; }
     #converse-embedded-chat form.pure-form.converse-form input[type=text],
     #converse-embedded-chat form.pure-form.converse-form input[type=password],
@@ -1445,10 +1458,9 @@ body {
   height: 100vh; }
   #conversejs form.pure-form.converse-form {
     margin: 1em; }
-    #conversejs form.pure-form.converse-form legend {
-      color: #777; }
-    #conversejs form.pure-form.converse-form label {
-      margin-top: 1em; }
+    #conversejs form.pure-form.converse-form input[type=checkbox] {
+      margin-left: 1em;
+      display: inline; }
     #conversejs form.pure-form.converse-form input[type=text],
     #conversejs form.pure-form.converse-form input[type=password],
     #conversejs form.pure-form.converse-form input[type=number],
@@ -2010,10 +2022,13 @@ body {
       color: red;
       display: none; }
     #conversejs #controlbox #converse-register .provider-title {
-      font-size: 22px; }
+      font-size: 26px;
+      margin: 0; }
     #conversejs #controlbox #converse-register .provider-score {
       width: 178px;
       margin-bottom: 8px; }
+    #conversejs #controlbox #converse-register .title, #conversejs #controlbox #converse-register .instructions, #conversejs #controlbox #converse-register label {
+      margin: 0.5em 0 0 0; }
     #conversejs #controlbox #converse-register .form-help .url {
       font-weight: bold;
       color: #578EA9; }
@@ -2351,9 +2366,13 @@ body {
     margin-top: 15%; }
     #conversejs #controlbox #converse-register #available-chatrooms, #conversejs #controlbox #converse-login #available-chatrooms {
       padding: 0 1em 2em 1em; }
-    #conversejs #controlbox #converse-register input[type=submit], #conversejs #controlbox #converse-login input[type=submit] {
+    #conversejs #controlbox #converse-register .title, #conversejs #controlbox #converse-register .instructions, #conversejs #controlbox #converse-register label, #conversejs #controlbox #converse-login .title, #conversejs #controlbox #converse-login .instructions, #conversejs #controlbox #converse-login label {
+      margin: 1em 0; }
+    #conversejs #controlbox #converse-register input[type=submit],
+    #conversejs #controlbox #converse-register input[type=button], #conversejs #controlbox #converse-login input[type=submit],
+    #conversejs #controlbox #converse-login input[type=button] {
       width: auto;
-      margin-top: 1.5em; }
+      margin: 1.5em 1em 0 0; }
   #conversejs #controlbox #controlbox-tabs {
     /* single tab */ }
     #conversejs #controlbox #controlbox-tabs li {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 461 - 1490
dist/converse-mobile.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 461 - 1490
dist/converse.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 461 - 1490
dist/inverse.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
dist/locales.js


+ 2 - 2
docs/source/conf.py

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

+ 7 - 1
docs/source/configuration.rst

@@ -485,7 +485,7 @@ default_domain
 Specify a domain to act as the default for user JIDs. This allows users to log
 in with only the username part of their JID, instead of the full JID.
 
-For example, if ``default_domain`` is ``example.org``, then the user:
+For example, if ``default_domain`` is ``example.org``, then the user
 ``johnny@example.org`` can log in with only ``johnny``.
 
 JIDs with other domains are still allowed but need to be provided in full.
@@ -701,6 +701,12 @@ locked_domain
 
 Similar to `default_domain`_ but no other domains are allowed.
 
+For example, if ``locked_domain`` is set to ``example.org``, then the user
+``johnny@example.org`` can log in with only ``johnny``.
+
+Additionally, only users registered on the ``example.org`` host can log in, no
+other users are allowed to log in.
+
 message_archiving
 -----------------
 

+ 2 - 2
index.html

@@ -12,12 +12,12 @@
     <link type="text/css" rel="stylesheet" media="screen" href="css/bootstrap.min.css" />
     <link type="text/css" rel="stylesheet" media="screen" href="css/font-awesome.min.css" />
     <link type="text/css" rel="stylesheet" media="screen" href="css/theme.min.css" />
-    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/3.1.0/css/converse.min.css" />
+    <link type="text/css" rel="stylesheet" media="screen" href="css/converse.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>
 	<script src="src/website.js"></script>
     <![if gte IE 11]>
-	<script src="https://cdn.conversejs.org/3.1.0/dist/converse.min.js"></script>
+	<script src="dist/converse.min.js"></script>
     <![endif]>
 </head>
 

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/af/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/ca/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/converse.pot


+ 83 - 39
locale/de/LC_MESSAGES/converse.json

@@ -9,7 +9,7 @@
          },
          "Bookmark this room": [
             null,
-            "Lesezeichen setzen"
+            "Raum als Lesezeichen setzen"
          ],
          "The name for this bookmark:": [
             null,
@@ -17,7 +17,7 @@
          ],
          "Would you like this room to be automatically joined upon startup?": [
             null,
-            "Diesem Raum automatisch betreten?"
+            "Beim Anmelden diesem Raum automatisch betreten?"
          ],
          "What should your nickname for this room be?": [
             null,
@@ -33,20 +33,32 @@
          ],
          "Are you sure you want to remove the bookmark \"%1$s\"?": [
             null,
-            "Wollen Sie dieses Lesezeichen wirklich entfernen \"%1$s\"?"
+            "Wollen Sie dieses Lesezeichen \"%1$s\" wirklich entfernen?"
          ],
          "Sorry, something went wrong while trying to save your bookmark.": [
             null,
-            "Etwas ging beim Versuch des Abspeicherns des Lesezeichens schief."
+            "Entschuldigung! Beim Versuch das Lesezeichens zu speichern schlug etwas fehl."
          ],
          "Click to toggle the bookmarks list": [
             null,
-            "Zum Aus-/Einklappen klicken"
+            "Liste der Lesezeichen umschalten"
+         ],
+         "Bookmarks": [
+            null,
+            "Lesezeichen"
+         ],
+         "Leave this room": [
+            null,
+            "Diesen Raum verlassen"
          ],
          "Remove this bookmark": [
             null,
             "Dieses Lesezeichen entfernen"
          ],
+         "Unbookmark this room": [
+            null,
+            "Lesezeichen dieses Raums entfernen"
+         ],
          "Show more information on this room": [
             null,
             "Mehr Information über diesen Raum zeigen"
@@ -385,7 +397,7 @@
          ],
          "${command}": [
             null,
-            ""
+            "${command}"
          ],
          "Are you sure you want to clear the messages from this room?": [
             null,
@@ -489,7 +501,7 @@
          ],
          "${notification.reason}": [
             null,
-            ""
+            "${notification.reason}"
          ],
          " has left the room. \"": [
             null,
@@ -533,7 +545,7 @@
          ],
          "This room has reached its maximum number of occupants.": [
             null,
-            "Dieser Raum hat die maximale Mitgliederanzahl erreicht"
+            "Maximale Anzahl an Mitgliedern für diesen Raum erreicht"
          ],
          "Topic set by %1$s to: %2$s": [
             null,
@@ -553,7 +565,7 @@
          ],
          "Occupants": [
             null,
-            "Teilnehmer"
+            "Teilnehmer/Innen"
          ],
          "Invite": [
             null,
@@ -631,6 +643,10 @@
             null,
             "Dieser Raum ist moderiert"
          ],
+         "All other room occupants can see your XMPP username": [
+            null,
+            "Jeder in dem Raum kann deine XMPP/ Jabber ID sehen"
+         ],
          "Anyone can join this room": [
             null,
             "Jeder kann diesen Raum betreten"
@@ -647,6 +663,10 @@
             null,
             "Dieser Raum ist per Suche auffindbar"
          ],
+         "Only moderators can see your XMPP username": [
+            null,
+            "Nur Moderatoren können deine XMPP/ Jabber ID sehen"
+         ],
          "This room will disappear once the last person leaves": [
             null,
             "Dieser Raum verschwindet sobald diesen die letzte Person verlassen hat"
@@ -669,7 +689,7 @@
          ],
          "Please enter a valid XMPP username": [
             null,
-            ""
+            "Bitte geben Sie eine gültige XMPP/ Jabber ID an"
          ],
          "Room name": [
             null,
@@ -701,7 +721,7 @@
          ],
          "Room Address (JID):": [
             null,
-            ""
+            "XMPP/ Jabber ID (JID) dieses Raumes:"
          ],
          "Occupants:": [
             null,
@@ -813,55 +833,55 @@
          ],
          "You will be prompted to provide a security question and then an answer to that question.\n\nYour contact will then be prompted the same question and if they type the exact same answer (case sensitive), their identity will be verified.": [
             null,
-            ""
+            "Sie werden im folgenden nach einer Sicherheitsfrage und dann nach dessen Antwort gefragt.\n\nIhr Kontakt wird dann die Sicherheitsfrage lesen und die Antwort korrekt beantworten müssen (Groß- und Kleinschreibung beachten!) um die Identitäten zu überprüfen."
          ],
          "What is your security question?": [
             null,
-            ""
+            "Wie lautet Ihre Sicherheitsfrage?"
          ],
          "What is the answer to the security question?": [
             null,
-            ""
+            "Wie lautet die Antwort Ihrer Sicherheitsfrage?"
          ],
          "Invalid authentication scheme provided": [
             null,
-            ""
+            "Ungültiges Authentifizierungsmuster wurde zur Verfügung gestellt"
          ],
          "Your messages are not encrypted. Click here to enable OTR encryption.": [
             null,
-            ""
+            "Ihre Nachricht ist unverschlüsselt. Klicken Sie hier um OTR Verschlüsselung zu aktivieren."
          ],
          "Your messages are encrypted, but your contact has not been verified.": [
             null,
-            ""
+            "Ihre Nachrichten werden verschlüsselt aber Ihr Kontakt wurde noch nicht verifiziert/ bestätigt."
          ],
          "Your messages are encrypted and your contact verified.": [
             null,
-            ""
+            "Ihre Nachrichten werden verschlüsselt und Ihr Kontakt wurde bestätigt."
          ],
          "Your contact has closed their end of the private session, you should do the same": [
             null,
-            ""
+            "Ihr Kontakt hat die private Sitzung geschlossen. Sie sollten das gleiche tun."
          ],
          "End encrypted conversation": [
             null,
-            ""
+            "Beende die verschlüsselte Unterhaltung"
          ],
          "Refresh encrypted conversation": [
             null,
-            ""
+            "Verschlüsselte Unterhaltung aktualisieren"
          ],
          "Start encrypted conversation": [
             null,
-            ""
+            "Beginn einer verschlüsselten Unterhaltung"
          ],
          "Verify with fingerprints": [
             null,
-            ""
+            "Überprüfung mit Fingerabdruck"
          ],
          "Verify with SMP": [
             null,
-            ""
+            "Überprüfung mit SMP"
          ],
          "What's this?": [
             null,
@@ -889,7 +909,7 @@
          ],
          "Your XMPP provider's domain name:": [
             null,
-            ""
+            "Ihr XMPP/ Jabber Provider Domain Name:"
          ],
          "Fetch registration form": [
             null,
@@ -897,19 +917,19 @@
          ],
          "Tip: A list of public XMPP providers is available": [
             null,
-            ""
+            "Tipp: Eine Liste öffentlicher Provider ist verfügbar"
          ],
          "here": [
             null,
-            ""
+            "hier"
          ],
          "Register": [
             null,
-            ""
+            "Registrierung"
          ],
          "Sorry, the given provider does not support in band account registration. Please try with a different provider.": [
             null,
-            ""
+            "Entschuldigung: Dieser Provider erlaubt keine direkte Benutzer- Registrierung. Versuchen Sie einen anderen Provider oder erstellen Sie einen Zugang beim Provider direkt."
          ],
          "Requesting a registration form from the XMPP server": [
             null,
@@ -917,23 +937,35 @@
          ],
          "Something went wrong while establishing a connection with \"%1$s\". Are you sure it exists?": [
             null,
-            ""
+            "Etwas schlägt fehl beim Versuch eine Verbindung mit \"%1$s\" her zu stellen. Sicher das \"%1$s\" existiert?"
          ],
          "Now logging you in": [
             null,
-            ""
+            "Sie werden angemeldet"
          ],
          "Registered successfully": [
             null,
-            ""
+            "Registrierung erfolgreich"
          ],
          "The provider rejected your registration attempt. Please check the values you entered for correctness.": [
             null,
-            ""
+            "Der Provider hat die Registrierung abgelehnt. Bitte überprüfen Sie Ihre Angaben auf Richtigkeit."
          ],
          "Retry": [
             null,
-            ""
+            "Wiederholen"
+         ],
+         "Click to toggle the rooms list": [
+            null,
+            "Umschalten der Raum listen"
+         ],
+         "Open Rooms": [
+            null,
+            "Offene Räume"
+         ],
+         "Are you sure you want to leave the room \"%1$s\"?": [
+            null,
+            "Wollen Sie diesen Raum \"%1$s\" wirklich verlassen?"
          ],
          "This contact is busy": [
             null,
@@ -981,11 +1013,11 @@
          ],
          "Filter": [
             null,
-            ""
+            "Filter"
          ],
          "State": [
             null,
-            ""
+            "Staat"
          ],
          "Any": [
             null,
@@ -993,7 +1025,7 @@
          ],
          "Unread": [
             null,
-            ""
+            "Ungelesen"
          ],
          "Chatty": [
             null,
@@ -1001,7 +1033,19 @@
          ],
          "Extended Away": [
             null,
-            ""
+            "Länger nicht anwesend"
+         ],
+         "Click to remove %1$s as a contact": [
+            null,
+            "Hier klicken um %1$s zu entfernen"
+         ],
+         "Click to accept the contact request from %1$s": [
+            null,
+            "Hier klicken um die Kontaktanfrage von %1$s zu akzeptieren"
+         ],
+         "Click to decline the contact request from %1$s": [
+            null,
+            "Hier klicken um die Kontaktanfrage von %1$s abzulehnen"
          ],
          "Click to chat with this contact": [
             null,
@@ -1009,7 +1053,7 @@
          ],
          "Name": [
             null,
-            ""
+            "Name"
          ],
          "Are you sure you want to remove this contact?": [
             null,

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 225 - 215
locale/de/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/es/LC_MESSAGES/converse.po


+ 101 - 13
locale/fr/LC_MESSAGES/converse.json

@@ -31,6 +31,10 @@
             null,
             "Annuler"
          ],
+         "Are you sure you want to remove the bookmark \"%1$s\"?": [
+            null,
+            "Voulez-vous vraiment retirer le marque-page « %1$s » ?"
+         ],
          "Sorry, something went wrong while trying to save your bookmark.": [
             null,
             "Désolé, quelque chose s’est mal passé pendant la sauvegarde de ce marque-page."
@@ -39,9 +43,21 @@
             null,
             "Cliquer pour ouvrir la liste des salons"
          ],
+         "Bookmarks": [
+            null,
+            "Marques-page"
+         ],
+         "Leave this room": [
+            null,
+            "Quitter ce salon"
+         ],
          "Remove this bookmark": [
             null,
-            "Supprimer ce marque-page"
+            "Retirer ce marque-page"
+         ],
+         "Unbookmark this room": [
+            null,
+            "Retirer ce salon"
          ],
          "Show more information on this room": [
             null,
@@ -65,7 +81,7 @@
          ],
          "Send": [
             null,
-            ""
+            "Envoyer"
          ],
          "me": [
             null,
@@ -77,7 +93,7 @@
          ],
          "Typing from another device": [
             null,
-            ""
+            "Saisie depuis un autre appareil"
          ],
          "is typing": [
             null,
@@ -85,7 +101,7 @@
          ],
          "Stopped typing on the other device": [
             null,
-            ""
+            "Fin de saisie depuis l’autre appareil"
          ],
          "has stopped typing": [
             null,
@@ -381,7 +397,7 @@
          ],
          "${command}": [
             null,
-            ""
+            "${command}"
          ],
          "Are you sure you want to clear the messages from this room?": [
             null,
@@ -503,6 +519,34 @@
             null,
             " a rejoint le salon."
          ],
+         "You are not on the member list of this room.": [
+            null,
+            "Vous n’êtes pas dans la liste des membres de ce salon."
+         ],
+         "You have been banned from this room.": [
+            null,
+            "Vous avez été banni de ce salon."
+         ],
+         "No nickname was specified.": [
+            null,
+            "Aucun alias n’a été indiqué."
+         ],
+         "You are not allowed to create new rooms.": [
+            null,
+            "Vous n’êtes pas autorisé à créer des salons."
+         ],
+         "Your nickname doesn't conform to this room's policies.": [
+            null,
+            "Votre alias n’est pas conforme à la politique de ce salon."
+         ],
+         "This room does not (yet) exist.": [
+            null,
+            "Ce salon n’existe pas (pour l’instant)."
+         ],
+         "This room has reached its maximum number of occupants.": [
+            null,
+            "Ce salon a atteint sa limite maximale d’occupants."
+         ],
          "Topic set by %1$s to: %2$s": [
             null,
             "Le sujet « %2$s » a été défini par %1$s"
@@ -583,6 +627,10 @@
             null,
             "Non sécurisé"
          ],
+         "This room is not publicly searchable": [
+            null,
+            "Ce salon ne peut pas être recherché publiquement"
+         ],
          "Messages are archived on the server": [
             null,
             "Les messages sont archivés sur le serveur"
@@ -595,6 +643,10 @@
             null,
             "Ce salon est modéré"
          ],
+         "All other room occupants can see your XMPP username": [
+            null,
+            "Tous les autres occupants de ce salon peuvent voir votre nom d’utilisateur XMPP"
+         ],
          "Anyone can join this room": [
             null,
             "N’importe qui peut rejoindre ce salon"
@@ -603,6 +655,18 @@
             null,
             "Ce salon nécessite un mot de passe pour y accéder"
          ],
+         "This room persists even if it's unoccupied": [
+            null,
+            "Ce salon persiste même s'il est inoccupé"
+         ],
+         "This room is publicly searchable": [
+            null,
+            "Ce salon peut être recherché publiquement"
+         ],
+         "Only moderators can see your XMPP username": [
+            null,
+            "Seuls les modérateurs peuvent voir votre identifiant XMPP"
+         ],
          "This room will disappear once the last person leaves": [
             null,
             "Ce salon disparaîtra au départ de la dernière personne"
@@ -625,7 +689,7 @@
          ],
          "Please enter a valid XMPP username": [
             null,
-            ""
+            "Veuillez saisir un identifiant utilisateur XMPP valide"
          ],
          "Room name": [
             null,
@@ -657,7 +721,7 @@
          ],
          "Room Address (JID):": [
             null,
-            ""
+            "Adresse du salon (JID) :"
          ],
          "Occupants:": [
             null,
@@ -889,7 +953,19 @@
          ],
          "Retry": [
             null,
-            ""
+            "Réessayer"
+         ],
+         "Click to toggle the rooms list": [
+            null,
+            "Cliquer pour ouvrir la liste des salons"
+         ],
+         "Open Rooms": [
+            null,
+            "Ouvrir les salons"
+         ],
+         "Are you sure you want to leave the room \"%1$s\"?": [
+            null,
+            "Voulez-vous vraiment supprimer le marque-page « %1$s » ?"
          ],
          "This contact is busy": [
             null,
@@ -949,15 +1025,27 @@
          ],
          "Unread": [
             null,
-            ""
+            "Non lu"
          ],
          "Chatty": [
             null,
-            ""
+            "Bavard"
          ],
          "Extended Away": [
             null,
-            ""
+            "Absence longue durée"
+         ],
+         "Click to remove %1$s as a contact": [
+            null,
+            "Cliquez pour retirer le contact « %1$s »"
+         ],
+         "Click to accept the contact request from %1$s": [
+            null,
+            "Cliquez pour accepter la demande de « %1$s »"
+         ],
+         "Click to decline the contact request from %1$s": [
+            null,
+            "Cliquez pour décliner la demande de contact de « %1$s »"
          ],
          "Click to chat with this contact": [
             null,
@@ -969,11 +1057,11 @@
          ],
          "Are you sure you want to remove this contact?": [
             null,
-            "Voulez-vous vraiment supprimer ce contact ?"
+            "Voulez-vous vraiment retirer ce contact ?"
          ],
          "Are you sure you want to decline this contact request?": [
             null,
-            "Voulez-vous vraiment refuser cette demande de contact ?"
+            "Voulez-vous vraiment décliner cette demande de contact ?"
          ]
       }
    }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 232 - 231
locale/fr/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/he/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/hu/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/id/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/it/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/ja/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/nb/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/nl/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/pl/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/pt_BR/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/ru/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 207 - 193
locale/uk/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 206 - 192
locale/zh/LC_MESSAGES/converse.po


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 552 - 124
package-lock.json


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "converse.js",
-  "version": "3.2.0",
+  "version": "3.2.1",
   "description": "Browser based XMPP instant messaging client",
   "main": "main.js",
   "directories": {

+ 5 - 1
sass/_controlbox.scss

@@ -67,12 +67,16 @@
                 display: none;
             }
             .provider-title {
-                font-size: 22px;
+                font-size: $font-size-huge;
+                margin: 0;
             }
             .provider-score {
                 width: 178px;
                 margin-bottom: 8px;
             }
+            .title, .instructions, label {
+                margin: 0.5em 0 0 0;
+            }
             .form-help .url {
                 font-weight: bold;
                 color: $link-color;

+ 11 - 1
sass/_core.scss

@@ -194,9 +194,19 @@
             padding: 1em;
             legend {
                 color: $text-color;
+                font-size: 125%;
+            }
+            input[type=checkbox] {
+                display: block;
+            }
+            select,
+            input[type=password],
+            input[type=number],
+            input[type=text] {
+                min-width: 50%;
             }
             label {
-                margin-top: 1em;
+                margin: 1em 0;
                 font-size: $font-size-large;
             }
             input[type=text],

+ 6 - 2
sass/inverse/_controlbox.scss

@@ -40,9 +40,13 @@
             #available-chatrooms {
                 padding: 0 1em 2em 1em;
             }
-            input[type=submit] {
+            .title, .instructions, label {
+                margin: 1em 0;
+            }
+            input[type=submit],
+            input[type=button] {
                 width: auto;
-                margin-top: 1.5em;
+                margin: 1.5em 1em 0 0;
             }
         }
         #controlbox-tabs {

+ 4 - 5
sass/inverse/_core.scss

@@ -39,11 +39,10 @@ body {
     form {
         &.pure-form.converse-form {
             margin: 1em;
-            legend {
-                color: $text-color;
-            }
-            label {
-                margin-top: 1em;
+
+            input[type=checkbox] {
+                margin-left: 1em;
+                display: inline;
             }
             input[type=text],
             input[type=password],

+ 2 - 1
spec/bookmarks.js

@@ -81,7 +81,8 @@
             $form.find('input[name="name"]').val('Play&apos;s the Thing');
             $form.find('input[name="autojoin"]').prop('checked', true);
             $form.find('input[name="nick"]').val('JC');
-            $form.submit();
+            view.$el.find('.button-primary').click();
+
             expect(view.model.get('bookmarked')).toBeTruthy();
             expect($bookmark.hasClass('on-button'), true);
 

+ 21 - 11
spec/chatbox.js

@@ -743,7 +743,7 @@
                         }));
                     });
 
-                    it("will cause the chat area to be scrolled down only if it was at the bottom already",
+                    it("will cause the chat area to be scrolled down only if it was at the bottom originally",
                         mock.initConverseWithPromises(
                             null, ['rosterGroupsFetched'], {},
                             function (done, _converse) {
@@ -769,10 +769,9 @@
                                 }).c('body').t('Message: '+i).up()
                                 .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree());
                         }
-
                         test_utils.waitUntil(function () {
                                 return chatboxview.$content.scrollTop();
-                            }, 500)
+                            }, 1000)
                         .then(function () {
                             return test_utils.waitUntil(function () {
                                 return !chatboxview.model.get('auto_scrolled');
@@ -1756,31 +1755,42 @@
                         test_utils.openContactsPanel(_converse);
                         test_utils.waitUntil(function () {
                             return _converse.rosterview.$el.find('dt').length;
-                        }, 300).then(function () {
+                        }, 500).then(function () {
                             // Make the timeouts shorter so that we can test
                             _converse.TIMEOUTS.PAUSED = 200;
                             _converse.TIMEOUTS.INACTIVE = 200;
                             contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
                             test_utils.openChatBoxFor(_converse, contact_jid);
                             view = _converse.chatboxviews.get(contact_jid);
+                            return test_utils.waitUntil(function () {
+                                return view.model.get('chat_state') === 'active';
+                            }, 500);
+                        }).then(function () {
+                            console.log('chat_state set to active');
+                            view = _converse.chatboxviews.get(contact_jid);
                             expect(view.model.get('chat_state')).toBe('active');
                             view.keyPressed({
                                 target: view.$el.find('textarea.chat-textarea'),
                                 keyCode: 1
                             });
+                            return test_utils.waitUntil(function () {
+                                return view.model.get('chat_state') === 'composing';
+                            }, 500);
+                        }).then(function () {
+                            console.log('chat_state set to composing');
+                            view = _converse.chatboxviews.get(contact_jid);
                             expect(view.model.get('chat_state')).toBe('composing');
                             spyOn(_converse.connection, 'send');
                             return test_utils.waitUntil(function () {
-                                if (view.model.get('chat_state') === 'paused') {
-                                    return true;
-                                }
-                                return false;
-                            }, 300);
+                                return view.model.get('chat_state') === 'paused';
+                            }, 500);
                         }).then(function () {
+                            console.log('chat_state set to paused');
                             return test_utils.waitUntil(function () {
                                 return view.model.get('chat_state') === 'inactive';
-                            }, 300);
+                            }, 500);
                         }).then(function () {
+                            console.log('chat_state set to inactive');
                             expect(_converse.connection.send).toHaveBeenCalled();
                             var calls = _.filter(_converse.connection.send.calls.all(), function (call) {
                                 return call.args[0] instanceof Strophe.Builder;
@@ -1800,7 +1810,7 @@
                             expect($stanza.children().get(1).tagName).toBe('no-store');
                             expect($stanza.children().get(2).tagName).toBe('no-permanent-store');
                             done();
-                        });
+                        }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     }));
 
                     it("is sent when the user a minimizes a chat box",

+ 7 - 12
spec/chatroom.js

@@ -1643,9 +1643,11 @@
                 mock.initConverseWithPromises(
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
+
                 test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
                 var view = _converse.chatboxviews.get('lounge@localhost'),
                     trimmed_chatboxes = _converse.minimized_chats;
+
                 spyOn(view, 'minimize').and.callThrough();
                 spyOn(view, 'maximize').and.callThrough();
                 spyOn(_converse, 'emit');
@@ -1659,18 +1661,11 @@
                 expect(view.minimize).toHaveBeenCalled();
                 var trimmedview = trimmed_chatboxes.get(view.model.get('id'));
                 trimmedview.$("a.restore-chat").click();
-
-                test_utils.waitUntil(function () {
-                        return view.$el.is(':visible');
-                    }, 300)
-                .then(function () {
-                    expect(view.maximize).toHaveBeenCalled();
-                    expect(_converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
-                    expect(view.$el.is(':visible')).toBeTruthy();
-                    expect(view.model.get('minimized')).toBeFalsy();
-                    expect(_converse.emit.calls.count(), 3);
-                    done();
-                });
+                expect(view.maximize).toHaveBeenCalled();
+                expect(_converse.emit).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
+                expect(view.model.get('minimized')).toBeFalsy();
+                expect(_converse.emit.calls.count(), 3);
+                done();
             }));
 
             it("can be closed again by clicking a DOM element with class 'close-chatbox-button'",

+ 11 - 0
spec/controlbox.js

@@ -137,6 +137,17 @@
                         });
                         _converse.rosterview.update(); // XXX: Will normally called as event handler
                     }
+
+                    $.fn.hasScrollBar = function() {
+                        if (!$.contains(document, this.get(0))) {
+                            return false;
+                        }
+                        if(this.parent().height() < this.get(0).scrollHeight) {
+                            return true;
+                        }
+                        return false;
+                    };
+
                     return test_utils.waitUntil(function () {
                         if (_converse.rosterview.$roster.hasScrollBar()) {
                             return $filter.is(':visible');

+ 90 - 25
spec/register.js

@@ -3,20 +3,42 @@
 } (this, function ($, jasmine, mock, converse, test_utils) {
     var Strophe = converse.env.Strophe;
     var $iq = converse.env.$iq;
+    var _ = converse.env._;
 
     describe("The Registration Panel", function () {
 
-        it("is not available unless allow_registration=true",  mock.initConverse(function (_converse) {
+        it("is not available unless allow_registration=true",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: false },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _converse.chatboxviews.get('controlbox');
+                }, 300)
+            .then(function () {
+
             test_utils.openControlBox();
             var cbview = _converse.chatboxviews.get('controlbox');
             expect(cbview.$('#controlbox-tabs li').length).toBe(1);
             expect(cbview.$('#controlbox-tabs li').text().trim()).toBe("Sign in");
-
-            }, { auto_login: false,
-                 allow_registration: false,
+            done();
+            });
         }));
 
-        it("can be opened by clicking on the registration tab", mock.initConverse(function (_converse) {
+        it("can be opened by clicking on the registration tab",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: true },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel');
+                }, 300)
+            .then(function () {
+
             var cbview = _converse.chatboxviews.get('controlbox');
             test_utils.openControlBox();
             var $tabs = cbview.$('#controlbox-tabs');
@@ -32,12 +54,22 @@
             expect($login.is(':visible')).toBe(false);
             expect($registration.is(':visible')).toBe(true);
             expect(cbview.switchTab).toHaveBeenCalled();
-
-            }, { auto_login: false,
-                 allow_registration: true,
+            done();
+            });
         }));
 
-        it("allows the user to choose an XMPP provider's domain", mock.initConverse(function (_converse) {
+        it("allows the user to choose an XMPP provider's domain",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: true },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel');
+                }, 300)
+            .then(function () {
+
             var cbview = _converse.chatboxviews.get('controlbox');
             var registerview = cbview.registerpanel;
             spyOn(registerview, 'onProviderChosen').and.callThrough();
@@ -59,11 +91,22 @@
             $form.find('input[type=submit]').click();
             expect(registerview.onProviderChosen).toHaveBeenCalled();
             expect(_converse.connection.connect).toHaveBeenCalled();
-        }, { auto_login: false,
-              allow_registration: true,
-            }));
+            done();
+            });
+        }));
+
+        it("will render a registration form as received from the XMPP provider",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: true },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel');
+                }, 300)
+            .then(function () {
 
-        it("will render a registration form as received from the XMPP provider", mock.initConverse(function (_converse) {
             var cbview = _converse.chatboxviews.get('controlbox');
             cbview.$('#controlbox-tabs').find('li').last().find('a').click(); // Click the Register tab
             var registerview = _converse.chatboxviews.get('controlbox').registerpanel;
@@ -107,14 +150,25 @@
             expect(registerview.$('input').length).toBe(5);
             expect(registerview.$('input[type=submit]').length).toBe(1);
             expect(registerview.$('input[type=button]').length).toBe(1);
-        }, { auto_login: false,
-              allow_registration: true,
-            }));
+            done();
+            });
+        }));
+
+        it("will set form_type to legacy and submit it as legacy",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: true },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel');
+                }, 300)
+            .then(function () {
 
-        it("will set form_type to legacy and submit it as legacy", mock.initConverse(function (_converse) {
             var cbview = _converse.chatboxviews.get('controlbox');
             cbview.$('#controlbox-tabs').find('li').last().find('a').click(); // Click the Register tab
-            var registerview = _converse.chatboxviews.get('controlbox').registerpanel;
+            var registerview = cbview.registerpanel;
             spyOn(registerview, 'onProviderChosen').and.callThrough();
             spyOn(registerview, 'getRegistrationFields').and.callThrough();
             spyOn(registerview, 'onRegistrationFields').and.callThrough();
@@ -156,11 +210,22 @@
             var $stanza = $(_converse.connection.send.calls.argsFor(0)[0].tree());
             expect($stanza.children('query').children().length).toBe(3);
             expect($stanza.children('query').children()[0].tagName).toBe('username');
-        }, { auto_login: false,
-              allow_registration: true,
-            }));
+            done();
+            });
+        }));
+
+        it("will set form_type to xform and submit it as xform",
+            mock.initConverseWithPromises(
+                null, ['connectionInitialized', 'chatBoxesInitialized'],
+                { auto_login: false,
+                  allow_registration: true },
+                function (done, _converse) {
+
+            test_utils.waitUntil(function () {
+                    return _.get(_converse.chatboxviews.get('controlbox'), 'registerpanel');
+                }, 300)
+            .then(function () {
 
-        it("will set form_type to xform and submit it as xform", mock.initConverse(function (_converse) {
             var cbview = _converse.chatboxviews.get('controlbox');
             cbview.$('#controlbox-tabs').find('li').last().find('a').click(); // Click the Register tab
             var registerview = _converse.chatboxviews.get('controlbox').registerpanel;
@@ -208,8 +273,8 @@
             expect($stanza.children('query').children().length).toBe(1);
             expect($stanza.children('query').children().children().length).toBe(3);
             expect($stanza.children('query').children().children()[0].tagName).toBe('field');
-        }, { auto_login: false,
-              allow_registration: true,
-            }));
+            done();
+            });
+        }));
     });
 }));

+ 2 - 0
src/build-inverse.js

@@ -4,9 +4,11 @@
     mainConfigFile: 'config.js',
     paths: {
         "converse-bookmarks":       "builds/converse-bookmarks",
+        "converse-chatboxes":       "builds/converse-chatboxes",
         "converse-chatview":        "builds/converse-chatview",
         "converse-controlbox":      "builds/converse-controlbox",
         "converse-core":            "builds/converse-core",
+        "converse-disco":           "builds/converse-disco",
         "converse-dragresize":      "builds/converse-dragresize",
         "converse-headline":        "builds/converse-headline",
         "converse-inverse":         "builds/converse-inverse",

+ 4 - 1
src/build.js

@@ -4,9 +4,11 @@
     mainConfigFile: 'config.js',
     paths: {
         "converse-bookmarks":       "builds/converse-bookmarks",
+        "converse-chatboxes":       "builds/converse-chatboxes",
         "converse-chatview":        "builds/converse-chatview",
         "converse-controlbox":      "builds/converse-controlbox",
         "converse-core":            "builds/converse-core",
+        "converse-disco":           "builds/converse-disco",
         "converse-dragresize":      "builds/converse-dragresize",
         "converse-headline":        "builds/converse-headline",
         "converse-inverse":         "builds/converse-inverse",
@@ -22,7 +24,8 @@
         "converse-rosterview":      "builds/converse-rosterview",
         "converse-singleton":       "builds/converse-singleton",
         "converse-vcard":           "builds/converse-vcard",
-        "utils":                    "builds/utils"
+        "utils":                    "builds/utils",
+        "form-utils":               "builds/form-utils"
     },
     wrap: {
         startFile: "start.frag",

+ 4 - 1
src/config.js

@@ -25,11 +25,13 @@ require.config({
         "emojione":                 "node_modules/emojione/lib/js/emojione",
         "es6-promise":              "node_modules/es6-promise/dist/es6-promise.auto",
         "eventemitter":             "node_modules/otr/build/dep/eventemitter",
+        "form-utils":               "src/form-utils",
         "jquery":                   "node_modules/jquery/dist/jquery",
         "jquery.browser":           "node_modules/jquery.browser/dist/jquery.browser",
         "jquery.noconflict":        "src/jquery.noconflict",
         "lodash":                   "node_modules/lodash/lodash",
         "lodash.converter":         "3rdparty/lodash.fp",
+        "lodash.fp":                "src/lodash.fp",
         "lodash.noconflict":        "src/lodash.noconflict",
         "pluggable":                "node_modules/pluggable.js/dist/pluggable",
         "polyfill":                 "src/polyfill",
@@ -44,14 +46,15 @@ require.config({
         "typeahead":                "components/typeahead.js/index",
         "underscore":               "src/underscore-shim",
         "utils":                    "src/utils",
-        "xss.noconflict":           "src/xss.noconflict",
         "xss":                      "node_modules/xss/dist/xss",
+        "xss.noconflict":           "src/xss.noconflict",
 
         // Converse
         "converse":                 "src/converse",
         "inverse":                  "src/inverse",
 
         "converse-bookmarks":       "src/converse-bookmarks",
+        "converse-chatboxes":       "src/converse-chatboxes",
         "converse-chatview":        "src/converse-chatview",
         "converse-controlbox":      "src/converse-controlbox",
         "converse-core":            "src/converse-core",

+ 26 - 10
src/converse-bookmarks.js

@@ -101,10 +101,11 @@
                 },
 
                 onBookmarked () {
+                    const icon = this.el.querySelector('.icon-pushpin');
                     if (this.model.get('bookmarked')) {
-                        this.$('.icon-pushpin').addClass('button-on');
+                        icon.classList.add('button-on');
                     } else {
-                        this.$('.icon-pushpin').removeClass('button-on');
+                        icon.classList.remove('button-on');
                     }
                 },
 
@@ -125,11 +126,18 @@
                 renderBookmarkForm () {
                     const { _converse } = this.__super__,
                         { __ } = _converse,
-                        $body = this.$('.chatroom-body');
-                    $body.children().addClass('hidden');
+                        body = this.el.querySelector('.chatroom-body');
+
+                    _.each(body.children, function (child) {
+                        child.classList.add('hidden');
+                    });
                     // Remove any existing forms
-                    $body.find('form.chatroom-form').remove();
-                    $body.append(
+                    let form = body.querySelector('form.chatroom-form');
+                    if (!_.isNull(form)) {
+                        form.parentNode.removeChild(form);
+                    }
+                    body.insertAdjacentHTML(
+                        'beforeend', 
                         tpl_chatroom_bookmark_form({
                             heading: __('Bookmark this room'),
                             label_name: __('The name for this bookmark:'),
@@ -138,9 +146,17 @@
                             default_nick: this.model.get('nick'),
                             label_submit: __('Save'),
                             label_cancel: __('Cancel')
-                        }));
-                    this.$('.chatroom-form').submit(this.onBookmarkFormSubmitted.bind(this));
-                    this.$('.chatroom-form .button-cancel').on('click', this.cancelConfiguration.bind(this));
+                        })
+                    );
+                    form = body.querySelector('form.chatroom-form');
+                    form.addEventListener(
+                        'submit',
+                        this.onBookmarkFormSubmitted.bind(this)
+                    );
+                    form.querySelector('.button-cancel').addEventListener(
+                        'click',
+                        this.cancelConfiguration.bind(this)
+                    );
                 },
 
                 onBookmarkFormSubmitted (ev) {
@@ -173,7 +189,7 @@
                         _.forEach(models, function (model) {
                             model.destroy();
                         });
-                        this.$('.icon-pushpin').removeClass('button-on');
+                        this.el.querySelector('.icon-pushpin').classList.remove('button-on');
                     }
                 }
             }

+ 384 - 0
src/converse-chatboxes.js

@@ -0,0 +1,384 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global define */
+
+(function (root, factory) {
+    define(["converse-core"], factory);
+}(this, function (converse) {
+    "use strict";
+    const { Backbone, Strophe, b64_sha1, utils, _ } = converse.env;
+
+    converse.plugins.add('converse-chatboxes', {
+
+        overrides: {
+            // Overrides mentioned here will be picked up by converse.js's
+            // plugin architecture they will replace existing methods on the
+            // relevant objects or classes.
+
+            disconnect: function () {
+                const { _converse } = this.__super__;
+                _converse.chatboxviews.closeAllChatBoxes();
+                return this.__super__.disconnect.apply(this, arguments);
+            },
+
+            logOut: function () {
+                const { _converse } = this.__super__;
+                _converse.chatboxviews.closeAllChatBoxes();
+                return this.__super__.logOut.apply(this, arguments);
+            },
+
+            initStatus: function () {
+                const { _converse } = this.__super__;
+                _converse.chatboxviews.closeAllChatBoxes();
+                return this.__super__.initStatus.apply(this, arguments);
+            },
+
+            onStatusInitialized: function () {
+                const { _converse } = this.__super__;
+                _converse.chatboxes.onConnected();
+                return this.__super__.onStatusInitialized.apply(this, arguments);
+            }
+        },
+
+        initialize () {
+            /* The initialize function gets called as soon as the plugin is
+             * loaded by converse.js's plugin machinery.
+             */
+            const { _converse } = this;
+
+            _converse.api.promises.add([
+                'chatBoxesFetched',
+                'chatBoxesInitialized'
+            ]);
+
+            _converse.ChatBoxes = Backbone.Collection.extend({
+                comparator: 'time_opened',
+
+                model (attrs, options) {
+                    return new _converse.ChatBox(attrs, options);
+                },
+
+                registerMessageHandler () {
+                    _converse.connection.addHandler(
+                        this.onMessage.bind(this), null, 'message', 'chat'
+                    );
+                    _converse.connection.addHandler(
+                        this.onErrorMessage.bind(this), null, 'message', 'error'
+                    );
+                },
+
+                chatBoxMayBeShown (chatbox) {
+                    return true;
+                },
+
+                onChatBoxesFetched (collection) {
+                    /* Show chat boxes upon receiving them from sessionStorage
+                    *
+                    * This method gets overridden entirely in src/converse-controlbox.js
+                    * if the controlbox plugin is active.
+                    */
+                    collection.each((chatbox) => {
+                        if (this.chatBoxMayBeShown(chatbox)) {
+                            chatbox.trigger('show');
+                        }
+                    });
+                    _converse.emit('chatBoxesFetched');
+                },
+
+                onConnected () {
+                    this.browserStorage = new Backbone.BrowserStorage[_converse.storage](
+                        b64_sha1(`converse.chatboxes-${_converse.bare_jid}`));
+                    this.registerMessageHandler();
+                    this.fetch({
+                        add: true,
+                        success: this.onChatBoxesFetched.bind(this)
+                    });
+                },
+
+                onErrorMessage (message) {
+                    /* Handler method for all incoming error message stanzas
+                    */
+                    // TODO: we can likely just reuse "onMessage" below
+                    const from_jid =  Strophe.getBareJidFromJid(message.getAttribute('from'));
+                    if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
+                        return true;
+                    }
+                    // Get chat box, but only create a new one when the message has a body.
+                    const chatbox = this.getChatBox(from_jid);
+                    if (!chatbox) {
+                        return true;
+                    }
+                    chatbox.createMessage(message, null, message);
+                    return true;
+                },
+
+                onMessage (message) {
+                    /* Handler method for all incoming single-user chat "message"
+                    * stanzas.
+                    */
+                    let contact_jid, delay, resource,
+                        from_jid = message.getAttribute('from'),
+                        to_jid = message.getAttribute('to');
+
+                    const original_stanza = message,
+                        to_resource = Strophe.getResourceFromJid(to_jid),
+                        is_carbon = !_.isNull(message.querySelector(`received[xmlns="${Strophe.NS.CARBONS}"]`));
+
+                    if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
+                        _converse.log(
+                            `onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`,
+                            Strophe.LogLevel.INFO
+                        );
+                        return true;
+                    } else if (utils.isHeadlineMessage(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.
+                        _converse.log(
+                            `onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${from_jid}`,
+                            Strophe.LogLevel.INFO
+                        );
+                        return true;
+                    }
+                    const forwarded = message.querySelector('forwarded');
+                    if (!_.isNull(forwarded)) {
+                        const forwarded_message = forwarded.querySelector('message');
+                        const forwarded_from = forwarded_message.getAttribute('from');
+                        if (is_carbon && Strophe.getBareJidFromJid(forwarded_from) !== from_jid) {
+                            // Prevent message forging via carbons
+                            //
+                            // https://xmpp.org/extensions/xep-0280.html#security
+                            return true;
+                        }
+                        message = forwarded_message;
+                        delay = forwarded.querySelector('delay');
+                        from_jid = message.getAttribute('from');
+                        to_jid = message.getAttribute('to');
+                    }
+
+                    const from_bare_jid = Strophe.getBareJidFromJid(from_jid),
+                        from_resource = Strophe.getResourceFromJid(from_jid),
+                        is_me = from_bare_jid === _converse.bare_jid;
+
+                    if (is_me) {
+                        // I am the sender, so this must be a forwarded message...
+                        contact_jid = Strophe.getBareJidFromJid(to_jid);
+                        resource = Strophe.getResourceFromJid(to_jid);
+                    } else {
+                        contact_jid = from_bare_jid;
+                        resource = from_resource;
+                    }
+                    // Get chat box, but only create a new one when the message has a body.
+                    const chatbox = this.getChatBox(contact_jid, !_.isNull(message.querySelector('body'))),
+                        msgid = message.getAttribute('id');
+
+                    if (chatbox) {
+                        const messages = msgid && chatbox.messages.findWhere({msgid}) || [];
+                        if (_.isEmpty(messages)) {
+                            // Only create the message when we're sure it's not a
+                            // duplicate
+                            chatbox.incrementUnreadMsgCounter(original_stanza);
+                            chatbox.createMessage(message, delay, original_stanza);
+                        }
+                    }
+                    _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
+                    return true;
+                },
+
+                createChatBox (jid, attrs) {
+                    /* Creates a chat box
+                    *
+                    * Parameters:
+                    *    (String) jid - The JID of the user for whom a chat box
+                    *      gets created.
+                    *    (Object) attrs - Optional chat box atributes.
+                    */
+                    const bare_jid = Strophe.getBareJidFromJid(jid),
+                        roster_item = _converse.roster.get(bare_jid);
+                    let roster_info = {};
+
+                    if (! _.isUndefined(roster_item)) {
+                        roster_info = {
+                            'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
+                            'image_type': roster_item.get('image_type'),
+                            'image': roster_item.get('image'),
+                            'url': roster_item.get('url'),
+                        };
+                    } else if (!_converse.allow_non_roster_messaging) {
+                        _converse.log(`Could not get roster item for JID ${bare_jid}`+
+                            ' and allow_non_roster_messaging is set to false',
+                            Strophe.LogLevel.ERROR);
+                        return;
+                    }
+                    return this.create(_.assignIn({
+                            'id': bare_jid,
+                            'jid': bare_jid,
+                            'fullname': jid,
+                            'image_type': _converse.DEFAULT_IMAGE_TYPE,
+                            'image': _converse.DEFAULT_IMAGE,
+                            'url': '',
+                        }, roster_info, attrs || {}));
+                },
+
+                getChatBox (jid, create, attrs) {
+                    /* Returns a chat box or optionally return a newly
+                    * created one if one doesn't exist.
+                    *
+                    * Parameters:
+                    *    (String) jid - The JID of the user whose chat box we want
+                    *    (Boolean) create - Should a new chat box be created if none exists?
+                    *    (Object) attrs - Optional chat box atributes.
+                    */
+                    jid = jid.toLowerCase();
+                    let  chatbox = this.get(Strophe.getBareJidFromJid(jid));
+                    if (!chatbox && create) {
+                        chatbox = this.createChatBox(jid, attrs);
+                    }
+                    return chatbox;
+                }
+            });
+
+            _converse.ChatBoxViews = Backbone.Overview.extend({
+
+                initialize () {
+                    this.model.on("add", this.onChatBoxAdded, this);
+                    this.model.on("destroy", this.removeChat, this);
+                },
+
+                _ensureElement () {
+                    /* Override method from backbone.js
+                    * If the #conversejs element doesn't exist, create it.
+                    */
+                    if (!this.el) {
+                        let el = document.querySelector('#conversejs');
+                        if (_.isNull(el)) {
+                            el = document.createElement('div');
+                            el.setAttribute('id', 'conversejs');
+                            // Converse.js expects a <body> tag to be present.
+                            document.querySelector('body').appendChild(el);
+                        }
+                        el.innerHTML = '';
+                        this.setElement(el, false);
+                    } else {
+                        this.setElement(_.result(this, 'el'), false);
+                    }
+                },
+
+                onChatBoxAdded (item) {
+                    // Views aren't created here, since the core code doesn't
+                    // contain any views. Instead, they're created in overrides in
+                    // plugins, such as in converse-chatview.js and converse-muc.js
+                    return this.get(item.get('id'));
+                },
+
+                removeChat (item) {
+                    this.remove(item.get('id'));
+                },
+
+                closeAllChatBoxes () {
+                    /* This method gets overridden in src/converse-controlbox.js if
+                    * the controlbox plugin is active.
+                    */
+                    this.each(function (view) { view.close(); });
+                    return this;
+                },
+
+                chatBoxMayBeShown (chatbox) {
+                    return this.model.chatBoxMayBeShown(chatbox);
+                },
+
+                getChatBox (attrs, create) {
+                    let chatbox  = this.model.get(attrs.jid);
+                    if (!chatbox && create) {
+                        chatbox = this.model.create(attrs, {
+                            'error' (model, response) {
+                                _converse.log(response.responseText);
+                            }
+                        });
+                    }
+                    return chatbox;
+                },
+
+                showChat (attrs) {
+                    /* Find the chat box and show it (if it may be shown).
+                    * If it doesn't exist, create it.
+                    */
+                    const chatbox = this.getChatBox(attrs, true);
+                    if (this.chatBoxMayBeShown(chatbox)) {
+                        chatbox.trigger('show', true);
+                    }
+                    return chatbox;
+                }
+            });
+
+            // BEGIN: Event handlers
+            _converse.api.listen.on('pluginsInitialized', () => {
+                _converse.chatboxes = new _converse.ChatBoxes();
+                _converse.chatboxviews = new _converse.ChatBoxViews({
+                    'model': _converse.chatboxes
+                });
+                _converse.emit('chatBoxesInitialized');
+            });
+
+            _converse.api.listen.on('beforeTearDown', () => {
+                _converse.chatboxes.remove(); // Don't call off(), events won't get re-registered upon reconnect.
+                delete _converse.chatboxes.browserStorage;
+            });
+            // END: Event handlers
+
+            _converse.getViewForChatBox = function (chatbox) {
+                if (!chatbox) { return; }
+                return _converse.chatboxviews.get(chatbox.get('id'));
+            };
+
+            /* We extend the default converse.js API */
+            _.extend(_converse.api, {
+                'chats': {
+                    'open' (jids, attrs) {
+                        if (_.isUndefined(jids)) {
+                            _converse.log("chats.open: You need to provide at least one JID", Strophe.LogLevel.ERROR);
+                            return null;
+                        } else if (_.isString(jids)) {
+                            return _converse.getViewForChatBox(
+                                _converse.chatboxes.getChatBox(jids, true, attrs).trigger('show')
+                            );
+                        }
+                        return _.map(jids, (jid) =>
+                            _converse.getViewForChatBox(
+                                _converse.chatboxes.getChatBox(jid, true, attrs).trigger('show')
+                            )
+                        );
+                    },
+                    'get' (jids) {
+                        if (_.isUndefined(jids)) {
+                            const result = [];
+                            _converse.chatboxes.each(function (chatbox) {
+                                // FIXME: Leaky abstraction from MUC. We need to add a
+                                // base type for chat boxes, and check for that.
+                                if (chatbox.get('type') !== 'chatroom') {
+                                    result.push(_converse.getViewForChatBox(chatbox));
+                                }
+                            });
+                            return result;
+                        } else if (_.isString(jids)) {
+                            return _converse.getViewForChatBox(_converse.chatboxes.getChatBox(jids));
+                        }
+                        return _.map(jids,
+                            _.partial(
+                                _.flow(
+                                    _converse.chatboxes.getChatBox.bind(_converse.chatboxes),
+                                    _converse.getViewForChatBox.bind(_converse)
+                                ), _, true
+                            )
+                        );
+                    }
+                }
+            });
+        }
+    });
+    return converse;
+}));

+ 7 - 4
src/converse-chatview.js

@@ -10,6 +10,7 @@
     define([
             "jquery.noconflict",
             "converse-core",
+            "converse-chatboxes",
             "emojione",
             "xss",
             "tpl!chatbox",
@@ -25,6 +26,7 @@
 }(this, function (
             $,
             converse,
+            dummy,
             emojione,
             xss,
             tpl_chatbox,
@@ -303,8 +305,9 @@
                 },
 
                 insertIntoDOM () {
-                    /* This method gets overridden in src/converse-controlbox.js if
-                     * the controlbox plugin is active.
+                    /* This method gets overridden in src/converse-controlbox.js
+                     * as well as src/converse-muc.js (if those plugins are
+                     * enabled).
                      */
                     const container = document.querySelector('#conversejs');
                     if (this.el.parentNode !== container) {
@@ -706,10 +709,10 @@
                     }
                     if (state === _converse.COMPOSING) {
                         this.chat_state_timeout = window.setTimeout(
-                                this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, _converse.PAUSED);
+                            this.setChatState.bind(this), _converse.TIMEOUTS.PAUSED, _converse.PAUSED);
                     } else if (state === _converse.PAUSED) {
                         this.chat_state_timeout = window.setTimeout(
-                                this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, _converse.INACTIVE);
+                            this.setChatState.bind(this), _converse.TIMEOUTS.INACTIVE, _converse.INACTIVE);
                     }
                     if (!no_save && this.model.get('chat_state') !== state) {
                         this.model.set('chat_state', state);

+ 44 - 32
src/converse-controlbox.js

@@ -9,6 +9,7 @@
 (function (root, factory) {
     define(["jquery.noconflict",
             "converse-core",
+            "lodash.fp",
             "tpl!add_contact_dropdown",
             "tpl!add_contact_form",
             "tpl!change_status_message",
@@ -28,6 +29,7 @@
 }(this, function (
             $,
             converse,
+            fp,
             tpl_add_contact_dropdown,
             tpl_add_contact_form,
             tpl_change_status_message,
@@ -46,7 +48,7 @@
 
     const USERS_PANEL_ID = 'users';
     const CHATBOX_TYPE = 'chatbox';
-    const { Strophe, Backbone, utils, _, fp, moment } = converse.env;
+    const { Strophe, Backbone, Promise, utils, _, moment } = converse.env;
 
 
     converse.plugins.add('converse-controlbox', {
@@ -58,18 +60,6 @@
             //
             // New functions which don't exist yet can also be added.
 
-            initChatBoxes () {
-                this.__super__.initChatBoxes.apply(this, arguments);
-                this.controlboxtoggle = new this.ControlBoxToggle();
-            },
-
-            initConnection () {
-                this.__super__.initConnection.apply(this, arguments);
-                if (this.connection) {
-                    this.addControlBox();
-                }
-            },
-
             _tearDown () {
                 this.__super__._tearDown.apply(this, arguments);
                 if (this.rosterview) {
@@ -100,8 +90,8 @@
                 },
 
                 onChatBoxesFetched (collection, resp) {
-                    const { _converse } = this.__super__;
                     this.__super__.onChatBoxesFetched.apply(this, arguments);
+                    const { _converse } = this.__super__;
                     if (!_.includes(_.map(collection, 'id'), 'controlbox')) {
                         _converse.addControlBox();
                     }
@@ -157,7 +147,6 @@
                 }
             },
 
-
             ChatBox: {
                 initialize () {
                     if (this.get('id') === 'controlbox') {
@@ -168,11 +157,14 @@
                 },
             },
 
-
             ChatBoxView: {
                 insertIntoDOM () {
-                    const { _converse } = this.__super__;
-                    this.$el.insertAfter(_converse.chatboxviews.get("controlbox").$el);
+                    const view = this.__super__._converse.chatboxviews.get("controlbox");
+                    if (view) {
+                        view.el.insertAdjacentElement('afterend', this.el)
+                    } else {
+                        this.__super__.insertIntoDOM.apply(this, arguments);
+                    }
                     return this;
                 }
             }
@@ -188,6 +180,7 @@
             _converse.api.settings.update({
                 allow_logout: true,
                 default_domain: undefined,
+                locked_domain: undefined,
                 show_controlbox_by_default: false,
                 sticky_controlbox: false,
                 xhr_user_search: false,
@@ -196,14 +189,14 @@
 
             const LABEL_CONTACTS = __('Contacts');
 
-            _converse.addControlBox = () =>
+            _converse.addControlBox = () => {
                 _converse.chatboxes.add({
                     id: 'controlbox',
                     box_id: 'controlbox',
                     type: 'controlbox',
                     closed: !_converse.show_controlbox_by_default
                 })
-            ;
+            };
 
             _converse.ControlBoxView = _converse.ChatBoxView.extend({
                 tagName: 'div',
@@ -215,7 +208,10 @@
                 },
 
                 initialize () {
-                    this.$el.insertAfter(_converse.controlboxtoggle.$el);
+                    if (_.isUndefined(_converse.controlboxtoggle)) {
+                        _converse.controlboxtoggle = new _converse.ControlBoxToggle();
+                        this.$el.insertAfter(_converse.controlboxtoggle.$el);
+                    }
                     this.model.on('change:connected', this.onConnected, this);
                     this.model.on('destroy', this.hide, this);
                     this.model.on('hide', this.hide, this);
@@ -223,7 +219,9 @@
                     this.model.on('change:closed', this.ensureClosedState, this);
                     this.render();
                     if (this.model.get('connected')) {
-                        this.insertRoster();
+                        _converse.api.waitUntil('rosterViewInitialized')
+                            .then(this.insertRoster.bind(this))
+                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                     }
                 },
 
@@ -256,7 +254,10 @@
 
                 onConnected () {
                     if (this.model.get('connected')) {
-                        this.render().insertRoster();
+                        this.render();
+                        _converse.api.waitUntil('rosterViewInitialized')
+                            .then(this.insertRoster.bind(this))
+                            .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                         this.model.save();
                     }
                 },
@@ -410,6 +411,8 @@
                 },
 
                 authenticate (ev) {
+                    /* Authenticate the user based on a form submission event.
+                     */
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
                     const $form = $(ev.target);
                     if (_converse.authentication === _converse.ANONYMOUS) {
@@ -423,10 +426,14 @@
                     let jid = $jid_input.val(),
                         errors = false;
 
-                    if (!jid || _.filter(jid.split('@')).length < 2) {
+                    if (!jid || (
+                            !_converse.locked_domain &&
+                            !_converse.default_domain &&
+                            _.filter(jid.split('@')).length < 2)) {
                         errors = true;
                         $jid_input.addClass('error');
                     }
+
                     if (!password && _converse.authentication !== _converse.EXTERNAL)  {
                         errors = true;
                         $pw_input.addClass('error');
@@ -732,15 +739,15 @@
                 },
 
                 initialize () {
-                    _converse.chatboxviews.$el.prepend(this.render());
+                    _converse.chatboxviews.$el.prepend(this.render().el);
                     this.updateOnlineCount();
                     const that = this;
-                    _converse.on('initialized', function () {
+                    _converse.api.waitUntil('initialized').then(() => {
                         _converse.roster.on("add", that.updateOnlineCount, that);
                         _converse.roster.on('change', that.updateOnlineCount, that);
                         _converse.roster.on("destroy", that.updateOnlineCount, that);
                         _converse.roster.on("remove", that.updateOnlineCount, that);
-                    });
+                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                 },
 
                 render () {
@@ -748,11 +755,10 @@
                     // the ControlBox or the Toggle must be shown. This prevents
                     // artifacts (i.e. on page load the toggle is shown only to then
                     // seconds later be hidden in favor of the control box).
-                    return this.$el.html(
-                        tpl_controlbox_toggle({
-                            'label_toggle': __('Toggle chat')
-                        })
-                    );
+                    this.el.innerHTML = tpl_controlbox_toggle({
+                        'label_toggle': __('Toggle chat')
+                    })
+                    return this;
                 },
 
                 updateOnlineCount: _.debounce(function () {
@@ -802,6 +808,12 @@
                 }
             });
 
+            Promise.all([
+                _converse.api.waitUntil('connectionInitialized'),
+                _converse.api.waitUntil('chatBoxesInitialized')
+            ]).then(_converse.addControlBox)
+              .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+
             const disconnect =  function () {
                 /* Upon disconnection, set connected to `false`, so that if
                  * we reconnect,

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 2 - 6
src/converse-core.js


+ 5 - 5
src/converse-inverse.js

@@ -1,7 +1,7 @@
 // Converse.js (A browser based XMPP chat client)
 // http://conversejs.org
 //
-// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
+// Copyright (c) JC Brand <jc@opkode.com>
 // Licensed under the Mozilla Public License (MPLv2)
 //
 /*global define */
@@ -65,16 +65,16 @@
 
                 renderRegistrationPanel () {
                     this.__super__.renderRegistrationPanel.apply(this, arguments);
-
-                    const el = document.getElementById('converse-register');
-                    el.parentNode.insertBefore(createBrandHeadingElement(), el);
+                    if (this.__super__._converse.allow_registration) {
+                        const el = document.getElementById('converse-register');
+                        el.parentNode.insertBefore(createBrandHeadingElement(), el);
+                    }
                     return this;
                 },
 
                 renderLoginPanel () {
                     this.__super__.renderLoginPanel.apply(this, arguments);
                     this.el.classList.add("fullscreen");
-
                     const el = document.getElementById('converse-login');
                     el.parentNode.insertBefore(createBrandHeadingElement(), el);
                     return this;

+ 90 - 58
src/converse-minimize.js

@@ -13,9 +13,7 @@
             "tpl!toggle_chats",
             "tpl!trimmed_chat",
             "tpl!chats_panel",
-            "converse-controlbox",
-            "converse-chatview",
-            "converse-muc"
+            "converse-chatview"
     ], factory);
 }(this, function (
         $,
@@ -27,9 +25,23 @@
     ) {
     "use strict";
 
-    const { _ , utils, Backbone, b64_sha1, moment } = converse.env;
+    const { _ , utils, Backbone, Promise, Strophe, b64_sha1, moment } = converse.env;
 
     converse.plugins.add('converse-minimize', {
+        /* Optional dependencies are other plugins which might be
+         * overridden or relied upon, and therefore need to be loaded before
+         * this plugin. They are called "optional" because they might not be
+         * available, in which case any overrides applicable to them will be
+         * ignored.
+         *
+         * It's possible however to make optional dependencies non-optional.
+         * If the setting "strict_plugin_dependencies" is set to true,
+         * an error will be raised if the plugin is not found.
+         *
+         * NB: These plugins need to have already been loaded via require.js.
+         */
+        optional_dependencies: ["converse-controlbox", "converse-muc"],
+
         overrides: {
             // Overrides mentioned here will be picked up by converse.js's
             // plugin architecture they will replace existing methods on the
@@ -37,15 +49,6 @@
             //
             // New functions which don't exist yet can also be added.
 
-            initChatBoxes () {
-                const { _converse } = this.__super__;
-                const result = this.__super__.initChatBoxes.apply(this, arguments);
-                _converse.minimized_chats = new _converse.MinimizedChats({
-                    model: _converse.chatboxes
-                });
-                return result;
-            },
-
             registerGlobalEventHandlers () {
                 const { _converse } = this.__super__;
                 $(window).on("resize", _.debounce(function (ev) {
@@ -136,12 +139,13 @@
                 maximize () {
                     // Restores a minimized chat box
                     const { _converse } = this.__super__;
-                    this.$el.insertAfter(_converse.chatboxviews.get("controlbox").$el);
+                    this.insertIntoDOM();
+
                     if (!this.model.isScrolledUp()) {
                         this.model.clearUnreadMsgCounter();
                     }
                     this.show();
-                    _converse.emit('chatBoxMaximized', this);
+                    this.__super__._converse.emit('chatBoxMaximized', this);
                     return this;
                 },
 
@@ -186,11 +190,12 @@
                     const html = this.__super__.generateHeadingHTML.apply(this, arguments);
                     const div = document.createElement('div');
                     div.innerHTML = html;
-                    const el = tpl_chatbox_minimize(
-                        {info_minimize: __('Minimize this chat box')}
-                    );
                     const button = div.querySelector('.close-chatbox-button');
-                    button.insertAdjacentHTML('afterend', el);
+                    button.insertAdjacentHTML('afterend',
+                        tpl_chatbox_minimize({
+                            'info_minimize': __('Minimize this chat box')
+                        })
+                    );
                     return div.innerHTML;
                 }
             },
@@ -251,29 +256,31 @@
                         // fullscreen. In this case we don't trim.
                         return;
                     }
-                    const $minimized = _converse.minimized_chats.$el,
-                        minimized_width = _.includes(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0,
-                        new_id = newchat ? newchat.model.get('id') : null;
-
-                    const boxes_width = _.reduce(
-                        this.xget(new_id),
-                        (memo, view) => memo + this.getChatBoxWidth(view),
-                        newchat ? newchat.$el.outerWidth(true) : 0);
-
-                    if ((minimized_width + boxes_width) > $('body').outerWidth(true)) {
-                        const oldest_chat = this.getOldestMaximizedChat([new_id]);
-                        if (oldest_chat) {
-                            // We hide the chat immediately, because waiting
-                            // for the event to fire (and letting the
-                            // ChatBoxView hide it then) causes race
-                            // conditions.
-                            const view = this.get(oldest_chat.get('id'));
-                            if (view) {
-                                view.hide();
+                    _converse.api.waitUntil('minimizedChatsInitialized').then(() => {
+                        const $minimized = _.get(_converse.minimized_chats, '$el'),
+                            minimized_width = _.includes(this.model.pluck('minimized'), true) ? $minimized.outerWidth(true) : 0,
+                            new_id = newchat ? newchat.model.get('id') : null;
+
+                        const boxes_width = _.reduce(
+                            this.xget(new_id),
+                            (memo, view) => memo + this.getChatBoxWidth(view),
+                            newchat ? newchat.$el.outerWidth(true) : 0);
+
+                        if ((minimized_width + boxes_width) > $('body').outerWidth(true)) {
+                            const oldest_chat = this.getOldestMaximizedChat([new_id]);
+                            if (oldest_chat) {
+                                // We hide the chat immediately, because waiting
+                                // for the event to fire (and letting the
+                                // ChatBoxView hide it then) causes race
+                                // conditions.
+                                const view = this.get(oldest_chat.get('id'));
+                                if (view) {
+                                    view.hide();
+                                }
+                                oldest_chat.minimize();
                             }
-                            oldest_chat.minimize();
                         }
-                    }
+                    }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
                 },
 
                 getOldestMaximizedChat (exclude_ids) {
@@ -312,6 +319,8 @@
                 no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
             });
 
+            _converse.api.promises.add('minimizedChatsInitialized');
+
             _converse.MinimizedChatBoxView = Backbone.View.extend({
                 tagName: 'div',
                 className: 'chat-head',
@@ -374,12 +383,27 @@
                 initialize () {
                     this.render();
                     this.initToggle();
+                    this.addMultipleChats(this.model.where({'minimized': true}));
                     this.model.on("add", this.onChanged, this);
                     this.model.on("destroy", this.removeChat, this);
                     this.model.on("change:minimized", this.onChanged, this);
                     this.model.on('change:num_unread', this.updateUnreadMessagesCounter, this);
                 },
 
+                render () {
+                    if (!this.el.parentElement) {
+                        this.el.innerHTML = tpl_chats_panel();
+                        _converse.chatboxviews.el.appendChild(this.el);
+                    }
+                    if (this.keys().length === 0) {
+                        this.el.classList.add('hidden');
+                    } else if (this.keys().length > 0 && !this.$el.is(':visible')) {
+                        this.el.classList.remove('hidden');
+                        _converse.chatboxviews.trimChats();
+                    }
+                    return this.$el;
+                },
+
                 tearDown () {
                     this.model.off("add", this.onChanged);
                     this.model.off("destroy", this.removeChat);
@@ -398,21 +422,6 @@
                     this.toggleview.model.fetch();
                 },
 
-                render () {
-                    if (!this.el.parentElement) {
-                        this.el.innerHTML = tpl_chats_panel();
-                        _converse.chatboxviews.el.appendChild(this.el);
-                    }
-                    if (this.keys().length === 0) {
-                        this.el.classList.add('hidden');
-                        _converse.chatboxviews.trimChats.bind(_converse.chatboxviews);
-                    } else if (this.keys().length > 0 && !this.$el.is(':visible')) {
-                        this.el.classList.remove('hidden');
-                        _converse.chatboxviews.trimChats();
-                    }
-                    return this.$el;
-                },
-
                 toggle (ev) {
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
                     this.toggleview.model.save({'collapsed': !this.toggleview.model.get('collapsed')});
@@ -431,6 +440,20 @@
                     }
                 },
 
+                addMultipleChats (items) {
+                    _.each(items, (item) => {
+                        const existing = this.get(item.get('id'));
+                        if (existing && existing.$el.parent().length !== 0) {
+                            return;
+                        }
+                        const view = new _converse.MinimizedChatBoxView({model: item});
+                        this.$('.minimized-chats-flyout').append(view.render());
+                        this.add(item.get('id'), view);
+                    });
+                    this.toggleview.model.set({'num_minimized': this.keys().length});
+                    this.render();
+                },
+
                 addChat (item) {
                     const existing = this.get(item.get('id'));
                     if (existing && existing.$el.parent().length !== 0) {
@@ -492,7 +515,17 @@
                 }
             });
 
-            const renderMinimizeButton = function (view) {
+            Promise.all([
+                _converse.api.waitUntil('connectionInitialized'),
+                _converse.api.waitUntil('chatBoxesInitialized')
+            ]).then(() => {
+                _converse.minimized_chats = new _converse.MinimizedChats({
+                    model: _converse.chatboxes
+                });
+                _converse.emit('minimizedChatsInitialized');
+            }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
+
+            _converse.on('chatBoxOpened', function renderMinimizeButton (view) {
                 // Inserts a "minimize" button in the chatview's header
                 const $el = view.$el.find('.toggle-chatbox-button');
                 const $new_el = tpl_chatbox_minimize(
@@ -503,8 +536,7 @@
                 } else {
                     view.$el.find('.close-chatbox-button').after($new_el);
                 }
-            };
-            _converse.on('chatBoxOpened', renderMinimizeButton);
+            });
 
             _converse.on('controlBoxOpened', function (chatbox) {
                 // Wrapped in anon method because at scan time, chatboxviews

+ 23 - 31
src/converse-muc.js

@@ -12,7 +12,9 @@
 (function (root, factory) {
     define([
             "jquery.noconflict",
+            "form-utils",
             "converse-core",
+            "lodash.fp",
             "tpl!chatarea",
             "tpl!chatroom",
             "tpl!chatroom_disconnect",
@@ -37,7 +39,9 @@
     ], factory);
 }(this, function (
             $,
+            utils,
             converse,
+            fp,
             tpl_chatarea,
             tpl_chatroom,
             tpl_chatroom_disconnect,
@@ -63,7 +67,7 @@
     const ROOMS_PANEL_ID = 'chatrooms';
     const CHATROOMS_TYPE = 'chatroom';
 
-    const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, utils, _, fp, moment } = converse.env;
+    const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, _, moment } = converse.env;
 
     // Add Strophe Namespaces
     Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
@@ -189,11 +193,12 @@
                         _converse.api.listen.on('serviceDiscovered', this.featureAdded, this);
                         // Features could have been added before the controlbox was
                         // initialized. We're only interested in MUC
-                        const feature = _converse.disco_entities[_converse.domain].features.findWhere({
-                            'var': Strophe.NS.MUC
-                        });
-                        if (feature) {
-                            this.featureAdded(feature);
+                        const entity = _converse.disco_entities[_converse.domain];
+                        if (!_.isUndefined(entity)) {
+                            const feature = entity.features.findWhere({'var': Strophe.NS.MUC });
+                            if (feature) {
+                                this.featureAdded(feature);
+                            }
                         }
                     });
                 },
@@ -520,19 +525,6 @@
                     return this;
                 },
 
-                insertIntoDOM () {
-                    if (document.querySelector('body').contains(this.el)) {
-                        return;
-                    }
-                    const view = _converse.chatboxviews.get("controlbox");
-                    if (view) {
-                        this.$el.insertAfter(view.$el);
-                    } else {
-                        $('#conversejs').prepend(this.$el);
-                    }
-                    return this;
-                },
-
                 generateHeadingHTML () {
                     /* Returns the heading HTML to be rendered.
                      */
@@ -968,7 +960,8 @@
                     // TODO check if first argument is valid
                     if (args.length < 1 || args.length > 2) {
                         this.showStatusNotification(
-                            __(`Error: the "${command}" command takes two arguments, the user's nickname and optionally a reason.`),
+                            __('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
+                                command),
                             true
                         );
                         return false;
@@ -1281,7 +1274,7 @@
                         $fieldset.append($('<p class="instructions">').text(instructions));
                     }
                     _.each($fields, function (field) {
-                        $fieldset.append(utils.xForm2webForm($(field), $stanza));
+                        $fieldset.append(utils.xForm2webForm(field, stanza));
                     });
                     $form.append('<fieldset></fieldset>');
                     $fieldset = $form.children('fieldset:last');
@@ -1565,7 +1558,7 @@
                     if (_converse.muc_nickname_from_jid) {
                         // We try to enter the room with the node part of
                         // the user's JID.
-                        this.join(Strophe.unescapeNode(Strophe.getNodeFromJid(_converse.bare_jid)));
+                        this.join(this.getDefaultNickName());
                     } else {
                         this.renderNicknameForm(message);
                     }
@@ -1753,10 +1746,10 @@
                     if (notification.disconnected) {
                         this.showDisconnectMessage(notification.disconnection_message);
                         if (notification.actor) {
-                            this.showDisconnectMessage(__(___('This action was done by %1$s.'), notification.actor));
+                            this.showDisconnectMessage(__('This action was done by %1$s.', notification.actor));
                         }
                         if (notification.reason) {
-                            this.showDisconnectMessage(__(___('The reason given is: "%1$s".'), notification.reason));
+                            this.showDisconnectMessage(__('The reason given is: "%1$s".', notification.reason));
                         }
                         this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED);
                         return;
@@ -1765,7 +1758,7 @@
                         this.$content.append(tpl_info({'message': message}));
                     });
                     if (notification.reason) {
-                        this.showStatusNotification(__(`The reason given is: "${notification.reason}"`), true);
+                        this.showStatusNotification(__('The reason given is: "%1$s "', notification.reason), true);
                     }
                     if (notification.messages.length) {
                         this.scrollDown();
@@ -2072,7 +2065,7 @@
                             { 'jid': '',
                               'show': show,
                               'hint_show': _converse.PRETTY_CHAT_STATUS[show],
-                              'hint_occupant': __(`Click to mention ${this.model.get('nick')} in your message.`),
+                              'hint_occupant': __('Click to mention %1$s in your message.', this.model.get('nick')),
                               'desc_moderator': __('This user is a moderator.'),
                               'desc_occupant': __('This user can send messages in this room.'),
                               'desc_visitor': __('This user can NOT send messages in this room.')
@@ -2329,7 +2322,7 @@
 
                 promptForInvite (suggestion) {
                     const reason = prompt(
-                        __(___('You are about to invite %1$s to the chat room "%2$s". '), suggestion.text.label, this.model.get('id')) +
+                        __('You are about to invite %1$s to the chat room "%2$s". ', suggestion.text.label, this.model.get('id')) +
                         __("You may optionally include a message, explaining the reason for the invitation.")
                     );
                     if (reason !== null) {
@@ -2472,7 +2465,7 @@
                 informNoRoomsFound () {
                     const $available_chatrooms = this.$el.find('#available-chatrooms');
                     // For translators: %1$s is a variable and will be replaced with the XMPP server name
-                    $available_chatrooms.html(`<dt>${__('No rooms on %1$s',this.model.get('muc_domain'))}</dt>`);
+                    $available_chatrooms.html(`<dt>${__('No rooms on %1$s', this.model.get('muc_domain'))}</dt>`);
                     $('input#show-rooms').show().siblings('span.spinner').remove();
                 },
 
@@ -2671,12 +2664,11 @@
                     contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from);
                     if (!reason) {
                         result = confirm(
-                            __(___("%1$s has invited you to join a chat room: %2$s"),
-                                contact, room_jid)
+                            __("%1$s has invited you to join a chat room: %2$s", contact, room_jid)
                         );
                     } else {
                         result = confirm(
-                            __(___('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"'),
+                            __('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"',
                                 contact, room_jid, reason)
                         );
                     }

+ 4 - 3
src/converse-otr.js

@@ -21,9 +21,10 @@
 
     const { Strophe, utils, b64_sha1, _ } = converse.env;
 
-    const HAS_CSPRNG = ((!_.isUndefined(crypto)) &&
-        ((_.isFunction(crypto.randomBytes)) || (_.isFunction(crypto.getRandomValues))
-    ));
+    const HAS_CSPRNG = _.isUndefined(window.crypto) ? false : (
+        _.isFunction(window.crypto.randomBytes) ||
+        _.isFunction(window.crypto.getRandomValues)
+    );
 
     const HAS_CRYPTO = HAS_CSPRNG && (
         (!_.isUndefined(otr.OTR)) &&

+ 3 - 1
src/converse-ping.js

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

+ 4 - 2
src/converse-register.js

@@ -11,6 +11,7 @@
  */
 (function (root, factory) {
     define(["jquery.noconflict",
+            "form-utils",
             "converse-core",
             "tpl!form_username",
             "tpl!register_panel",
@@ -22,6 +23,7 @@
     ], factory);
 }(this, function (
             $,
+            utils,
             converse,
             tpl_form_username,
             tpl_register_panel,
@@ -34,7 +36,7 @@
     "use strict";
 
     // Strophe methods for building stanzas
-    const { Strophe, Backbone, utils, $iq, _ } = converse.env;
+    const { Strophe, Backbone, $iq, _ } = converse.env;
 
     // Add Strophe Namespaces
     Strophe.addNamespace('REGISTER', 'jabber:iq:register');
@@ -356,7 +358,7 @@
                     if (this.form_type === 'xform') {
                         $fields = $stanza.find('field');
                         _.each($fields, (field) => {
-                            $form.append(utils.xForm2webForm.bind(this, $(field), $stanza));
+                            $form.append(utils.xForm2webForm.bind(this, field, stanza));
                         });
                     } else {
                         // Show fields

+ 5 - 2
src/converse-rosterview.js

@@ -14,7 +14,8 @@
             "tpl!requesting_contact",
             "tpl!roster",
             "tpl!roster_filter",
-            "tpl!roster_item"
+            "tpl!roster_item",
+            "converse-chatboxes"
     ], factory);
 }(this, function (
             $,
@@ -75,6 +76,7 @@
                 allow_contact_removal: true,
                 show_toolbar: true,
             });
+            _converse.api.promises.add('rosterViewInitialized');
 
             const STATUSES = {
                 'dnd': __('This contact is busy'),
@@ -665,7 +667,7 @@
                                 this.remove();
                             },
                             function (err) {
-                                alert(__(`Sorry, there was an error while trying to remove ${name} as a contact.`));
+                                alert(__('Sorry, there was an error while trying to remove %1$s as a contact.', name));
                                 _converse.log(err, Strophe.LogLevel.ERROR);
                             }
                         );
@@ -949,6 +951,7 @@
                     'model': _converse.rostergroups
                 });
                 _converse.rosterview.render();
+                _converse.emit('rosterViewInitialized');
             };
             _converse.api.listen.on('rosterInitialized', initRoster);
             _converse.api.listen.on('rosterReadyAfterReconnection', initRoster);

+ 26 - 22
src/converse-vcard.js

@@ -71,7 +71,7 @@
             };
 
             _converse.onVCardError = function (jid, iq, errback) {
-                const contact = _converse.roster.get(jid);
+                const contact = _.get(_converse.roster, jid);
                 if (contact) {
                     contact.save({ 'vcard_updated': moment().format() });
                 }
@@ -131,28 +131,32 @@
             });
 
             const updateVCardForChatBox = function (chatbox) {
-                if (!_converse.use_vcards) { return; }
-                const jid = chatbox.model.get('jid'),
-                    contact = _converse.roster.get(jid);
-                if ((contact) && (!contact.get('vcard_updated'))) {
-                    _converse.getVCard(
-                        jid,
-                        function (iq, jid, fullname, image, image_type, url) {
-                            chatbox.model.save({
-                                'fullname' : fullname || jid,
-                                'url': url,
-                                'image_type': image_type,
-                                'image': image
-                            });
-                        },
-                        function () {
-                            _converse.log(
-                                "updateVCardForChatBox: Error occured while fetching vcard",
-                                Strophe.LogLevel.ERROR
-                            );
-                        }
-                    );
+                if (!_converse.use_vcards || chatbox.model.get('type') === 'headline') {
+                    return;
                 }
+                _converse.api.waitUntil('rosterInitialized').then(() => {
+                    const jid = chatbox.model.get('jid'),
+                        contact = _converse.roster.get(jid);
+                    if ((contact) && (!contact.get('vcard_updated'))) {
+                        _converse.getVCard(
+                            jid,
+                            function (iq, jid, fullname, image, image_type, url) {
+                                chatbox.model.save({
+                                    'fullname' : fullname || jid,
+                                    'url': url,
+                                    'image_type': image_type,
+                                    'image': image
+                                });
+                            },
+                            function () {
+                                _converse.log(
+                                    "updateVCardForChatBox: Error occured while fetching vcard",
+                                    Strophe.LogLevel.ERROR
+                                );
+                            }
+                        );
+                    }
+                }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
             };
             _converse.on('chatBoxInitialized', updateVCardForChatBox);
 

+ 160 - 0
src/form-utils.js

@@ -0,0 +1,160 @@
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// This is the utilities module.
+//
+// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global define, escape, locales, Jed */
+(function (root, factory) {
+    define([
+        "sizzle",
+        "lodash.noconflict",
+        "utils",
+        "tpl!field",
+        "tpl!select_option",
+        "tpl!form_select",
+        "tpl!form_textarea",
+        "tpl!form_checkbox",
+        "tpl!form_username",
+        "tpl!form_input",
+        "tpl!form_captcha"
+    ], factory);
+}(this, function (
+        sizzle,
+        _,
+        u,
+        tpl_field,
+        tpl_select_option,
+        tpl_form_select,
+        tpl_form_textarea,
+        tpl_form_checkbox,
+        tpl_form_username,
+        tpl_form_input,
+        tpl_form_captcha
+    ) {
+    "use strict";
+
+    var XFORM_TYPE_MAP = {
+        'text-private': 'password',
+        'text-single': 'text',
+        'fixed': 'label',
+        'boolean': 'checkbox',
+        'hidden': 'hidden',
+        'jid-multi': 'textarea',
+        'list-single': 'dropdown',
+        'list-multi': 'dropdown'
+    };
+
+    u.webForm2xForm = function (field) {
+        /* Takes an HTML DOM and turns it into an XForm field.
+         *
+         * Parameters:
+         *      (DOMElement) field - the field to convert
+         */
+        let value;
+        if (field.getAttribute('type') === 'checkbox') {
+            value = field.checked && 1 || 0;
+        } else if (field.tagName == "textarea") {
+            value = _.filter(field.value.split('\n'), _.trim);
+        } else {
+            value = field.value;
+        }
+        return u.stringToDOM(
+            tpl_field({
+                name: field.getAttribute('name'),
+                value: value
+            })
+        )[0];
+    };
+
+    u.xForm2webForm = function (field, stanza) {
+        /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
+         * and turns it into an HTML field.
+         *
+         * Returns either text or a DOM element (which is not ideal, but fine
+         * for now).
+         *
+         *  Parameters:
+         *      (XMLElement) field - the field to convert
+         */
+        if (field.getAttribute('type') === 'list-single' ||
+            field.getAttribute('type') === 'list-multi') {
+
+            const values = _.map(
+                u.queryChildren(field, 'value'),
+                _.partial(_.get, _, 'textContent')
+            );
+            const options = _.map(
+                u.queryChildren(field, 'option'),
+                function (option) {
+                    const value = _.get(option.querySelector('value'), 'textContent');
+                    return tpl_select_option({
+                        'value': value,
+                        'label': option.getAttribute('label'),
+                        'selected': _.startsWith(values, value),
+                        'required': _.isNil(field.querySelector('required'))
+                    })
+                }
+            );
+            return tpl_form_select({
+                'name': field.getAttribute('var'),
+                'label': field.getAttribute('label'),
+                'options': options.join(''),
+                'multiple': (field.getAttribute('type') === 'list-multi'),
+                'required': _.isNil(field.querySelector('required'))
+            })
+        } else if (field.getAttribute('type') === 'fixed') {
+            const text = _.get(field.querySelector('value'), 'textContent');
+            const el = u.stringToDOM('<p class="form-help">');
+            el.textContent = text;
+            return el;
+        } else if (field.getAttribute('type') === 'jid-multi') {
+            return tpl_form_textarea({
+                'name': field.getAttribute('var'),
+                'label': field.getAttribute('label') || '',
+                'value': _.get(field.querySelector('value'), 'textContent'),
+                'required': _.isNil(field.querySelector('required'))
+            })
+        } else if (field.getAttribute('type') === 'boolean') {
+            return tpl_form_checkbox({
+                'name': field.getAttribute('var'),
+                'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+                'label': field.getAttribute('label') || '',
+                'checked': _.get(field.querySelector('value'), 'textContent') === "1" && 'checked="1"' || '',
+                'required': _.isNil(field.querySelector('required'))
+            })
+        } else if (field.getAttribute('type') && field.getAttribute('var') === 'username') {
+            return tpl_form_username({
+                'domain': ' @'+this.domain,
+                'name': field.getAttribute('var'),
+                'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+                'label': field.getAttribute('label') || '',
+                'value': _.get(field.querySelector('value'), 'textContent'),
+                'required': _.isNil(field.querySelector('required'))
+            })
+        } else if (field.getAttribute('type')) {
+            return tpl_form_input({
+                'name': field.getAttribute('var'),
+                'type': XFORM_TYPE_MAP[field.getAttribute('type')],
+                'label': field.getAttribute('label') || '',
+                'value': _.get(field.querySelector('value'), 'textContent'),
+                'required': _.isNil(field.querySelector('required'))
+            })
+        } else {
+            if (field.getAttribute('var') === 'ocr') { // Captcha
+                const uri = field.querySelector('uri');
+                const el = sizzle('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]', stanza)[0];
+                return tpl_form_captcha({
+                    'label': field.getAttribute('label'),
+                    'name': field.getAttribute('var'),
+                    'data': _.get(el, 'textContent'),
+                    'type': uri.getAttribute('type'),
+                    'required': _.isNil(field.querySelector('required'))
+                })
+            }
+        }
+    }
+    return u;
+}));

+ 4 - 2
src/lodash.fp.js

@@ -1,3 +1,5 @@
-define(['lodash', 'lodash.fpConverter'], function (_, lodashBrowserConvert) {
-    return lodashBrowserConvert(_.runInContext());
+define(['lodash', 'lodash.converter', 'converse-core'], function (_, lodashConverter, converse) {
+    var fp = lodashConverter(_.runInContext());
+    converse.env.fp = fp;
+    return fp;
 });

+ 14 - 0
src/polyfill.js

@@ -1,3 +1,17 @@
+if (!String.prototype.includes) {
+  String.prototype.includes = function(search, start) {
+        'use strict';
+        if (typeof start !== 'number') {
+            start = 0;
+        }
+        if (start + search.length > this.length) {
+            return false;
+        } else {
+            return this.indexOf(search, start) !== -1;  // eslint-disable-line lodash/prefer-includes
+        }
+  };
+}
+
 if (!String.prototype.endsWith) {
   String.prototype.endsWith = function (searchString, position) {
       var subjectString = this.toString();

+ 1 - 1
src/start.frag

@@ -2,7 +2,7 @@
  *
  *  An XMPP chat client that runs in the browser.
  *
- *  Version: 3.2.0-rc
+ *  Version: 3.2.1
  */
 
 /* jshint ignore:start */

+ 2 - 1
src/templates/field.html

@@ -1,4 +1,5 @@
-<field var="{{{name}}}">{[ if (_.isArray(value)) { ]}
+<field var="{{{name}}}">
+{[ if (_.isArray(value)) { ]}
     {[ _.each(value,function(arrayValue) { ]}<value>{{{arrayValue}}}</value>{[ }); ]}
 {[ } else { ]}
     <value>{{{value}}}</value>

+ 2 - 2
src/templates/form_checkbox.html

@@ -1,2 +1,2 @@
-<label>{{{label}}}</label>
-<input name="{{{name}}}" type="{{{type}}}" {{{checked}}}>
+<label class="checkbox" for="{{{name}}}">{{{label}}}<input name="{{{name}}}" type="{{{type}}}" {{{checked}}}></label>
+

+ 3 - 5
src/templates/form_input.html

@@ -1,8 +1,6 @@
-{[ if (label) { ]}
 <label>
     {{{label}}}
+    <input name="{{{name}}}" type="{{{type}}}" 
+        {[ if (value) { ]} value="{{{value}}}" {[ } ]}
+        {[ if (required) { ]} class="required" {[ } ]} >
 </label>
-{[ } ]}
-<input name="{{{name}}}" type="{{{type}}}" 
-    {[ if (value) { ]} value="{{{value}}}" {[ } ]}
-    {[ if (required) { ]} class="required" {[ } ]} >

+ 4 - 2
src/templates/form_select.html

@@ -1,2 +1,4 @@
-<label>{{{label}}}</label>
-<select name="{{{name}}}"  {[ if (multiple) { ]} multiple="multiple" {[ } ]}>{{options}}</select>
+<label>
+    {{{label}}}
+    <select name="{{{name}}}"  {[ if (multiple) { ]} multiple="multiple" {[ } ]}>{{options}}</select>
+</label>

+ 146 - 229
src/utils.js

@@ -1,7 +1,14 @@
-/*global define, escape, locales, Jed */
+// Converse.js (A browser based XMPP chat client)
+// http://conversejs.org
+//
+// This is the utilities module.
+//
+// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
+// Licensed under the Mozilla Public License (MPLv2)
+//
+/*global define, escape, locales, window, Jed */
 (function (root, factory) {
     define([
-        "jquery.noconflict",
         "sizzle",
         "es6-promise",
         "jquery.browser",
@@ -9,30 +16,15 @@
         "locales",
         "moment_with_locales",
         "strophe",
-        "tpl!field",
-        "tpl!select_option",
-        "tpl!form_select",
-        "tpl!form_textarea",
-        "tpl!form_checkbox",
-        "tpl!form_username",
-        "tpl!form_input",
-        "tpl!form_captcha"
     ], factory);
 }(this, function (
-        $, sizzle,
+        sizzle,
         Promise,
-        dummy, _,
+        jQBrowser,
+        _,
         locales,
         moment,
-        Strophe,
-        tpl_field,
-        tpl_select_option,
-        tpl_form_select,
-        tpl_form_textarea,
-        tpl_form_checkbox,
-        tpl_form_username,
-        tpl_form_input,
-        tpl_form_captcha
+        Strophe
     ) {
     "use strict";
     locales = locales || {};
@@ -41,16 +33,12 @@
 
     const URL_REGEX = /\b(https?:\/\/|www\.|https?:\/\/www\.)[^\s<>]{2,200}\b/g;
 
-    var XFORM_TYPE_MAP = {
-        'text-private': 'password',
-        'text-single': 'text',
-        'fixed': 'label',
-        'boolean': 'checkbox',
-        'hidden': 'hidden',
-        'jid-multi': 'textarea',
-        'list-single': 'dropdown',
-        'list-multi': 'dropdown'
-    };
+    const logger = _.assign({
+        'debug': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'error': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'info': _.get(console, 'log') ? console.log.bind(console) : _.noop,
+        'warn': _.get(console, 'log') ? console.log.bind(console) : _.noop
+    }, console);
 
     var afterAnimationEnd = function (el, callback) {
         el.classList.remove('visible');
@@ -90,16 +78,6 @@
         });
     };
 
-    $.fn.hasScrollBar = function() {
-        if (!$.contains(document, this.get(0))) {
-            return false;
-        }
-        if(this.parent().height() < this.get(0).scrollHeight) {
-            return true;
-        }
-        return false;
-    };
-
     function calculateSlideStep (height) {
         if (height > 100) {
             return 10;
@@ -110,13 +88,35 @@
         }
     }
 
-    var utils = {};
+    function calculateElementHeight (el) {
+        /* Return the height of the passed in DOM element,
+         * based on the heights of its children.
+         */
+        return _.reduce(
+            el.children,
+            (result, child) => result + child.offsetHeight, 0
+        );
+    }
+
+    function slideOutWrapup (el) {
+        /* Wrapup function for slideOut. */
+        el.removeAttribute('data-slider-marker');
+        el.classList.remove('collapsed');
+        el.style.overflow = "";
+        el.style.height = "";
+    }
+
+
+    var u = {};
 
     // Translation machinery
     // ---------------------
-    utils.__ = function (str) {
-        if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
-            return Jed.sprintf.apply(Jed, arguments);
+    u.__ = function (str) {
+        if (_.isUndefined(window.Jed)) {
+            return str;
+        }
+        if (!u.isConverseLocale(this.locale) || this.locale === 'en') {
+            return Jed.sprintf.apply(window.Jed, arguments);
         }
         if (typeof this.jed === "undefined") {
             this.jed = new Jed(window.JSON.parse(locales[this.locale]));
@@ -129,7 +129,7 @@
         }
     };
 
-    utils.___ = function (str) {
+    u.___ = function (str) {
         /* XXX: This is part of a hack to get gettext to scan strings to be
          * translated. Strings we cannot send to the function above because
          * they require variable interpolation and we don't yet have the
@@ -140,7 +140,7 @@
         return str;
     };
 
-    utils.isLocaleAvailable = function (locale, available) {
+    u.isLocaleAvailable = function (locale, available) {
         /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
          *
          * Parameters:
@@ -156,7 +156,7 @@
         }
     };
 
-    utils.addHyperlinks = function (text) {
+    u.addHyperlinks = function (text) {
         const list = text.match(URL_REGEX) || [];
         var links = [];
         _.each(list, (match) => {
@@ -176,7 +176,7 @@
         return text;
     };
 
-    utils.renderImageURLs = function (obj) {
+    u.renderImageURLs = function (obj) {
         const list = obj.textContent.match(URL_REGEX) || [];
         _.forEach(list, function (url) {
             isImage(url).then(function (img) {
@@ -188,43 +188,33 @@
         return obj;
     };
 
-    utils.slideInAllElements = function (elements) {
+    u.slideInAllElements = function (elements) {
         return Promise.all(
             _.map(
                 elements,
-                _.partial(utils.slideIn, _, 600)
+                _.partial(u.slideIn, _, 600)
             ));
     };
 
-    utils.slideToggleElement = function (el) {
+    u.slideToggleElement = function (el) {
         if (_.includes(el.classList, 'collapsed')) {
-            return utils.slideOut(el);
+            return u.slideOut(el);
         } else {
-            return utils.slideIn(el);
+            return u.slideIn(el);
         }
     };
 
-    utils.slideOut = function (el, duration=900) {
-        /* Shows/expands an element by sliding it out of itself. */
-
-        function calculateEndHeight (el) {
-            return _.reduce(
-                el.children,
-                (result, child) => result + child.offsetHeight, 0
-            );
-        }
-
-        function wrapup (el) {
-            el.removeAttribute('data-slider-marker');
-            el.classList.remove('collapsed');
-            el.style.overflow = "";
-            el.style.height = "";
-        }
-
+    u.slideOut = function (el, duration=900) {
+        /* Shows/expands an element by sliding it out of itself
+         *
+         * Parameters:
+         *      (HTMLElement) el - The HTML string
+         *      (Number) duration - The duration amount in milliseconds
+         */
         return new Promise((resolve, reject) => {
             if (_.isNil(el)) {
                 const err = "Undefined or null element passed into slideOut"
-                console.warn(err);
+                logger.warn(err);
                 reject(new Error(err));
                 return;
             }
@@ -233,10 +223,10 @@
                 el.removeAttribute('data-slider-marker');
                 window.clearInterval(interval_marker);
             }
-            const end_height = calculateEndHeight(el);
-            if ($.fx.off) { // Effects are disabled (for tests)
+            const end_height = calculateElementHeight(el);
+            if (window.converse_disable_effects) { // Effects are disabled (for tests)
                 el.style.height = end_height + 'px';
-                wrapup(el);
+                slideOutWrapup(el);
                 resolve();
                 return;
             }
@@ -253,9 +243,9 @@
                     // We recalculate the height to work around an apparent
                     // browser bug where browsers don't know the correct
                     // offsetHeight beforehand.
-                    el.style.height = calculateEndHeight(el) + 'px';
+                    el.style.height = calculateElementHeight(el) + 'px';
                     window.clearInterval(interval_marker);
-                    wrapup(el);
+                    slideOutWrapup(el);
                     resolve();
                 }
             }, interval);
@@ -263,16 +253,16 @@
         });
     };
 
-    utils.slideIn = function (el, duration=600) {
+    u.slideIn = function (el, duration=600) {
         /* Hides/collapses an element by sliding it into itself. */
         return new Promise((resolve, reject) => {
             if (_.isNil(el)) {
                 const err = "Undefined or null element passed into slideIn";
-                console.warn(err);
+                logger.warn(err);
                 return reject(new Error(err));
             } else if (_.includes(el.classList, 'collapsed')) {
                 return resolve();
-            } else if ($.fx.off) { // Effects are disabled (for tests)
+            } else if (window.converse_disable_effects) { // Effects are disabled (for tests)
                 el.classList.add('collapsed');
                 el.style.height = "";
                 return resolve();
@@ -304,11 +294,11 @@
         });
     };
 
-    utils.fadeIn = function (el, callback) {
+    u.fadeIn = function (el, callback) {
         if (_.isNil(el)) {
-            console.warn("Undefined or null element passed into fadeIn");
+            logger.warn("Undefined or null element passed into fadeIn");
         }
-        if ($.fx.off) {
+        if (window.converse_disable_effects) { // Effects are disabled (for tests)
             el.classList.remove('hidden');
             if (_.isFunction(callback)) {
                 callback();
@@ -328,12 +318,12 @@
         }
     };
 
-    utils.isSameBareJID = function (jid1, jid2) {
+    u.isSameBareJID = function (jid1, jid2) {
         return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
                 Strophe.getBareJidFromJid(jid2).toLowerCase();
     };
 
-    utils.isNewMessage = function (message) {
+    u.isNewMessage = function (message) {
         /* Given a stanza, determine whether it's a new
          * message, i.e. not a MAM archived one.
          */
@@ -344,13 +334,13 @@
         }
     };
 
-    utils.isOTRMessage = function (message) {
+    u.isOTRMessage = function (message) {
         var body = message.querySelector('body'),
             text = (!_.isNull(body) ? body.textContent: undefined);
         return text && !!text.match(/^\?OTR/);
     };
 
-    utils.isHeadlineMessage = function (message) {
+    u.isHeadlineMessage = function (message) {
         var from_jid = message.getAttribute('from');
         if (message.getAttribute('type') === 'headline') {
             return true;
@@ -367,7 +357,7 @@
         return false;
     };
 
-    utils.merge = function merge (first, second) {
+    u.merge = function merge (first, second) {
         /* Merge the second object into the first one.
          */
         for (var k in second) {
@@ -379,7 +369,7 @@
         }
     };
 
-    utils.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
+    u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
         /* Configuration settings might be nested objects. We only want to
          * add settings which are whitelisted.
          */
@@ -395,11 +385,11 @@
         }
     };
 
-    utils.refreshWebkit = function () {
+    u.refreshWebkit = function () {
         /* This works around a webkit bug. Refreshes the browser's viewport,
          * otherwise chatboxes are not moved along when one is closed.
          */
-        if ($.browser.webkit && window.requestAnimationFrame) {
+        if (jQBrowser.webkit && window.requestAnimationFrame) {
             window.requestAnimationFrame(function () {
                 var conversejs = document.getElementById('conversejs');
                 conversejs.style.display = 'none';
@@ -409,34 +399,47 @@
         }
     };
 
-    utils.webForm2xForm = function (field) {
-        /* Takes an HTML DOM and turns it into an XForm field.
-        *
-        * Parameters:
-        *      (DOMElement) field - the field to convert
-        */
-        var $input = $(field), value;
-        if ($input.is('[type=checkbox]')) {
-            value = $input.is(':checked') && 1 || 0;
-        } else if ($input.is('textarea')) {
-            value = [];
-            var lines = $input.val().split('\n');
-            for( var vk=0; vk<lines.length; vk++) {
-                var val = $.trim(lines[vk]);
-                if (val === '')
-                    continue;
-                value.push(val);
-            }
-        } else {
-            value = $input.val();
-        }
-        return $(tpl_field({
-            name: $input.attr('name'),
-            value: value
-        }))[0];
+    u.stringToDOM = function (s) {
+        /* Converts an HTML string into a DOM element.
+         *
+         * Parameters:
+         *      (String) s - The HTML string
+         */
+        var div = document.createElement('div');
+        div.innerHTML = s;
+        return div.childNodes;
     };
 
-    utils.contains = function (attr, query) {
+    u.matchesSelector = function (el, selector) {
+        /* Checks whether the DOM element matches the given selector.
+         *
+         * Parameters:
+         *      (DOMElement) el - The DOM element
+         *      (String) selector - The selector
+         */
+        return (
+            el.matches ||
+            el.matchesSelector ||
+            el.msMatchesSelector ||
+            el.mozMatchesSelector ||
+            el.webkitMatchesSelector ||
+            el.oMatchesSelector
+        ).call(el, selector);
+    };
+
+    u.queryChildren = function (el, selector) {
+        /* Returns a list of children of the DOM element that match the
+         * selector.
+         *
+         *  Parameters:
+         *      (DOMElement) el - the DOM element
+         *      (String) selector - the selector they should be matched
+         *          against.
+         */
+        return _.filter(el.children, _.partial(u.matchesSelector, _, selector));
+    };
+
+    u.contains = function (attr, query) {
         return function (item) {
             if (typeof attr === 'object') {
                 var value = false;
@@ -452,94 +455,8 @@
         };
     };
 
-    utils.xForm2webForm = function ($field, $stanza) {
-        /* Takes a field in XMPP XForm (XEP-004: Data Forms) format
-        * and turns it into a HTML DOM field.
-        *
-        *  Parameters:
-        *      (XMLElement) field - the field to convert
-        */
-
-        // FIXME: take <required> into consideration
-        var options = [], j, $options, $values, value, values;
-
-        if ($field.attr('type') === 'list-single' || $field.attr('type') === 'list-multi') {
-            values = [];
-            $values = $field.children('value');
-            for (j=0; j<$values.length; j++) {
-                values.push($($values[j]).text());
-            }
-            $options = $field.children('option');
-            for (j=0; j<$options.length; j++) {
-                value = $($options[j]).find('value').text();
-                options.push(tpl_select_option({
-                    value: value,
-                    label: $($options[j]).attr('label'),
-                    selected: _.startsWith(values, value),
-                    required: $field.find('required').length
-                }));
-            }
-            return tpl_form_select({
-                name: $field.attr('var'),
-                label: $field.attr('label'),
-                options: options.join(''),
-                multiple: ($field.attr('type') === 'list-multi'),
-                required: $field.find('required').length
-            });
-        } else if ($field.attr('type') === 'fixed') {
-            return $('<p class="form-help">').text($field.find('value').text());
-        } else if ($field.attr('type') === 'jid-multi') {
-            return tpl_form_textarea({
-                name: $field.attr('var'),
-                label: $field.attr('label') || '',
-                value: $field.find('value').text(),
-                required: $field.find('required').length
-            });
-        } else if ($field.attr('type') === 'boolean') {
-            return tpl_form_checkbox({
-                name: $field.attr('var'),
-                type: XFORM_TYPE_MAP[$field.attr('type')],
-                label: $field.attr('label') || '',
-                checked: $field.find('value').text() === "1" && 'checked="1"' || '',
-                required: $field.find('required').length
-            });
-        } else if ($field.attr('type') && $field.attr('var') === 'username') {
-            return tpl_form_username({
-                domain: ' @'+this.domain,
-                name: $field.attr('var'),
-                type: XFORM_TYPE_MAP[$field.attr('type')],
-                label: $field.attr('label') || '',
-                value: $field.find('value').text(),
-                required: $field.find('required').length
-            });
-        } else if ($field.attr('type')) {
-            return tpl_form_input({
-                name: $field.attr('var'),
-                type: XFORM_TYPE_MAP[$field.attr('type')],
-                label: $field.attr('label') || '',
-                value: $field.find('value').text(),
-                required: $field.find('required').length
-            });
-        } else {
-            if ($field.attr('var') === 'ocr') { // Captcha
-                return _.reduce(_.map($field.find('uri'),
-                        $.proxy(function (uri) {
-                            return tpl_form_captcha({
-                                label: this.$field.attr('label'),
-                                name: this.$field.attr('var'),
-                                data: this.$stanza.find('data[cid="'+uri.textContent.replace(/^cid:/, '')+'"]').text(),
-                                type: uri.getAttribute('type'),
-                                required: this.$field.find('required').length
-                            });
-                        }, {'$stanza': $stanza, '$field': $field})
-                    ),
-                    function (memo, num) { return memo + num; }, ''
-                );
-            }
-        }
-    }
 
-    utils.detectLocale = function (library_check) {
+    u.detectLocale = function (library_check) {
         /* Determine which locale is supported by the user's system as well
          * as by the relevant library (e.g. converse.js or moment.js).
          *
@@ -549,36 +466,36 @@
          */
         var locale, i;
         if (window.navigator.userLanguage) {
-            locale = utils.isLocaleAvailable(window.navigator.userLanguage, library_check);
+            locale = u.isLocaleAvailable(window.navigator.userLanguage, library_check);
         }
         if (window.navigator.languages && !locale) {
             for (i=0; i<window.navigator.languages.length && !locale; i++) {
-                locale = utils.isLocaleAvailable(window.navigator.languages[i], library_check);
+                locale = u.isLocaleAvailable(window.navigator.languages[i], library_check);
             }
         }
         if (window.navigator.browserLanguage && !locale) {
-            locale = utils.isLocaleAvailable(window.navigator.browserLanguage, library_check);
+            locale = u.isLocaleAvailable(window.navigator.browserLanguage, library_check);
         }
         if (window.navigator.language && !locale) {
-            locale = utils.isLocaleAvailable(window.navigator.language, library_check);
+            locale = u.isLocaleAvailable(window.navigator.language, library_check);
         }
         if (window.navigator.systemLanguage && !locale) {
-            locale = utils.isLocaleAvailable(window.navigator.systemLanguage, library_check);
+            locale = u.isLocaleAvailable(window.navigator.systemLanguage, library_check);
         }
         return locale || 'en';
     };
 
-    utils.isConverseLocale = function (locale) {
+    u.isConverseLocale = function (locale) {
         if (!_.isString(locale)) { return false; }
         return _.includes(_.keys(locales || {}), locale);
     };
 
-    utils.isMomentLocale  = function (locale) {
+    u.isMomentLocale  = function (locale) {
         if (!_.isString(locale)) { return false; }
         return moment.locale() !== moment.locale(locale);
     };
 
-    utils.getLocale = function (preferred_locale, isSupportedByLibrary) {
+    u.getLocale = function (preferred_locale, isSupportedByLibrary) {
         if (_.isString(preferred_locale)) {
             if (preferred_locale === 'en' || isSupportedByLibrary(preferred_locale)) {
                 return preferred_locale;
@@ -587,31 +504,31 @@
                 var obj = window.JSON.parse(preferred_locale);
                 return obj.locale_data.converse[""].lang;
             } catch (e) {
-                console.log(e);
+                logger.error(e);
             }
         }
-        return utils.detectLocale(isSupportedByLibrary) || 'en';
+        return u.detectLocale(isSupportedByLibrary) || 'en';
     };
 
-    utils.isOfType = function (type, item) {
+    u.isOfType = function (type, item) {
         return item.get('type') == type;
     };
 
-    utils.isInstance = function (type, item) {
+    u.isInstance = function (type, item) {
         return item instanceof type;
     };
 
-    utils.getAttribute = function (key, item) {
+    u.getAttribute = function (key, item) {
         return item.get(key);
     };
 
-    utils.contains.not = function (attr, query) {
+    u.contains.not = function (attr, query) {
         return function (item) {
-            return !(utils.contains(attr, query)(item));
+            return !(u.contains(attr, query)(item));
         };
     };
 
-    utils.createFragmentFromText = function (markup) {
+    u.createFragmentFromText = function (markup) {
         /* Returns a DocumentFragment containing DOM nodes based on the
          * passed-in markup text.
          */
@@ -627,7 +544,7 @@
         return frag
     };
 
-    utils.addEmoji = function (_converse, emojione, text) {
+    u.addEmoji = function (_converse, emojione, text) {
         if (_converse.use_emojione) {
             return emojione.toImage(text);
         } else {
@@ -635,7 +552,7 @@
         }
     }
 
-    utils.getEmojisByCategory = function (_converse, emojione) {
+    u.getEmojisByCategory = function (_converse, emojione) {
         /* Return a dict of emojis with the categories as keys and
          * lists of emojis in that category as values.
          */
@@ -681,11 +598,11 @@
         return _converse.emojis_by_category;
     };
 
-    utils.getTonedEmojis = function (_converse) {
+    u.getTonedEmojis = function (_converse) {
         _converse.toned_emojis = _.uniq(
             _.map(
                 _.filter(
-                    utils.getEmojisByCategory(_converse).people,
+                    u.getEmojisByCategory(_converse).people,
                     (person) => _.includes(person._shortname, '_tone')
                 ),
                 (person) => person._shortname.replace(/_tone[1-5]/, '')
@@ -693,11 +610,11 @@
         return _converse.toned_emojis;
     };
 
-    utils.isPersistableModel = function (model) {
+    u.isPersistableModel = function (model) {
         return model.collection && model.collection.browserStorage;
     };
 
-    utils.getWrappedPromise = function () {
+    u.getWrappedPromise = function () {
         const wrapper = {};
         wrapper.promise = new Promise((resolve, reject) => {
             wrapper.resolve = resolve;
@@ -706,12 +623,12 @@
         return wrapper;
     };
 
-    utils.safeSave = function (model, attributes) {
-        if (utils.isPersistableModel(model)) {
+    u.safeSave = function (model, attributes) {
+        if (u.isPersistableModel(model)) {
             model.save(attributes);
         } else {
             model.set(attributes);
         }
     }
-    return utils;
+    return u;
 }));

+ 7 - 7
tests/mock.js

@@ -1,10 +1,10 @@
 (function (root, factory) {
     define("mock", ['jquery.noconflict', 'converse'], factory);
-}(this, function ($, converse_api) {
-    var _ = converse_api.env._;
-    var Promise = converse_api.env.Promise;
-    var Strophe = converse_api.env.Strophe;
-    var $iq = converse_api.env.$iq;
+}(this, function ($, converse) {
+    var _ = converse.env._;
+    var Promise = converse.env.Promise;
+    var Strophe = converse.env.Strophe;
+    var $iq = converse.env.$iq;
     var mock = {};
     // Names from http://www.fakenamegenerator.com/
     mock.req_names = [
@@ -99,7 +99,7 @@
             });
         }
 
-        var _converse = converse_api.initialize(_.extend({
+        var _converse = converse.initialize(_.extend({
             'i18n': 'en',
             'auto_subscribe': false,
             'play_sounds': false,
@@ -113,7 +113,7 @@
             'debug': false
         }, settings || {}));
         _converse.ChatBoxViews.prototype.trimChat = function () {};
-        _converse.disable_effects = true;
+        window.converse_disable_effects = true;
         $.fx.off = true;
         return _converse;
     }

Vissa filer visades inte eftersom för många filer har ändrats