Browse Source

Refactor slide methods in utils and use them for toolbar menus

JC Brand 8 years ago
parent
commit
b2a118ca9e

+ 1 - 1
Makefile

@@ -148,7 +148,7 @@ css/mobile.min.css:: stamp-npm sass/*
 
 .PHONY: watch
 watch: stamp-bundler
-	$(SASS) --watch -I ./node_modules/bourbon/app/assets/stylesheets/ sass/converse/converse.scss:css/converse.css sass/_muc_embedded.scss:css/converse-muc-embedded.css
+	$(SASS) --watch -I ./node_modules/bourbon/app/assets/stylesheets/ sass/converse/converse.scss:css/converse.css sass/_muc_embedded.scss:css/converse-muc-embedded.css sass/inverse/inverse.scss:css/inverse.css
 
 .PHONY: watchjs
 watchjs: stamp-npm

+ 70 - 79
css/converse.css

@@ -1201,9 +1201,6 @@
     -moz-user-select: none;
     -ms-user-select: none;
     user-select: none; }
-  #converse-embedded-chat .emoticon,
-  #conversejs .emoticon {
-    font-size: 14px; }
 @keyframes fadein {
   0% {
     opacity: 0; }
@@ -1234,6 +1231,10 @@
   #conversejs .hidden {
     opacity: 0;
     display: none; }
+  #converse-embedded-chat .collapsed,
+  #conversejs .collapsed {
+    height: 0;
+    overflow: hidden; }
   #converse-embedded-chat .locked,
   #conversejs .locked {
     padding-right: 22px; }
@@ -1697,12 +1698,6 @@
         color: #818479;
         text-decoration: none;
         text-shadow: none; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .emoji-picker,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .emoji-picker {
-        margin-bottom: 30px; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toolbar-picker-panel a,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toolbar-picker-panel a {
-        color: #578EA9; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .chat-toolbar-text,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .chat-toolbar-text {
         font-size: 12px;
@@ -1712,10 +1707,10 @@
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted {
         color: #818479; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-picker-panel a,
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-picker-panel a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-picker-panel a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-picker-panel a {
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-menu a,
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-menu a,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-menu a,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-menu a {
           color: #578EA9; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unverified a,
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unverified,
@@ -1736,79 +1731,75 @@
         float: right; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li {
+        cursor: pointer;
         display: inline-block;
         list-style: none;
-        padding: 0 3px 0 3px;
-        cursor: pointer;
-        margin-top: 1px; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li:hover,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li:hover {
-        cursor: pointer; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul {
-        background: #fff;
-        bottom: 100%;
-        box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
-        font-size: 12px;
-        margin: 0;
-        position: absolute;
-        right: 0; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul li,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul li {
-          cursor: pointer;
-          list-style: none;
-          position: relative; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul li a:hover,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul li a:hover {
-            color: #8f2831; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li {
-        margin-left: 0; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley {
-        color: #818479;
-        padding-left: 5px; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul {
-          left: 0; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker {
+        margin-top: 1px;
+        padding: 0 3px 0 3px; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li:hover,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li:hover {
+          cursor: pointer; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu {
+          background-color: #fff;
+          bottom: 100%;
+          box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
+          font-size: 12px;
+          margin: 0;
+          position: absolute;
+          right: 0; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu a,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu a {
+            color: #578EA9; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker {
+            box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
             z-index: 100; }
-            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker .picked,
-            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker .picked {
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker .picked,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker .picked {
               background-color: #DCF9F6; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-picker,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-picker {
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-picker,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-picker {
             height: 250px;
             overflow: scroll; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li {
-            font-size: 14px;
-            padding: 5px;
-            z-index: 98; }
-            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li.emoji a,
-            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li.emoji a {
-              font-size: 20px; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li:hover,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li:hover {
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li {
+            margin-left: 0;
+            cursor: pointer;
+            list-style: none;
+            position: relative; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li a:hover,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li a:hover {
+              color: #8f2831; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu {
+          color: #818479; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu ul li:hover,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu ul li:hover {
             background-color: #DCF9F6; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li {
-        padding: 7px;
-        background-color: white;
-        display: block;
-        z-index: 99; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li a {
-          -moz-transition: background-color 0.2s ease-in-out;
-          -webkit-transition: background-color 0.2s ease-in-out;
-          transition: background-color 0.2s ease-in-out;
-          display: block;
-          padding: 1px;
-          text-decoration: none; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li:hover,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li:hover {
-        background-color: #DCF9F6; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley {
+          padding-left: 5px; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul {
+            left: 0; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li {
+              padding: 5px;
+              z-index: 98; }
+              #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li.emoji a,
+              #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li.emoji a {
+                font-size: 20px; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul {
+          z-index: 99; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li {
+            display: block;
+            padding: 7px; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li a,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li a {
+              display: block; }
   #converse-embedded-chat .chatbox .dragresize,
   #conversejs .chatbox .dragresize {
     background: transparent;

+ 70 - 79
css/inverse.css

@@ -1201,9 +1201,6 @@
     -moz-user-select: none;
     -ms-user-select: none;
     user-select: none; }
-  #converse-embedded-chat .emoticon,
-  #conversejs .emoticon {
-    font-size: 16px; }
 @keyframes fadein {
   0% {
     opacity: 0; }
@@ -1234,6 +1231,10 @@
   #conversejs .hidden {
     opacity: 0;
     display: none; }
+  #converse-embedded-chat .collapsed,
+  #conversejs .collapsed {
+    height: 0;
+    overflow: hidden; }
   #converse-embedded-chat .locked,
   #conversejs .locked {
     padding-right: 22px; }
@@ -1743,12 +1744,6 @@ body {
         color: #818479;
         text-decoration: none;
         text-shadow: none; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .emoji-picker,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .emoji-picker {
-        margin-bottom: 34px; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toolbar-picker-panel a,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toolbar-picker-panel a {
-        color: #578EA9; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .chat-toolbar-text,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .chat-toolbar-text {
         font-size: 12px;
@@ -1758,10 +1753,10 @@ body {
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted {
         color: #818479; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-picker-panel a,
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-picker-panel a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-picker-panel a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-picker-panel a {
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-menu a,
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-menu a,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted a .toolbar-menu a,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .unencrypted .toolbar-menu a {
           color: #578EA9; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unverified a,
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .unverified,
@@ -1782,79 +1777,75 @@ body {
         float: right; }
       #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li,
       #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li {
+        cursor: pointer;
         display: inline-block;
         list-style: none;
-        padding: 0 3px 0 3px;
-        cursor: pointer;
-        margin-top: 1px; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li:hover,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li:hover {
-        cursor: pointer; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul {
-        background: #fff;
-        bottom: 100%;
-        box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
-        font-size: 12px;
-        margin: 0;
-        position: absolute;
-        right: 0; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul li,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul li {
-          cursor: pointer;
-          list-style: none;
-          position: relative; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar ul li a:hover,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar ul li a:hover {
-            color: #8f2831; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li {
-        margin-left: 0; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley {
-        color: #818479;
-        padding-left: 5px; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul {
-          left: 0; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker {
+        margin-top: 1px;
+        padding: 0 3px 0 3px; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li:hover,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li:hover {
+          cursor: pointer; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu {
+          background-color: #fff;
+          bottom: 100%;
+          box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
+          font-size: 12px;
+          margin: 0;
+          position: absolute;
+          right: 0; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu a,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu a {
+            color: #578EA9; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker {
+            box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
             z-index: 100; }
-            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker .picked,
-            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-category-picker .picked {
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker .picked,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-category-picker .picked {
               background-color: #DCF9F6; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-picker,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul.emoji-picker {
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-picker,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul.emoji-picker {
             height: 250px;
             overflow: scroll; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li {
-            font-size: 16px;
-            padding: 5px;
-            z-index: 98; }
-            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li.emoji a,
-            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li.emoji a {
-              font-size: 26px; }
-          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li:hover,
-          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-smiley ul li:hover {
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li {
+            margin-left: 0;
+            cursor: pointer;
+            list-style: none;
+            position: relative; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li a:hover,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li .toolbar-menu ul li a:hover {
+              color: #8f2831; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu {
+          color: #818479; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu ul li:hover,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-toolbar-menu ul li:hover {
             background-color: #DCF9F6; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li {
-        padding: 7px;
-        background-color: white;
-        display: block;
-        z-index: 99; }
-        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li a,
-        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li a {
-          -moz-transition: background-color 0.2s ease-in-out;
-          -webkit-transition: background-color 0.2s ease-in-out;
-          transition: background-color 0.2s ease-in-out;
-          display: block;
-          padding: 1px;
-          text-decoration: none; }
-      #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li:hover,
-      #conversejs .chatbox form.sendXMPPMessage .chat-toolbar .toggle-otr ul li:hover {
-        background-color: #DCF9F6; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley {
+          padding-left: 5px; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul {
+            left: 0; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li {
+              padding: 5px;
+              z-index: 98; }
+              #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li.emoji a,
+              #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-smiley ul li.emoji a {
+                font-size: 26px; }
+        #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul,
+        #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul {
+          z-index: 99; }
+          #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li,
+          #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li {
+            display: block;
+            padding: 7px; }
+            #converse-embedded-chat .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li a,
+            #conversejs .chatbox form.sendXMPPMessage .chat-toolbar li.toggle-otr ul li a {
+              display: block; }
   #converse-embedded-chat .chatbox .dragresize,
   #conversejs .chatbox .dragresize {
     background: transparent;

+ 63 - 71
sass/_chatbox.scss

@@ -272,14 +272,6 @@
                     text-decoration: none;
                     text-shadow: none;
                 }
-                .emoji-picker {
-                    margin-bottom: $toolbar-height + 5px;
-                }
-                .toolbar-picker-panel {
-                    a {
-                        color: $link-color;
-                    }
-                }
                 .chat-toolbar-text {
                     font-size: 12px;
                     padding-right: 3px;
@@ -287,7 +279,7 @@
                 .unencrypted a,
                 .unencrypted {
                     color: $text-color;
-                    .toolbar-picker-panel {
+                    .toolbar-menu {
                         a {
                             color: $link-color;
                         }
@@ -307,84 +299,84 @@
                     float: right;
                 }
                 li {
+                    cursor: pointer;
                     display: inline-block;
                     list-style: none;
-                    padding: 0 3px 0 3px;
-                    cursor: pointer;
                     margin-top: 1px;
-                }
-                li:hover {
-                    cursor: pointer;
-                }
-                ul {
-                    background: #fff;
-                    bottom: 100%;
-                    box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
-                    font-size: 12px;
-                    margin: 0;
-                    position: absolute;
-                    right: 0;
-                    li {
+                    padding: 0 3px 0 3px;
+                    &:hover {
                         cursor: pointer;
-                        list-style: none;
-                        position: relative;
-                        a:hover {
-                            color: #8f2831;
-                        }
                     }
-                }
-                li {
-                    margin-left: 0;
-                }
-                .toggle-smiley {
-                    color: $text-color;
-                    padding-left: 5px;
-                    ul {
-                        &.emoji-category-picker {
-                            z-index: 100;
-                            .picked {
-                                background-color: $highlight-color;
-                            }
+                    .toolbar-menu {
+                        background-color: #fff;
+                        bottom: 100%;
+                        box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
+                        font-size: 12px;
+                        margin: 0;
+                        position: absolute;
+                        right: 0;
+                        a {
+                            color: $link-color;
                         }
-                        &.emoji-picker {
-                            height: 250px;
-                            overflow: scroll;
+                        ul {
+                            &.emoji-category-picker {
+                                box-shadow: -1px -1px 2px 0 rgba(0, 0, 0, 0.4);
+                                z-index: 100;
+                                .picked {
+                                    background-color: $highlight-color;
+                                }
+                            }
+                            &.emoji-picker {
+                                height: 250px;
+                                overflow: scroll;
+                            }
+                            li {
+                                margin-left: 0;
+                                cursor: pointer;
+                                list-style: none;
+                                position: relative;
+                                a:hover {
+                                    color: #8f2831;
+                                }
+                            }
                         }
-                        left: 0;
-                        li {
-                            font-size: $font-size;
-                            padding: 5px;
-                            z-index: 98;
-                            &.emoji {
-                                a {
-                                    font-size: $font-size-huge;
+                    }
+                    &.toggle-toolbar-menu {
+                        color: $text-color;
+                        ul {
+                            li {
+                                &:hover {
+                                    background-color: $highlight-color;
                                 }
                             }
                         }
-                        li:hover {
-                            background-color: $highlight-color;
+                    }
+                    &.toggle-smiley {
+                        padding-left: 5px;
+                        ul {
+                            left: 0;
+                            li {
+                                padding: 5px;
+                                z-index: 98;
+                                &.emoji {
+                                    a {
+                                        font-size: $font-size-huge;
+                                    }
+                                }
+                            }
                         }
                     }
-                }
-                .toggle-otr {
-                    ul {
-                        li {
-                            padding: 7px;
-                            background-color: white;
-                            display: block;
+                    &.toggle-otr {
+                        ul {
                             z-index: 99;
-                            a {
-                                -moz-transition: background-color 0.2s ease-in-out;
-                                -webkit-transition: background-color 0.2s ease-in-out;
-                                transition: background-color 0.2s ease-in-out;
+                            li {
                                 display: block;
-                                padding: 1px;
-                                text-decoration: none;
+                                padding: 7px;
+                                a {
+                                    display: block;
+                                }
                             }
                         }
-                        li:hover {
-                            background-color: $highlight-color;
-                        }
                     }
                 }
             }

+ 4 - 0
sass/_core.scss

@@ -74,6 +74,10 @@
         opacity: 0;
         display: none;
     }
+    .collapsed {
+        height: 0;
+        overflow: hidden;
+    }
 
     .locked {
         padding-right: 22px;

+ 17 - 9
src/converse-chatview.js

@@ -54,14 +54,13 @@
             //
             registerGlobalEventHandlers: function () {
                 this.__super__.registerGlobalEventHandlers();
-                document.addEventListener('click', function () {
-                    if ($('.toggle-smiley ul').is(':visible')) {
-                        _.each(
-                            document.querySelectorAll('.toggle-smiley .emoji-picker-container'),
-                            utils.hideElement
-                        );
+                document.addEventListener(
+                    'click', function () {
+                        utils.slideInAllElements(
+                            document.querySelectorAll('.toolbar-menu')
+                        )
                     }
-                });
+                );
             },
 
             ChatBoxViews: {
@@ -114,7 +113,7 @@
             });
 
             _converse.EmojiPickerView = Backbone.View.extend({
-                className: 'emoji-picker-container hidden',
+                className: 'emoji-picker-container toolbar-menu collapsed',
                 events: {
                     'click .emoji-category-picker li a': 'chooseCategory',
                 },
@@ -710,7 +709,16 @@
                             return;
                         }
                     }
-                    utils.toggleElement(this.emoji_picker_view.el);
+                    const elements = _.difference(
+                        document.querySelectorAll('.toolbar-menu'),
+                        [this.emoji_picker_view.el]
+                    );
+                    utils.slideInAllElements(elements).then(
+                        _.partial(
+                            utils.slideToggleElement,
+                            this.emoji_picker_view.el
+                        )
+                    );
                 },
 
                 toggleCall (ev) {

+ 12 - 11
src/converse-otr.js

@@ -42,6 +42,7 @@
     OTR_CLASS_MAPPING[VERIFIED] = 'verified';
     OTR_CLASS_MAPPING[FINISHED] = 'finished';
 
+
     converse.plugins.add('converse-otr', {
 
         overrides: {
@@ -51,15 +52,6 @@
             //
             // New functions which don't exist yet can also be added.
  
-            registerGlobalEventHandlers () {
-                this.__super__.registerGlobalEventHandlers();
-                document.addEventListener('click', function () {
-                    if ($('.toggle-otr ul').is(':visible')) {
-                        _.each($('.toggle-otr ul', this), utils.hideElement);
-                    }
-                });
-            },
-
             ChatBox: {
                 initialize () {
                     this.__super__.initialize.apply(this, arguments);
@@ -347,7 +339,6 @@
                 },
 
                 startOTRFromToolbar (ev) {
-                    $(ev.target).parent().parent().slideUp();
                     ev.stopPropagation();
                     this.model.initiateOTR();
                 },
@@ -392,7 +383,17 @@
 
                 toggleOTRMenu (ev) {
                     ev.stopPropagation();
-                    utils.toggleElement(this.el.querySelector('.toggle-otr ul'));
+                    const menu = this.el.querySelector('.toggle-otr ul');
+                    const elements = _.difference(
+                        document.querySelectorAll('.toolbar-menu'),
+                        [menu]
+                    );
+                    utils.slideInAllElements(elements).then(
+                        _.partial(
+                            utils.slideToggleElement,
+                            menu
+                        )
+                    );
                 },
                 
                 getOTRTooltip () {

+ 3 - 3
src/converse-roomslist.js

@@ -70,7 +70,7 @@
                     });
                     this.hide();
                     if (this.list_model.get('toggle-state') !== _converse.OPENED) {
-                        this.el.querySelector('.open-rooms-list').classList.add('hidden');
+                        this.el.querySelector('.open-rooms-list').classList.add('collapsed');
                     }
                     this.model.each(this.renderRoomsListElement.bind(this));
                     const controlboxview = _converse.chatboxviews.get('controlbox');
@@ -142,13 +142,13 @@
                     if (ev && ev.preventDefault) { ev.preventDefault(); }
                     const el = ev.target;
                     if (el.classList.contains("icon-opened")) {
-                        utils.slideUp(this.el.querySelector('.open-rooms-list')).then(() => {
+                        utils.slideIn(this.el.querySelector('.open-rooms-list')).then(() => {
                             this.list_model.save({'toggle-state': _converse.CLOSED});
                             el.classList.remove("icon-opened");
                             el.classList.add("icon-closed");
                         });
                     } else {
-                        utils.slideDown(this.el.querySelector('.open-rooms-list')).then(() => {
+                        utils.slideOut(this.el.querySelector('.open-rooms-list')).then(() => {
                             this.list_model.save({'toggle-state': _converse.OPENED});
                             el.classList.remove("icon-closed");
                             el.classList.add("icon-opened");

+ 7 - 7
src/templates/emojis.html

@@ -1,10 +1,3 @@
-<ul class="emoji-category-picker">
-    {[ _.forEach(emojis_by_category, function (obj, category) { ]}
-        <li data-category="{{{category}}}" class="emoji-category {[ if (current_category === category) { ]} picked {[ } ]}">
-            <a href="#" data-category="{{{category}}}"> {{ emojione.shortnameToUnicode(emojis_by_category[category][0]._shortname) }} </a>
-        </li>
-    {[ }); ]}
-</ul>
 {[ _.forEach(emojis_by_category, function (obj, category) { ]}
     <ul class="emoji-picker emoji-picker-{{{category}}} {[ if (current_category !== category) { ]} hidden {[ } ]}">
         {[ _.forEach(emojis_by_category[category], function (emoji) { ]}
@@ -14,3 +7,10 @@
         {[ }); ]}
     </ul>
 {[ }); ]}
+<ul class="emoji-category-picker">
+    {[ _.forEach(emojis_by_category, function (obj, category) { ]}
+        <li data-category="{{{category}}}" class="emoji-category {[ if (current_category === category) { ]} picked {[ } ]}">
+            <a href="#" data-category="{{{category}}}"> {{ emojione.shortnameToUnicode(emojis_by_category[category][0]._shortname) }} </a>
+        </li>
+    {[ }); ]}
+</ul>

+ 1 - 1
src/templates/toolbar.html

@@ -1,5 +1,5 @@
 {[ if (use_emoji)  { ]}
-    <li class="toggle-smiley icon-happy" title="{{{label_insert_smiley}}}">
+    <li class="toggle-toolbar-menu toggle-smiley icon-happy" title="{{{label_insert_smiley}}}">
         <ul class="emoji-picker"></ul>
     </li>
 {[ } ]}

+ 2 - 2
src/templates/toolbar_otr.html

@@ -1,5 +1,5 @@
 {[ if (allow_otr)  { ]}
-    <li class="toggle-otr {{{otr_status_class}}}" title="{{{otr_tooltip}}}">
+    <li class="toggle-toolbar-menu toggle-otr {{{otr_status_class}}}" title="{{{otr_tooltip}}}">
         <span class="chat-toolbar-text">{{{otr_translated_status}}}</span>
         {[ if (otr_status == UNENCRYPTED) { ]}
             <span class="icon-unlocked"></span>
@@ -13,7 +13,7 @@
         {[ if (otr_status == FINISHED) { ]}
             <span class="icon-unlocked"></span>
         {[ } ]}
-        <ul class="toolbar-picker-panel">
+        <ul class="toolbar-menu collapsed">
             {[ if (otr_status == UNENCRYPTED) { ]}
                <li><a class="start-otr" href="#">{{{label_start_encrypted_conversation}}}</a></li>
             {[ } ]}

+ 353 - 324
src/utils.js

@@ -129,356 +129,385 @@
         return this;
     };
 
-    var utils = {
-        // Translation machinery
-        // ---------------------
-        __: function (str) {
-            if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
-                return Jed.sprintf.apply(Jed, arguments);
-            }
-            if (typeof this.jed === "undefined") {
-                this.jed = new Jed(window.JSON.parse(locales[this.locale]));
-            }
-            var t = this.jed.translate(str);
-            if (arguments.length>1) {
-                return t.fetch.apply(t, [].slice.call(arguments,1));
-            } else {
-                return t.fetch();
-            }
-        },
-
-        ___: 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
-             * variables at scan time.
-             *
-             * See actionInfoMessages in src/converse-muc.js
-             */
-            return str;
-        },
-
-        isLocaleAvailable: function (locale, available) {
-            /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
-             *
-             * Parameters:
-             *      (Function) available - returns a boolean indicating whether the locale is supported
-             */
-            if (available(locale)) {
-                return locale;
-            } else {
-                var sublocale = locale.split("-")[0];
-                if (sublocale !== locale && available(sublocale)) {
-                    return sublocale;
-                }
+    function calculateSlideStep (height) {
+        if (height > 100) {
+            return 10;
+        } else if (height > 50) {
+            return 5;
+        } else {
+            return 1;
+        }
+    }
+
+    var utils = {};
+
+    // Translation machinery
+    // ---------------------
+    utils.__ = function (str) {
+        if (!utils.isConverseLocale(this.locale) || this.locale === 'en') {
+            return Jed.sprintf.apply(Jed, arguments);
+        }
+        if (typeof this.jed === "undefined") {
+            this.jed = new Jed(window.JSON.parse(locales[this.locale]));
+        }
+        var t = this.jed.translate(str);
+        if (arguments.length>1) {
+            return t.fetch.apply(t, [].slice.call(arguments,1));
+        } else {
+            return t.fetch();
+        }
+    };
+
+    utils.___ = 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
+         * variables at scan time.
+         *
+         * See actionInfoMessages in src/converse-muc.js
+         */
+        return str;
+    };
+
+    utils.isLocaleAvailable = function (locale, available) {
+        /* Check whether the locale or sub locale (e.g. en-US, en) is supported.
+         *
+         * Parameters:
+         *      (Function) available - returns a boolean indicating whether the locale is supported
+         */
+        if (available(locale)) {
+            return locale;
+        } else {
+            var sublocale = locale.split("-")[0];
+            if (sublocale !== locale && available(sublocale)) {
+                return sublocale;
             }
-        },
+        }
+    };
 
-        hideElement: function (el) {
-            el.classList.add('hidden');
-        },
+    utils.slideInAllElements = function (elements) {
+        return Promise.all(
+            _.map(
+                elements,
+                _.partial(utils.slideIn, _, 600)
+            ));
+    };
 
-        toggleElement: function (el) {
-            if (_.includes(el.classList, 'hidden')) {
-                // XXX: use fadeIn?
-                el.classList.remove('hidden');
-            } else {
-                this.hideElement (el);
+    utils.slideToggleElement = function (el) {
+        if (!el.offsetHeight) {
+            return utils.slideOut(el);
+        } else {
+            return utils.slideIn(el);
+        }
+    };
+
+    utils.slideOut = function (el, duration=600) {
+        /* Shows/expands an element by sliding it out of itself. */
+        return new Promise((resolve, reject) => {
+            if (_.isNil(el)) {
+                const err = "Undefined or null element passed into slideOut"
+                console.warn(err);
+                reject(new Error(err));
             }
-        },
-
-        slideDown: function (el, interval=0.6) {
-            return new Promise((resolve, reject) => {
-                if (_.isNil(el)) {
-                    const err = "Undefined or null element passed into slideDown"
-                    console.warn(err);
-                    reject(new Error(err));
-                }
-                let intval = el.getAttribute('data-slider-intval');
-                if (intval) {
-                    window.clearInterval(intval);
-                }
-                let h = 0;
-                const end_height = el.getAttribute('data-slider-height');
-                intval = window.setInterval(function () {
-                    h++;
+            let interval_marker = el.getAttribute('data-slider-marker');
+            if (interval_marker) {
+                window.clearInterval(interval_marker);
+            }
+            const end_height = _.reduce(el.children, function (result, child) {
+                return result + child.offsetHeight;
+            }, 0);
+            const step = calculateSlideStep(end_height),
+                  interval = end_height/duration*step;
+
+            let h = 0;
+            interval_marker = window.setInterval(function () {
+                h += step;
+                if (h < end_height) {
                     el.style.height = h + 'px';
-                    if (h >= end_height) {
-                        window.clearInterval(intval);
-                        el.style.height = '';
-                        el.style.overflow = '';
-                        el.removeAttribute('data-slider-intval');
-                        el.removeAttribute('data-slider-height');
-                        resolve();
-                    }
-                }, interval);
-                el.setAttribute('data-slider-intval', intval);
-            });
-        },
-
-        slideUp: function (el, interval=0.6) {
-            return new Promise((resolve, reject) => {
-                if (_.isNil(el)) {
-                    const err = "Undefined or null element passed into slideUp";
-                    console.warn(err);
-                    reject(new Error(err));
-                }
-                let intval = el.getAttribute('data-slider-intval');
-                if (intval) {
-                    window.clearInterval(intval);
+                } else {
+                    el.style.height = end_height + 'px';
+                    window.clearInterval(interval_marker);
+                    el.style.overflow = '';
+                    el.removeAttribute('data-slider-marker');
+                    resolve();
                 }
-                let h = el.offsetHeight;
-                el.setAttribute('data-slider-height', h);
-                el.style.overflow = 'hidden';
-                intval = window.setInterval(function () {
-                    el.style.height = h + 'px';
-                    h--;
-                    if (h < 0) {
-                        window.clearInterval(intval);
-                        el.removeAttribute('data-slider-intval');
-                        resolve();
-                    }
-                }, interval);
-                el.setAttribute('data-slider-intval', intval);
-            });
-        },
+            }, interval);
+            el.setAttribute('data-slider-marker', interval_marker);
+        });
+    };
 
-        fadeIn: function (el, callback) {
+    utils.slideIn = function (el, duration=600) {
+        /* Hides/collapses an element by sliding it into itself. */
+        return new Promise((resolve, reject) => {
             if (_.isNil(el)) {
-                console.warn("Undefined or null element passed into fadeIn");
+                const err = "Undefined or null element passed into slideIn";
+                console.warn(err);
+                reject(new Error(err));
             }
-            if ($.fx.off) {
-                el.classList.remove('hidden');
-                if (_.isFunction(callback)) {
-                    callback();
-                }
+            if (!el.offsetHeight) {
+                resolve();
                 return;
             }
-            if (_.includes(el.classList, 'hidden')) {
-                /* XXX: This doesn't appear to be working...
-                    el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
-                    el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
-                */
-                setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
-                el.classList.add('visible');
-                el.classList.remove('hidden');
-            } else {
-                afterAnimationEnd(el, callback);
-            }
-        },
-
-        isSameBareJID: function (jid1, jid2) {
-            return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
-                   Strophe.getBareJidFromJid(jid2).toLowerCase();
-        },
-
-        isNewMessage: function (message) {
-            /* Given a stanza, determine whether it's a new
-             * message, i.e. not a MAM archived one.
-             */
-            if (message instanceof Element) {
-                return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
-            } else {
-                return !message.get('archive_id');
-            }
-        },
-
-        isOTRMessage: function (message) {
-            var body = message.querySelector('body'),
-                text = (!_.isNull(body) ? body.textContent: undefined);
-            return text && !!text.match(/^\?OTR/);
-        },
-
-        isHeadlineMessage: function (message) {
-            var from_jid = message.getAttribute('from');
-            if (message.getAttribute('type') === 'headline') {
-                return true;
-            }
-            if (message.getAttribute('type') !== 'error' &&
-                    !_.isNil(from_jid) &&
-                    !_.includes(from_jid, '@')) {
-                // Some servers (I'm looking at you Prosody) don't set the message
-                // type to "headline" when sending server messages. For now we
-                // check if an @ signal is included, and if not, we assume it's
-                // a headline message.
-                return true;
+            let interval_marker = el.getAttribute('data-slider-marker');
+            if (interval_marker) {
+                window.clearInterval(interval_marker);
             }
-            return false;
-        },
-
-        merge: function merge (first, second) {
-            /* Merge the second object into the first one.
-             */
-            for (var k in second) {
-                if (_.isObject(first[k])) {
-                    merge(first[k], second[k]);
-                } else {
-                    first[k] = second[k];
-                }
-            }
-        },
-
-        applyUserSettings: function applyUserSettings (context, settings, user_settings) {
-            /* Configuration settings might be nested objects. We only want to
-             * add settings which are whitelisted.
-             */
-            for (var k in settings) {
-                if (_.isUndefined(user_settings[k])) {
-                    continue;
-                }
-                if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
-                    applyUserSettings(context[k], settings[k], user_settings[k]);
+            let h = el.offsetHeight;
+            const step = calculateSlideStep(h),
+                  interval = h/duration*step;
+
+            el.style.overflow = 'hidden';
+
+            interval_marker = window.setInterval(function () {
+                h -= step;
+                if (h > 0) {
+                    el.style.height = h + 'px';
                 } else {
-                    context[k] = user_settings[k];
+                    el.style.height = 0 + 'px';
+                    window.clearInterval(interval_marker);
+                    el.removeAttribute('data-slider-marker');
+                    resolve();
                 }
+            }, interval);
+            el.setAttribute('data-slider-marker', interval_marker);
+        });
+    };
+
+    utils.fadeIn = function (el, callback) {
+        if (_.isNil(el)) {
+            console.warn("Undefined or null element passed into fadeIn");
+        }
+        if ($.fx.off) {
+            el.classList.remove('hidden');
+            if (_.isFunction(callback)) {
+                callback();
             }
-        },
-
-        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) {
-                window.requestAnimationFrame(function () {
-                    var conversejs = document.getElementById('conversejs');
-                    conversejs.style.display = 'none';
-                    var tmp = conversejs.offsetHeight; // jshint ignore:line
-                    conversejs.style.display = 'block';
-                });
+            return;
+        }
+        if (_.includes(el.classList, 'hidden')) {
+            /* XXX: This doesn't appear to be working...
+                el.addEventListener("webkitAnimationEnd", _.partial(afterAnimationEnd, el, callback), false);
+                el.addEventListener("animationend", _.partial(afterAnimationEnd, el, callback), false);
+            */
+            setTimeout(_.partial(afterAnimationEnd, el, callback), 351);
+            el.classList.add('visible');
+            el.classList.remove('hidden');
+        } else {
+            afterAnimationEnd(el, callback);
+        }
+    };
+
+    utils.isSameBareJID = function (jid1, jid2) {
+        return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
+                Strophe.getBareJidFromJid(jid2).toLowerCase();
+    };
+
+    utils.isNewMessage = function (message) {
+        /* Given a stanza, determine whether it's a new
+         * message, i.e. not a MAM archived one.
+         */
+        if (message instanceof Element) {
+            return !(sizzle('result[xmlns="'+Strophe.NS.MAM+'"]', message).length);
+        } else {
+            return !message.get('archive_id');
+        }
+    };
+
+    utils.isOTRMessage = function (message) {
+        var body = message.querySelector('body'),
+            text = (!_.isNull(body) ? body.textContent: undefined);
+        return text && !!text.match(/^\?OTR/);
+    };
+
+    utils.isHeadlineMessage = function (message) {
+        var from_jid = message.getAttribute('from');
+        if (message.getAttribute('type') === 'headline') {
+            return true;
+        }
+        if (message.getAttribute('type') !== 'error' &&
+                !_.isNil(from_jid) &&
+                !_.includes(from_jid, '@')) {
+            // Some servers (I'm looking at you Prosody) don't set the message
+            // type to "headline" when sending server messages. For now we
+            // check if an @ signal is included, and if not, we assume it's
+            // a headline message.
+            return true;
+        }
+        return false;
+    };
+
+    utils.merge = function merge (first, second) {
+        /* Merge the second object into the first one.
+         */
+        for (var k in second) {
+            if (_.isObject(first[k])) {
+                merge(first[k], second[k]);
+            } else {
+                first[k] = second[k];
             }
-        },
+        }
+    };
 
-        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);
-                }
+    utils.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
+        /* Configuration settings might be nested objects. We only want to
+         * add settings which are whitelisted.
+         */
+        for (var k in settings) {
+            if (_.isUndefined(user_settings[k])) {
+                continue;
+            }
+            if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
+                applyUserSettings(context[k], settings[k], user_settings[k]);
             } else {
-                value = $input.val();
+                context[k] = user_settings[k];
             }
-            return $(tpl_field({
-                name: $input.attr('name'),
-                value: value
-            }))[0];
-        },
-
-        contains: function (attr, query) {
-            return function (item) {
-                if (typeof attr === 'object') {
-                    var value = false;
-                    _.forEach(attr, function (a) {
-                        value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
-                    });
-                    return value;
-                } else if (typeof attr === 'string') {
-                    return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
-                } else {
-                    throw new TypeError('contains: wrong attribute type. Must be string or array.');
-                }
-            };
-        },
-
-        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
-            */
+        }
+    };
+
+    utils.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) {
+            window.requestAnimationFrame(function () {
+                var conversejs = document.getElementById('conversejs');
+                conversejs.style.display = 'none';
+                var tmp = conversejs.offsetHeight; // jshint ignore:line
+                conversejs.style.display = 'block';
+            });
+        }
+    };
 
-            // FIXME: take <required> into consideration
-            var options = [], j, $options, $values, value, values;
+    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];
+    };
 
-            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
+    utils.contains = function (attr, query) {
+        return function (item) {
+            if (typeof attr === 'object') {
+                var value = false;
+                _.forEach(attr, function (a) {
+                    value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
                 });
+                return value;
+            } else if (typeof attr === 'string') {
+                return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
             } 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; }, ''
-                    );
-                }
+                throw new TypeError('contains: wrong attribute type. Must be string or array.');
             }
-        }
+        };
     };
 
+    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) {
         /* Determine which locale is supported by the user's system as well
          * as by the relevant library (e.g. converse.js or moment.js).