Bläddra i källkod

Add devices section to own profile modal

Allow for devices to be removed.
JC Brand 6 år sedan
förälder
incheckning
26e936583f

+ 343 - 13
css/converse.css

@@ -4371,6 +4371,321 @@
     background-color: #e9ecef;
     border-left: 1px solid #ced4da;
     border-radius: 0 0.25rem 0.25rem 0; }
+#conversejs .nav {
+  display: flex;
+  flex-wrap: wrap;
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none; }
+#conversejs .nav-link {
+  display: block;
+  padding: 0.5rem 1rem; }
+  #conversejs .nav-link:hover, #conversejs .nav-link:focus {
+    text-decoration: none; }
+  #conversejs .nav-link.disabled {
+    color: #6c757d; }
+#conversejs .nav-tabs {
+  border-bottom: 1px solid #dee2e6; }
+  #conversejs .nav-tabs .nav-item {
+    margin-bottom: -1px; }
+  #conversejs .nav-tabs .nav-link {
+    border: 1px solid transparent;
+    border-top-left-radius: 0.25rem;
+    border-top-right-radius: 0.25rem; }
+    #conversejs .nav-tabs .nav-link:hover, #conversejs .nav-tabs .nav-link:focus {
+      border-color: #e9ecef #e9ecef #dee2e6; }
+    #conversejs .nav-tabs .nav-link.disabled {
+      color: #6c757d;
+      background-color: transparent;
+      border-color: transparent; }
+  #conversejs .nav-tabs .nav-link.active,
+  #conversejs .nav-tabs .nav-item.show .nav-link {
+    color: #495057;
+    background-color: #fff;
+    border-color: #dee2e6 #dee2e6 #fff; }
+  #conversejs .nav-tabs .dropdown-menu {
+    margin-top: -1px;
+    border-top-left-radius: 0;
+    border-top-right-radius: 0; }
+#conversejs .nav-pills .nav-link {
+  border-radius: 0.25rem; }
+#conversejs .nav-pills .nav-link.active,
+#conversejs .nav-pills .show > .nav-link {
+  color: #fff;
+  background-color: #387592; }
+#conversejs .nav-fill .nav-item {
+  flex: 1 1 auto;
+  text-align: center; }
+#conversejs .nav-justified .nav-item {
+  flex-basis: 0;
+  flex-grow: 1;
+  text-align: center; }
+#conversejs .tab-content > .tab-pane {
+  display: none; }
+#conversejs .tab-content > .active {
+  display: block; }
+#conversejs .navbar {
+  position: relative;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0.5rem 1rem; }
+  #conversejs .navbar > .container,
+  #conversejs .navbar > .container-fluid {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    justify-content: space-between; }
+#conversejs .navbar-brand {
+  display: inline-block;
+  padding-top: 0.3125rem;
+  padding-bottom: 0.3125rem;
+  margin-right: 1rem;
+  font-size: 1.25rem;
+  line-height: inherit;
+  white-space: nowrap; }
+  #conversejs .navbar-brand:hover, #conversejs .navbar-brand:focus {
+    text-decoration: none; }
+#conversejs .navbar-nav {
+  display: flex;
+  flex-direction: column;
+  padding-left: 0;
+  margin-bottom: 0;
+  list-style: none; }
+  #conversejs .navbar-nav .nav-link {
+    padding-right: 0;
+    padding-left: 0; }
+  #conversejs .navbar-nav .dropdown-menu {
+    position: static;
+    float: none; }
+#conversejs .navbar-text {
+  display: inline-block;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem; }
+#conversejs .navbar-collapse {
+  flex-basis: 100%;
+  flex-grow: 1;
+  align-items: center; }
+#conversejs .navbar-toggler {
+  padding: 0.25rem 0.75rem;
+  font-size: 1.25rem;
+  line-height: 1;
+  background-color: transparent;
+  border: 1px solid transparent;
+  border-radius: 0.25rem; }
+  #conversejs .navbar-toggler:hover, #conversejs .navbar-toggler:focus {
+    text-decoration: none; }
+  #conversejs .navbar-toggler:not(:disabled):not(.disabled) {
+    cursor: pointer; }
+#conversejs .navbar-toggler-icon {
+  display: inline-block;
+  width: 1.5em;
+  height: 1.5em;
+  vertical-align: middle;
+  content: "";
+  background: no-repeat center center;
+  background-size: 100% 100%; }
+@media (max-width: 575.98px) {
+  #conversejs .navbar-expand-sm > .container,
+  #conversejs .navbar-expand-sm > .container-fluid {
+    padding-right: 0;
+    padding-left: 0; } }
+@media (min-width: 576px) {
+  #conversejs .navbar-expand-sm {
+    flex-flow: row nowrap;
+    justify-content: flex-start; }
+    #conversejs .navbar-expand-sm .navbar-nav {
+      flex-direction: row; }
+      #conversejs .navbar-expand-sm .navbar-nav .dropdown-menu {
+        position: absolute; }
+      #conversejs .navbar-expand-sm .navbar-nav .dropdown-menu-right {
+        right: 0;
+        left: auto; }
+      #conversejs .navbar-expand-sm .navbar-nav .nav-link {
+        padding-right: 0.5rem;
+        padding-left: 0.5rem; }
+    #conversejs .navbar-expand-sm > .container,
+    #conversejs .navbar-expand-sm > .container-fluid {
+      flex-wrap: nowrap; }
+    #conversejs .navbar-expand-sm .navbar-collapse {
+      display: flex !important;
+      flex-basis: auto; }
+    #conversejs .navbar-expand-sm .navbar-toggler {
+      display: none; }
+    #conversejs .navbar-expand-sm .dropup .dropdown-menu {
+      top: auto;
+      bottom: 100%; } }
+@media (max-width: 767.98px) {
+  #conversejs .navbar-expand-md > .container,
+  #conversejs .navbar-expand-md > .container-fluid {
+    padding-right: 0;
+    padding-left: 0; } }
+@media (min-width: 768px) {
+  #conversejs .navbar-expand-md {
+    flex-flow: row nowrap;
+    justify-content: flex-start; }
+    #conversejs .navbar-expand-md .navbar-nav {
+      flex-direction: row; }
+      #conversejs .navbar-expand-md .navbar-nav .dropdown-menu {
+        position: absolute; }
+      #conversejs .navbar-expand-md .navbar-nav .dropdown-menu-right {
+        right: 0;
+        left: auto; }
+      #conversejs .navbar-expand-md .navbar-nav .nav-link {
+        padding-right: 0.5rem;
+        padding-left: 0.5rem; }
+    #conversejs .navbar-expand-md > .container,
+    #conversejs .navbar-expand-md > .container-fluid {
+      flex-wrap: nowrap; }
+    #conversejs .navbar-expand-md .navbar-collapse {
+      display: flex !important;
+      flex-basis: auto; }
+    #conversejs .navbar-expand-md .navbar-toggler {
+      display: none; }
+    #conversejs .navbar-expand-md .dropup .dropdown-menu {
+      top: auto;
+      bottom: 100%; } }
+@media (max-width: 991.98px) {
+  #conversejs .navbar-expand-lg > .container,
+  #conversejs .navbar-expand-lg > .container-fluid {
+    padding-right: 0;
+    padding-left: 0; } }
+@media (min-width: 992px) {
+  #conversejs .navbar-expand-lg {
+    flex-flow: row nowrap;
+    justify-content: flex-start; }
+    #conversejs .navbar-expand-lg .navbar-nav {
+      flex-direction: row; }
+      #conversejs .navbar-expand-lg .navbar-nav .dropdown-menu {
+        position: absolute; }
+      #conversejs .navbar-expand-lg .navbar-nav .dropdown-menu-right {
+        right: 0;
+        left: auto; }
+      #conversejs .navbar-expand-lg .navbar-nav .nav-link {
+        padding-right: 0.5rem;
+        padding-left: 0.5rem; }
+    #conversejs .navbar-expand-lg > .container,
+    #conversejs .navbar-expand-lg > .container-fluid {
+      flex-wrap: nowrap; }
+    #conversejs .navbar-expand-lg .navbar-collapse {
+      display: flex !important;
+      flex-basis: auto; }
+    #conversejs .navbar-expand-lg .navbar-toggler {
+      display: none; }
+    #conversejs .navbar-expand-lg .dropup .dropdown-menu {
+      top: auto;
+      bottom: 100%; } }
+@media (max-width: 1199.98px) {
+  #conversejs .navbar-expand-xl > .container,
+  #conversejs .navbar-expand-xl > .container-fluid {
+    padding-right: 0;
+    padding-left: 0; } }
+@media (min-width: 1200px) {
+  #conversejs .navbar-expand-xl {
+    flex-flow: row nowrap;
+    justify-content: flex-start; }
+    #conversejs .navbar-expand-xl .navbar-nav {
+      flex-direction: row; }
+      #conversejs .navbar-expand-xl .navbar-nav .dropdown-menu {
+        position: absolute; }
+      #conversejs .navbar-expand-xl .navbar-nav .dropdown-menu-right {
+        right: 0;
+        left: auto; }
+      #conversejs .navbar-expand-xl .navbar-nav .nav-link {
+        padding-right: 0.5rem;
+        padding-left: 0.5rem; }
+    #conversejs .navbar-expand-xl > .container,
+    #conversejs .navbar-expand-xl > .container-fluid {
+      flex-wrap: nowrap; }
+    #conversejs .navbar-expand-xl .navbar-collapse {
+      display: flex !important;
+      flex-basis: auto; }
+    #conversejs .navbar-expand-xl .navbar-toggler {
+      display: none; }
+    #conversejs .navbar-expand-xl .dropup .dropdown-menu {
+      top: auto;
+      bottom: 100%; } }
+#conversejs .navbar-expand {
+  flex-flow: row nowrap;
+  justify-content: flex-start; }
+  #conversejs .navbar-expand > .container,
+  #conversejs .navbar-expand > .container-fluid {
+    padding-right: 0;
+    padding-left: 0; }
+  #conversejs .navbar-expand .navbar-nav {
+    flex-direction: row; }
+    #conversejs .navbar-expand .navbar-nav .dropdown-menu {
+      position: absolute; }
+    #conversejs .navbar-expand .navbar-nav .dropdown-menu-right {
+      right: 0;
+      left: auto; }
+    #conversejs .navbar-expand .navbar-nav .nav-link {
+      padding-right: 0.5rem;
+      padding-left: 0.5rem; }
+  #conversejs .navbar-expand > .container,
+  #conversejs .navbar-expand > .container-fluid {
+    flex-wrap: nowrap; }
+  #conversejs .navbar-expand .navbar-collapse {
+    display: flex !important;
+    flex-basis: auto; }
+  #conversejs .navbar-expand .navbar-toggler {
+    display: none; }
+  #conversejs .navbar-expand .dropup .dropdown-menu {
+    top: auto;
+    bottom: 100%; }
+#conversejs .navbar-light .navbar-brand {
+  color: rgba(0, 0, 0, 0.9); }
+  #conversejs .navbar-light .navbar-brand:hover, #conversejs .navbar-light .navbar-brand:focus {
+    color: rgba(0, 0, 0, 0.9); }
+#conversejs .navbar-light .navbar-nav .nav-link {
+  color: rgba(0, 0, 0, 0.5); }
+  #conversejs .navbar-light .navbar-nav .nav-link:hover, #conversejs .navbar-light .navbar-nav .nav-link:focus {
+    color: rgba(0, 0, 0, 0.7); }
+  #conversejs .navbar-light .navbar-nav .nav-link.disabled {
+    color: rgba(0, 0, 0, 0.3); }
+#conversejs .navbar-light .navbar-nav .show > .nav-link,
+#conversejs .navbar-light .navbar-nav .active > .nav-link,
+#conversejs .navbar-light .navbar-nav .nav-link.show,
+#conversejs .navbar-light .navbar-nav .nav-link.active {
+  color: rgba(0, 0, 0, 0.9); }
+#conversejs .navbar-light .navbar-toggler {
+  color: rgba(0, 0, 0, 0.5);
+  border-color: rgba(0, 0, 0, 0.1); }
+#conversejs .navbar-light .navbar-toggler-icon {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); }
+#conversejs .navbar-light .navbar-text {
+  color: rgba(0, 0, 0, 0.5); }
+  #conversejs .navbar-light .navbar-text a {
+    color: rgba(0, 0, 0, 0.9); }
+    #conversejs .navbar-light .navbar-text a:hover, #conversejs .navbar-light .navbar-text a:focus {
+      color: rgba(0, 0, 0, 0.9); }
+#conversejs .navbar-dark .navbar-brand {
+  color: #fff; }
+  #conversejs .navbar-dark .navbar-brand:hover, #conversejs .navbar-dark .navbar-brand:focus {
+    color: #fff; }
+#conversejs .navbar-dark .navbar-nav .nav-link {
+  color: rgba(255, 255, 255, 0.5); }
+  #conversejs .navbar-dark .navbar-nav .nav-link:hover, #conversejs .navbar-dark .navbar-nav .nav-link:focus {
+    color: rgba(255, 255, 255, 0.75); }
+  #conversejs .navbar-dark .navbar-nav .nav-link.disabled {
+    color: rgba(255, 255, 255, 0.25); }
+#conversejs .navbar-dark .navbar-nav .show > .nav-link,
+#conversejs .navbar-dark .navbar-nav .active > .nav-link,
+#conversejs .navbar-dark .navbar-nav .nav-link.show,
+#conversejs .navbar-dark .navbar-nav .nav-link.active {
+  color: #fff; }
+#conversejs .navbar-dark .navbar-toggler {
+  color: rgba(255, 255, 255, 0.5);
+  border-color: rgba(255, 255, 255, 0.1); }
+#conversejs .navbar-dark .navbar-toggler-icon {
+  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); }
+#conversejs .navbar-dark .navbar-text {
+  color: rgba(255, 255, 255, 0.5); }
+  #conversejs .navbar-dark .navbar-text a {
+    color: #fff; }
+    #conversejs .navbar-dark .navbar-text a:hover, #conversejs .navbar-dark .navbar-text a:focus {
+      color: #fff; }
 #conversejs .card {
   position: relative;
   display: flex;
@@ -6868,6 +7183,8 @@ body.reset {
   font-size: 14px;
   direction: ltr;
   z-index: 1031; }
+  #conversejs .nopadding {
+    padding: 0 !important; }
   #conversejs.converse-overlayed > .row {
     flex-direction: row-reverse; }
   #conversejs.converse-fullscreen .converse-chatboxes, #conversejs.converse-mobile .converse-chatboxes {
@@ -7216,8 +7533,6 @@ body.reset {
 #conversejs .btn--small {
   font-size: 80%;
   font-weight: normal; }
-#conversejs form .form-group {
-  margin-bottom: 2em; }
 #conversejs form .form-check-label {
   margin-top: 0.3rem; }
 #conversejs form .form-control::-webkit-input-placeholder {
@@ -7289,16 +7604,11 @@ body.reset {
       color: #79a5ba; }
     #conversejs form.converse-form .text-muted.error {
       color: #A53214; }
+#conversejs form.converse-form--modal {
+  padding-bottom: 0; }
 #conversejs form.converse-centered-form {
   text-align: center; }
 
-#conversejs #user-profile-modal label {
-  font-weight: bold; }
-#conversejs .fingerprint-trust {
-  display: flex;
-  justify-content: space-between;
-  font-size: 95%; }
-
 #conversejs .chatbox-navback {
   display: none; }
 #conversejs .flyout {
@@ -7857,10 +8167,6 @@ body.reset {
   padding: 0.3em 0;
   clear: left;
   width: 100%; }
-#conversejs #converse-modals .set-xmpp-status {
-  margin: 1em; }
-  #conversejs #converse-modals .set-xmpp-status .custom-control-label {
-    margin-top: 0.25em; }
 #conversejs #controlbox {
   margin-right: 1.5em; }
   #conversejs #controlbox .box-flyout {
@@ -8261,6 +8567,30 @@ body.reset {
 
   #conversejs.converse-overlayed .converse-chatboxes .chatbox .box-flyout {
     margin-left: 30px; } }
+#conversejs #converse-modals .set-xmpp-status {
+  margin: 1em; }
+  #conversejs #converse-modals .set-xmpp-status .custom-control-label {
+    margin-top: 0.25em; }
+#conversejs #converse-modals #omemo-tabpanel {
+  margin-top: 1em; }
+#conversejs #converse-modals .btn {
+  font-weight: normal; }
+#conversejs #converse-modals #user-profile-modal label {
+  font-weight: bold; }
+#conversejs #converse-modals #user-profile-modal .list-group-item {
+  display: flex;
+  justify-content: left;
+  font-size: 95%; }
+  #conversejs #converse-modals #user-profile-modal .list-group-item input[type="checkbox"] {
+    margin-right: 1em; }
+#conversejs #converse-modals .fingerprints {
+  width: 100%;
+  margin-bottom: 1em; }
+#conversejs #converse-modals .fingerprint-trust {
+  display: flex;
+  justify-content: space-between;
+  font-size: 95%; }
+
 #conversejs #converse-roster {
   text-align: left;
   width: 100%;

+ 217 - 113
dist/converse.js

@@ -36,19 +36,34 @@
 /******/ 	// define getter function for harmony exports
 /******/ 	__webpack_require__.d = function(exports, name, getter) {
 /******/ 		if(!__webpack_require__.o(exports, name)) {
-/******/ 			Object.defineProperty(exports, name, {
-/******/ 				configurable: false,
-/******/ 				enumerable: true,
-/******/ 				get: getter
-/******/ 			});
+/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 /******/ 		}
 /******/ 	};
 /******/
 /******/ 	// define __esModule on exports
 /******/ 	__webpack_require__.r = function(exports) {
+/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 		}
 /******/ 		Object.defineProperty(exports, '__esModule', { value: true });
 /******/ 	};
 /******/
+/******/ 	// create a fake namespace object
+/******/ 	// mode & 1: value is a module id, require it
+/******/ 	// mode & 2: merge all properties of value into the ns
+/******/ 	// mode & 4: return value when already ns object
+/******/ 	// mode & 8|1: behave like require
+/******/ 	__webpack_require__.t = function(value, mode) {
+/******/ 		if(mode & 1) value = __webpack_require__(value);
+/******/ 		if(mode & 8) return value;
+/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ 		var ns = Object.create(null);
+/******/ 		__webpack_require__.r(ns);
+/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ 		return ns;
+/******/ 	};
+/******/
 /******/ 	// getDefaultExport function for compatibility with non-harmony modules
 /******/ 	__webpack_require__.n = function(module) {
 /******/ 		var getter = module && module.__esModule ?
@@ -2560,13 +2575,7 @@ backbone.nativeview = __webpack_require__(/*! backbone.nativeview */ "./node_mod
             if (_.isFunction(this.beforeRender)) {
                 this.beforeRender();
             }
-            let new_vnode;
-            if (!_.isNil(this.toHTML)) {
-                new_vnode = tovnode.toVNode(parseHTMLToDOM(this.toHTML()));
-            } else {
-                new_vnode = tovnode.toVNode(this.toDOM());
-            }
-
+            const new_vnode = tovnode.toVNode(parseHTMLToDOM(this.toHTML()));
             new_vnode.data.hook = _.extend({
                create: this.updateEventListeners.bind(this),
                update: this.updateEventListeners.bind(this)
@@ -27145,13 +27154,12 @@ var map = {
 
 function webpackContext(req) {
 	var id = webpackContextResolve(req);
-	var module = __webpack_require__(id);
-	return module;
+	return __webpack_require__(id);
 }
 function webpackContextResolve(req) {
 	var id = map[req];
 	if(!(id + 1)) { // check for number or string
-		var e = new Error('Cannot find module "' + req + '".');
+		var e = new Error("Cannot find module '" + req + "'");
 		e.code = 'MODULE_NOT_FOUND';
 		throw e;
 	}
@@ -59647,26 +59655,26 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 /*! no static exports found */
 /***/ (function(module, exports) {
 
-var g;
-
-// This works in non-strict mode
-g = (function() {
-	return this;
-})();
-
-try {
-	// This works if eval is allowed (see CSP)
-	g = g || Function("return this")() || (1, eval)("this");
-} catch (e) {
-	// This works if the window reference is available
-	if (typeof window === "object") g = window;
-}
-
-// g can still be undefined, but nothing to do about it...
-// We return undefined, instead of nothing here, so it's
-// easier to handle this case. if(!global) { ...}
-
-module.exports = g;
+var g;
+
+// This works in non-strict mode
+g = (function() {
+	return this;
+})();
+
+try {
+	// This works if eval is allowed (see CSP)
+	g = g || Function("return this")() || (1, eval)("this");
+} catch (e) {
+	// This works if the window reference is available
+	if (typeof window === "object") g = window;
+}
+
+// g can still be undefined, but nothing to do about it...
+// We return undefined, instead of nothing here, so it's
+// easier to handle this case. if(!global) { ...}
+
+module.exports = g;
 
 
 /***/ }),
@@ -59678,28 +59686,28 @@ module.exports = g;
 /*! no static exports found */
 /***/ (function(module, exports) {
 
-module.exports = function(module) {
-	if (!module.webpackPolyfill) {
-		module.deprecate = function() {};
-		module.paths = [];
-		// module.parent = undefined by default
-		if (!module.children) module.children = [];
-		Object.defineProperty(module, "loaded", {
-			enumerable: true,
-			get: function() {
-				return module.l;
-			}
-		});
-		Object.defineProperty(module, "id", {
-			enumerable: true,
-			get: function() {
-				return module.i;
-			}
-		});
-		module.webpackPolyfill = 1;
-	}
-	return module;
-};
+module.exports = function(module) {
+	if (!module.webpackPolyfill) {
+		module.deprecate = function() {};
+		module.paths = [];
+		// module.parent = undefined by default
+		if (!module.children) module.children = [];
+		Object.defineProperty(module, "loaded", {
+			enumerable: true,
+			get: function() {
+				return module.l;
+			}
+		});
+		Object.defineProperty(module, "id", {
+			enumerable: true,
+			get: function() {
+				return module.i;
+			}
+		});
+		module.webpackPolyfill = 1;
+	}
+	return module;
+};
 
 
 /***/ }),
@@ -74045,6 +74053,43 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
     dependencies: ["converse-chatview"],
     overrides: {
+      ProfileModal: {
+        events: {
+          'change input.select-all': 'selectAll',
+          'submit .fingerprint-removal': 'removeSelectedFingerprints'
+        },
+
+        initialize() {
+          const _converse = this.__super__._converse,
+                device_id = _converse.omemo_store.get('device_id');
+
+          this.devicelist = _converse.devicelists.get(_converse.bare_jid);
+          this.current_device = this.devicelist.devices.get(device_id);
+          this.other_devices = this.devicelist.devices.filter(d => d.get('id') !== device_id);
+          this.devicelist.devices.on('change:bundle', this.render, this);
+          return this.__super__.initialize.apply(this, arguments);
+        },
+
+        selectAll(ev) {
+          let sibling = ev.target.parentElement.nextElementSibling;
+
+          while (sibling) {
+            sibling.firstElementChild.checked = ev.target.checked;
+            sibling = sibling.nextElementSibling;
+          }
+        },
+
+        removeSelectedFingerprints(ev) {
+          ev.preventDefault();
+          ev.stopPropagation();
+
+          const checkboxes = ev.target.querySelectorAll('.fingerprint-removal-item input[type="checkbox"]:checked'),
+                device_ids = _.map(checkboxes, 'value');
+
+          this.devicelist.removeOwnDevices(device_ids);
+        }
+
+      },
       UserDetailsModal: {
         events: {
           'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
@@ -74376,7 +74421,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       function generateFingerprint(device) {
         return new Promise((resolve, reject) => {
           device.getBundle().then(bundle => {
-            // TODO: only generate fingerprints when necessary
+            if (_.isNil(bundle)) {
+              resolve();
+            } // TODO: only generate fingerprints when necessary
+
+
             crypto.subtle.digest('SHA-1', u.base64ToArrayBuffer(bundle['identity_key'])).then(fp => {
               bundle['fingerprint'] = u.arrayBufferToHex(fp);
               device.save('bundle', bundle);
@@ -74388,10 +74437,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         });
       }
 
-      _converse.getFingerprintsForContact = function (jid) {
-        return new Promise((resolve, reject) => {
-          _converse.getDevicesForContact(jid).then(devices => Promise.all(devices.map(d => generateFingerprint(d))).then(resolve).catch(reject));
-        });
+      _converse.generateFingerprints = function (jid) {
+        return _converse.getDevicesForContact(jid).then(devices => Promise.all(devices.map(d => generateFingerprint(d))));
       };
 
       _converse.getDevicesForContact = function (jid) {
@@ -74628,24 +74675,23 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         },
 
         fetchBundleFromServer() {
-          return new Promise((resolve, reject) => {
-            const stanza = $iq({
-              'type': 'get',
-              'from': _converse.bare_jid,
-              'to': this.get('jid')
-            }).c('pubsub', {
-              'xmlns': Strophe.NS.PUBSUB
-            }).c('items', {
-              'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`
-            });
-
-            _converse.connection.sendIQ(stanza, iq => {
-              const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
-                    bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
-                    bundle = parseBundle(bundle_el);
-              this.save('bundle', bundle);
-              resolve(bundle);
-            }, reject, _converse.IQ_TIMEOUT);
+          const stanza = $iq({
+            'type': 'get',
+            'from': _converse.bare_jid,
+            'to': this.get('jid')
+          }).c('pubsub', {
+            'xmlns': Strophe.NS.PUBSUB
+          }).c('items', {
+            'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`
+          });
+          return _converse.api.sendIQ(stanza).then(iq => {
+            const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
+                  bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
+                  bundle = parseBundle(bundle_el);
+            this.save('bundle', bundle);
+            return bundle;
+          }).catch(iq => {
+            _converse.log(iq.outerHTML, Strophe.LogLevel.ERROR);
           });
         },
 
@@ -74654,7 +74700,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
            * this device, if the information is not at hand already.
            */
           if (this.get('bundle')) {
-            return Promise.resolve(this.get('bundle').toJSON(), this);
+            return Promise.resolve(this.get('bundle'), this);
           } else {
             return this.fetchBundleFromServer();
           }
@@ -74745,6 +74791,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
 
             _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
           }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+        },
+
+        removeOwnDevices(device_ids) {// TODO
         }
 
       });
@@ -74926,7 +74975,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
       _converse.api.listen.on('userDetailsModalInitialized', contact => {
         const jid = contact.get('jid');
 
-        _converse.getFingerprintsForContact(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+        _converse.generateFingerprints(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+      });
+
+      _converse.api.listen.on('profileModalInitialized', contact => {
+        _converse.generateFingerprints(_converse.bare_jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
       });
     }
 
@@ -75117,31 +75170,40 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
         events: {
           'click .change-avatar': "openFileSelection",
           'change input[type="file"': "updateFilePreview",
-          'submit form': 'onFormSubmitted'
+          'submit .profile-form': 'onFormSubmitted'
         },
 
         initialize() {
           _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
 
           this.model.on('change', this.render, this);
+
+          _converse.emit('profileModalInitialized', this.model);
         },
 
         toHTML() {
           return tpl_profile_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), {
+            '_': _,
+            '__': __,
+            '_converse': _converse,
+            'alt_avatar': __('Your avatar image'),
             'heading_profile': __('Your Profile'),
             'label_close': __('Close'),
             'label_email': __('Email'),
             'label_fullname': __('Full Name'),
-            'label_nickname': __('Nickname'),
             'label_jid': __('XMPP Address (JID)'),
+            'label_nickname': __('Nickname'),
             'label_role': __('Role'),
             'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'),
-            'label_save': __('Save'),
             'label_url': __('URL'),
-            'alt_avatar': __('Your avatar image')
+            'view': this
           }));
         },
 
+        afterRender() {
+          this.tabs = _.map(this.el.querySelectorAll('.nav-item'), tab => new bootstrap.Tab(tab));
+        },
+
         openFileSelection(ev) {
           ev.preventDefault();
           this.el.querySelector('input[type="file"]').click();
@@ -81345,51 +81407,93 @@ __p += '<!-- src/templates/profile_modal.html -->\n<div class="modal fade" id="u
 __e(o.heading_profile) +
 '</h5>\n                <button type="button" class="close" data-dismiss="modal" aria-label="' +
 __e(o.label_close) +
-'"><span aria-hidden="true">&times;</span></button>\n            </div>\n            <form class="converse-form">\n                <div class="modal-body">\n                    <div class="row">\n                        <div class="col-auto">\n                            <a class="change-avatar" href="#">\n                                ';
+'"><span aria-hidden="true">&times;</span></button>\n            </div>\n            <div class="modal-body">\n                <ul class="nav nav-pills justify-content-center">\n                    <li role="presentation" class="nav-item">\n                        <a class="nav-link active" id="profile-tab" href="#profile-tabpanel" aria-controls="profile-tabpanel" role="tab" data-toggle="tab">Profile</a>\n                    </li>\n                    <li role="presentation" class="nav-item">\n                        <a class="nav-link" id="omemo-tab" href="#omemo-tabpanel" aria-controls="omemo-tabpanel" role="tab" data-toggle="tab">OMEMO</a>\n                    </li>\n                </ul>\n                <div class="tab-content">\n                    <div class="tab-pane fade show active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">\n                        <form class="converse-form converse-form--modal profile-form" action="#">\n                            <div class="row">\n                                <div class="col-auto">\n                                    <a class="change-avatar" href="#">\n                                        ';
  if (o.image) { ;
-__p += '\n                                    <img alt="' +
+__p += '\n                                            <img alt="' +
 __e(o.alt_avatar) +
 '" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:' +
 __e(o.image_type) +
 ';base64,' +
 __e(o.image) +
-'"/>\n                                ';
+'"/>\n                                        ';
  } ;
-__p += '\n                                ';
+__p += '\n                                        ';
  if (!o.image) { ;
-__p += '\n                                    <canvas class="avatar" height="100px" width="100px"/>\n                                ';
+__p += '\n                                            <canvas class="avatar" height="100px" width="100px"/>\n                                        ';
  } ;
-__p += '\n                            </a>\n                            <input class="hidden" name="image" type="file">\n                        </div>\n                        <div class="col">\n                            <div class="form-group">\n                                <label class="col-form-label">' +
+__p += '\n                                    </a>\n                                    <input class="hidden" name="image" type="file">\n                                </div>\n                                <div class="col">\n                                    <div class="form-group">\n                                        <label class="col-form-label">' +
 __e(o.label_jid) +
-':</label>\n                                <div>' +
+':</label>\n                                        <div>' +
 __e(o.jid) +
-'</div>\n                            </div>\n                        </div>\n                    </div>\n                    <div class="form-group">\n                        <label for="vcard-fullname" class="col-form-label">' +
+'</div>\n                                    </div>\n                                </div>\n                            </div>\n                            <div class="form-group">\n                                <label for="vcard-fullname" class="col-form-label">' +
 __e(o.label_fullname) +
-':</label>\n                        <input id="vcard-fullname" type="text" class="form-control" name="fn" value="' +
+':</label>\n                                <input id="vcard-fullname" type="text" class="form-control" name="fn" value="' +
 __e(o.fullname) +
-'">\n                    </div>\n                    <div class="form-group">\n                        <label for="vcard-nickname" class="col-form-label">' +
+'">\n                            </div>\n                            <div class="form-group">\n                                <label for="vcard-nickname" class="col-form-label">' +
 __e(o.label_nickname) +
-':</label>\n                        <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="' +
+':</label>\n                                <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="' +
 __e(o.nickname) +
-'">\n                    </div>\n                    <div class="form-group">\n                        <label for="vcard-url" class="col-form-label">' +
+'">\n                            </div>\n                            <div class="form-group">\n                                <label for="vcard-url" class="col-form-label">' +
 __e(o.label_url) +
-':</label>\n                        <input id="vcard-url" type="url" class="form-control" name="url" value="' +
+':</label>\n                                <input id="vcard-url" type="url" class="form-control" name="url" value="' +
 __e(o.url) +
-'">\n                    </div>\n                    <div class="form-group">\n                        <label for="vcard-email" class="col-form-label">' +
+'">\n                            </div>\n                            <div class="form-group">\n                                <label for="vcard-email" class="col-form-label">' +
 __e(o.label_email) +
-':</label>\n                        <input id="vcard-email" type="email" class="form-control" name="email" value="' +
+':</label>\n                                <input id="vcard-email" type="email" class="form-control" name="email" value="' +
 __e(o.email) +
-'">\n                    </div>\n                    <div class="form-group">\n                        <label for="vcard-role" class="col-form-label">' +
+'">\n                            </div>\n                            <div class="form-group">\n                                <label for="vcard-role" class="col-form-label">' +
 __e(o.label_role) +
-':</label>\n                        <input id="vcard-role" type="text" class="form-control" name="role" value="' +
+':</label>\n                                <input id="vcard-role" type="text" class="form-control" name="role" value="' +
 __e(o.role) +
-'" aria-describedby="vcard-role-help">\n                        <small id="vcard-role-help" class="form-text text-muted">' +
+'" aria-describedby="vcard-role-help">\n                                <small id="vcard-role-help" class="form-text text-muted">' +
 __e(o.label_role_help) +
-'</small>\n                    </div>\n                </div>\n                <div class="modal-footer">\n                    <button type="submit" class="save-form btn btn-primary">' +
-__e(o.label_save) +
-'</button>\n                    <button type="button" class="btn btn-secondary" data-dismiss="modal">' +
-__e(o.label_close) +
-'</button>\n                </div>\n            </form>\n        </div>\n    </div>\n</div>\n';
+'</small>\n                            </div>\n                            <hr/>\n                            <div class="form-group">\n                                <button type="submit" class="save-form btn btn-primary">' +
+__e(o.__('Save and close')) +
+'</button>\n                            </div>\n                        </form>\n                    </div>\n                    ';
+ if (o._converse.pluggable.plugins['converse-omemo'].enabled()) { ;
+__p += '\n                        <div class="tab-pane fade" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">\n                            <form class="converse-form fingerprint-removal">\n                                <ul class="list-group fingerprints">\n                                    <li class="list-group-item active">' +
+__e(o.__("This device's OMEMO fingerprint")) +
+'</li>\n                                    <li class="fingerprint-removal-item list-group-item">\n                                        ';
+ if (o.view.current_device.get('bundle') && o.view.current_device.get('bundle').fingerprint) { ;
+__p += '\n                                            <input type="checkbox" value="' +
+__e(o.view.current_device.get('id')) +
+'"\n                                                   aria-label="' +
+__e(o.__('Checkbox for removing the following fingerprint')) +
+'">\n                                            <span class="fingerprint">' +
+__e(o.view.current_device.get('bundle').fingerprint) +
+'</span>\n                                        ';
+ } else {;
+__p += '\n                                            <span class="spinner fa fa-spinner centered"/>\n                                        ';
+ } ;
+__p += '\n                                    </li>\n                                </ul>\n                                ';
+ if (o.view.other_devices) { ;
+__p += '\n                                    <ul class="list-group fingerprints">\n                                        <li class="list-group-item active">\n                                            <input type="checkbox" class="select-all" title="' +
+__e(o.__('Select all')) +
+'"\n                                                   aria-label="' +
+__e(o.__('Checkbox to select fingerprints of all other OMEMO devices')) +
+'">\n                                            ' +
+__e(o.__('Other OMEMO-enabled devices')) +
+'\n                                        </li>\n                                        ';
+ o._.forEach(o.view.other_devices, function (device) { ;
+__p += '\n                                            ';
+ if (device.get('bundle') && device.get('bundle').fingerprint) { ;
+__p += '\n                                            <li class="fingerprint-removal-item list-group-item">\n                                                <input type="checkbox" value="' +
+__e(device.get('id')) +
+'"\n                                                       aria-label="' +
+__e(o.__('Checkbox for selecting the following fingerprint')) +
+'">\n                                                <span class="fingerprint">' +
+__e(device.get('bundle').fingerprint) +
+'</span>\n                                            </li>\n                                            ';
+ } ;
+__p += '\n                                        ';
+ }); ;
+__p += '\n                                    </ul>\n                                ';
+ } ;
+__p += '\n                                <div class="form-group">\n                                    <button type="submit" class="save-form btn btn-primary">' +
+__e(o.__('Remove checked devices and close')) +
+'</button>\n                                </div>\n                            </form>\n                        </div>\n                    ';
+ } ;
+__p += '\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n';
 return __p
 };
 
@@ -82407,9 +82511,7 @@ __p += '\n                            ';
  o.view.devicelist.devices.each(function (device) { ;
 __p += '\n                                ';
  if (device.get('bundle') && device.get('bundle').fingerprint) { ;
-__p += '\n                                <li class="list-group-item">\n                                    <form class="fingerprint-trust">\n                                    <span class="fingerprint">' +
-__e(device.get('bundle').fingerprint) +
-'</span>\n                                    <div class="btn-group btn-group-toggle">\n                                        <label class="btn btn--small ';
+__p += '\n                                <li class="list-group-item">\n                                    <form class="fingerprint-trust">\n                                    <div class="btn-group btn-group-toggle">\n                                        <label class="btn btn--small ';
  if (device.get('trusted') !== -1) { ;
 __p += ' btn-primary active ';
  } else { ;
@@ -82437,7 +82539,9 @@ __p += ' checked="checked" ';
  } ;
 __p += '>' +
 __e(o.__('Untrusted')) +
-'\n                                        </label>\n                                    </div>\n                                    </form>\n                                </li>\n                                ';
+'\n                                        </label>\n                                    </div>\n                                    <span class="fingerprint">' +
+__e(device.get('bundle').fingerprint) +
+'</span>\n                                    </form>\n                                </li>\n                                ';
  } ;
 __p += '\n                            ';
  }); ;

+ 0 - 9
sass/_controlbox.scss

@@ -60,15 +60,6 @@
         width: 100%;
     }
 
-    #converse-modals {
-        .set-xmpp-status {
-            margin: 1em;
-            .custom-control-label {
-                margin-top: 0.25em;
-            }
-        }
-    }
-
     #controlbox {
         .box-flyout {
             background-color: white;

+ 4 - 0
sass/_core.scss

@@ -61,6 +61,10 @@ body.reset {
     direction: ltr;
     z-index: 1031; // One more than bootstrap navbar
 
+    .nopadding {
+        padding: 0 !important;
+    }
+
     &.converse-overlayed {
         > .row {
             flex-direction: row-reverse;

+ 5 - 4
sass/_forms.scss

@@ -5,10 +5,6 @@
     }
 
     form {
-        .form-group {
-            margin-bottom: 2em;
-        }
-
         .form-check-label {
             margin-top: $form-check-input-margin-y;
         }
@@ -108,6 +104,11 @@
                 }
             }
         }
+
+        &.converse-form--modal {
+            padding-bottom: 0;
+        }
+
         &.converse-centered-form {
             text-align: center;
         }

+ 44 - 0
sass/_modal.scss

@@ -0,0 +1,44 @@
+#conversejs {
+    #converse-modals {
+        .set-xmpp-status {
+            margin: 1em;
+            .custom-control-label {
+                margin-top: 0.25em;
+            }
+        }
+
+        #omemo-tabpanel {
+            margin-top: 1em;
+        }
+
+        .btn {
+            font-weight: normal;
+        }
+
+        #user-profile-modal {
+            label {
+                font-weight: bold;
+            }
+            .list-group-item {
+                display: flex;
+                justify-content: left;
+                font-size: 95%;
+
+                input[type="checkbox"] {
+                    margin-right: 1em;
+                }
+            }
+        }
+
+        .fingerprints {
+            width: 100%;
+            margin-bottom: 1em;
+        }
+        
+        .fingerprint-trust {
+            display: flex;
+            justify-content: space-between;
+            font-size: 95%;
+        }
+    }
+}

+ 0 - 12
sass/_profile.scss

@@ -1,12 +0,0 @@
-#conversejs {
-    #user-profile-modal {
-        label {
-            font-weight: bold;
-        }
-    }
-    .fingerprint-trust {
-        display: flex;
-        justify-content: space-between;
-        font-size: 95%;
-    }
-}

+ 3 - 1
sass/converse.scss

@@ -26,6 +26,8 @@
     @import "bootstrap/scss/button-group";
     @import "bootstrap/scss/input-group";
     @import "bootstrap/scss/custom-forms";
+    @import "bootstrap/scss/nav";
+    @import "bootstrap/scss/navbar";
     @import "bootstrap/scss/card";
     @import "bootstrap/scss/breadcrumb";
     @import "bootstrap/scss/badge";
@@ -40,9 +42,9 @@
 }
 @import "core";
 @import "forms";
-@import "profile";
 @import "chatbox";
 @import "controlbox";
+@import "modal";
 @import "roster";
 @import "lists";
 @import "chatrooms";

+ 45 - 2
src/converse-omemo.js

@@ -70,6 +70,41 @@
 
         overrides: {
 
+            ProfileModal: {
+                events: {
+                    'change input.select-all': 'selectAll',
+                    'submit .fingerprint-removal': 'removeSelectedFingerprints'
+                },
+
+                initialize () {
+                    const { _converse } = this.__super__,
+                          device_id = _converse.omemo_store.get('device_id');
+
+                    this.devicelist = _converse.devicelists.get(_converse.bare_jid);
+                    this.current_device = this.devicelist.devices.get(device_id);
+                    this.other_devices = this.devicelist.devices.filter(d => (d.get('id') !== device_id));
+
+                    this.devicelist.devices.on('change:bundle', this.render, this);
+                    return this.__super__.initialize.apply(this, arguments);
+                },
+
+                selectAll (ev) {
+                    let sibling = ev.target.parentElement.nextElementSibling;
+                    while (sibling) {
+                        sibling.firstElementChild.checked = ev.target.checked;
+                        sibling = sibling.nextElementSibling;
+                    }
+                },
+
+                removeSelectedFingerprints (ev) {
+                    ev.preventDefault();
+                    ev.stopPropagation();
+                    const checkboxes = ev.target.querySelectorAll('.fingerprint-removal-item input[type="checkbox"]:checked'),
+                          device_ids = _.map(checkboxes, 'value');
+                    this.devicelist.removeOwnDevices(device_ids);
+                },
+            },
+
             UserDetailsModal: {
                 events: {
                     'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
@@ -415,7 +450,7 @@
                 });
             }
 
-            _converse.getFingerprintsForContact = function (jid) {
+            _converse.generateFingerprints= function (jid) {
                 return _converse.getDevicesForContact(jid)
                     .then(devices => Promise.all(devices.map(d => generateFingerprint(d))))
             }
@@ -734,6 +769,10 @@
                         });
                         _converse.connection.sendIQ(stanza, resolve, reject, _converse.IQ_TIMEOUT);
                     }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+                },
+
+                removeOwnDevices (device_ids) {
+                    // TODO
                 }
             });
 
@@ -893,7 +932,11 @@
 
             _converse.api.listen.on('userDetailsModalInitialized', (contact) => {
                 const jid = contact.get('jid');
-                _converse.getFingerprintsForContact(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+                _converse.generateFingerprints(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+            });
+
+            _converse.api.listen.on('profileModalInitialized', (contact) => {
+                _converse.generateFingerprints(_converse.bare_jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
             });
         }
     });

+ 12 - 4
src/converse-profile.js

@@ -48,32 +48,40 @@
                 events: {
                     'click .change-avatar': "openFileSelection",
                     'change input[type="file"': "updateFilePreview",
-                    'submit form': 'onFormSubmitted'
+                    'submit .profile-form': 'onFormSubmitted'
                 },
 
                 initialize () {
                     _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                     this.model.on('change', this.render, this);
+                    _converse.emit('profileModalInitialized', this.model);
                 },
 
                 toHTML () {
                     return tpl_profile_modal(_.extend(
                         this.model.toJSON(),
                         this.model.vcard.toJSON(), {
+                        '_': _,
+                        '__': __,
+                        '_converse': _converse,
+                        'alt_avatar': __('Your avatar image'),
                         'heading_profile': __('Your Profile'),
                         'label_close': __('Close'),
                         'label_email': __('Email'),
                         'label_fullname': __('Full Name'),
-                        'label_nickname': __('Nickname'),
                         'label_jid': __('XMPP Address (JID)'),
+                        'label_nickname': __('Nickname'),
                         'label_role': __('Role'),
                         'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'),
-                        'label_save': __('Save'),
                         'label_url': __('URL'),
-                        'alt_avatar': __('Your avatar image')
+                        'view': this
                     }));
                 },
 
+                afterRender () {
+                    this.tabs = _.map(this.el.querySelectorAll('.nav-item'), (tab) => new bootstrap.Tab(tab));
+                },
+
                 openFileSelection (ev) {
                     ev.preventDefault();
                     this.el.querySelector('input[type="file"]').click();

+ 95 - 44
src/templates/profile_modal.html

@@ -5,54 +5,105 @@
                 <h5 class="modal-title" id="user-profile-modal-label">{{{o.heading_profile}}}</h5>
                 <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">&times;</span></button>
             </div>
-            <form class="converse-form">
-                <div class="modal-body">
-                    <div class="row">
-                        <div class="col-auto">
-                            <a class="change-avatar" href="#">
-                                {[ if (o.image) { ]}
-                                    <img alt="{{{o.alt_avatar}}}" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
-                                {[ } ]}
-                                {[ if (!o.image) { ]}
-                                    <canvas class="avatar" height="100px" width="100px"/>
-                                {[ } ]}
-                            </a>
-                            <input class="hidden" name="image" type="file">
-                        </div>
-                        <div class="col">
+            <div class="modal-body">
+                <ul class="nav nav-pills justify-content-center">
+                    <li role="presentation" class="nav-item">
+                        <a class="nav-link active" id="profile-tab" href="#profile-tabpanel" aria-controls="profile-tabpanel" role="tab" data-toggle="tab">Profile</a>
+                    </li>
+                    <li role="presentation" class="nav-item">
+                        <a class="nav-link" id="omemo-tab" href="#omemo-tabpanel" aria-controls="omemo-tabpanel" role="tab" data-toggle="tab">OMEMO</a>
+                    </li>
+                </ul>
+                <div class="tab-content">
+                    <div class="tab-pane fade show active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
+                        <form class="converse-form converse-form--modal profile-form" action="#">
+                            <div class="row">
+                                <div class="col-auto">
+                                    <a class="change-avatar" href="#">
+                                        {[ if (o.image) { ]}
+                                            <img alt="{{{o.alt_avatar}}}" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
+                                        {[ } ]}
+                                        {[ if (!o.image) { ]}
+                                            <canvas class="avatar" height="100px" width="100px"/>
+                                        {[ } ]}
+                                    </a>
+                                    <input class="hidden" name="image" type="file">
+                                </div>
+                                <div class="col">
+                                    <div class="form-group">
+                                        <label class="col-form-label">{{{o.label_jid}}}:</label>
+                                        <div>{{{o.jid}}}</div>
+                                    </div>
+                                </div>
+                            </div>
                             <div class="form-group">
-                                <label class="col-form-label">{{{o.label_jid}}}:</label>
-                                <div>{{{o.jid}}}</div>
+                                <label for="vcard-fullname" class="col-form-label">{{{o.label_fullname}}}:</label>
+                                <input id="vcard-fullname" type="text" class="form-control" name="fn" value="{{{o.fullname}}}">
                             </div>
-                        </div>
-                    </div>
-                    <div class="form-group">
-                        <label for="vcard-fullname" class="col-form-label">{{{o.label_fullname}}}:</label>
-                        <input id="vcard-fullname" type="text" class="form-control" name="fn" value="{{{o.fullname}}}">
-                    </div>
-                    <div class="form-group">
-                        <label for="vcard-nickname" class="col-form-label">{{{o.label_nickname}}}:</label>
-                        <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="{{{o.nickname}}}">
-                    </div>
-                    <div class="form-group">
-                        <label for="vcard-url" class="col-form-label">{{{o.label_url}}}:</label>
-                        <input id="vcard-url" type="url" class="form-control" name="url" value="{{{o.url}}}">
-                    </div>
-                    <div class="form-group">
-                        <label for="vcard-email" class="col-form-label">{{{o.label_email}}}:</label>
-                        <input id="vcard-email" type="email" class="form-control" name="email" value="{{{o.email}}}">
-                    </div>
-                    <div class="form-group">
-                        <label for="vcard-role" class="col-form-label">{{{o.label_role}}}:</label>
-                        <input id="vcard-role" type="text" class="form-control" name="role" value="{{{o.role}}}" aria-describedby="vcard-role-help">
-                        <small id="vcard-role-help" class="form-text text-muted">{{{o.label_role_help}}}</small>
+                            <div class="form-group">
+                                <label for="vcard-nickname" class="col-form-label">{{{o.label_nickname}}}:</label>
+                                <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="{{{o.nickname}}}">
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-url" class="col-form-label">{{{o.label_url}}}:</label>
+                                <input id="vcard-url" type="url" class="form-control" name="url" value="{{{o.url}}}">
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-email" class="col-form-label">{{{o.label_email}}}:</label>
+                                <input id="vcard-email" type="email" class="form-control" name="email" value="{{{o.email}}}">
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-role" class="col-form-label">{{{o.label_role}}}:</label>
+                                <input id="vcard-role" type="text" class="form-control" name="role" value="{{{o.role}}}" aria-describedby="vcard-role-help">
+                                <small id="vcard-role-help" class="form-text text-muted">{{{o.label_role_help}}}</small>
+                            </div>
+                            <hr/>
+                            <div class="form-group">
+                                <button type="submit" class="save-form btn btn-primary">{{{o.__('Save and close')}}}</button>
+                            </div>
+                        </form>
                     </div>
+                    {[ if (o._converse.pluggable.plugins['converse-omemo'].enabled()) { ]}
+                        <div class="tab-pane fade" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">
+                            <form class="converse-form fingerprint-removal">
+                                <ul class="list-group fingerprints">
+                                    <li class="list-group-item active">{{{o.__("This device's OMEMO fingerprint")}}}</li>
+                                    <li class="fingerprint-removal-item list-group-item">
+                                        {[ if (o.view.current_device.get('bundle') && o.view.current_device.get('bundle').fingerprint) { ]}
+                                            <input type="checkbox" value="{{{o.view.current_device.get('id')}}}"
+                                                   aria-label="{{{o.__('Checkbox for removing the following fingerprint')}}}">
+                                            <span class="fingerprint">{{{o.view.current_device.get('bundle').fingerprint}}}</span>
+                                        {[ } else {]}
+                                            <span class="spinner fa fa-spinner centered"/>
+                                        {[ } ]}
+                                    </li>
+                                </ul>
+                                {[ if (o.view.other_devices) { ]}
+                                    <ul class="list-group fingerprints">
+                                        <li class="list-group-item active">
+                                            <input type="checkbox" class="select-all" title="{{{o.__('Select all')}}}"
+                                                   aria-label="{{{o.__('Checkbox to select fingerprints of all other OMEMO devices')}}}">
+                                            {{{o.__('Other OMEMO-enabled devices')}}}
+                                        </li>
+                                        {[ o._.forEach(o.view.other_devices, function (device) { ]}
+                                            {[ if (device.get('bundle') && device.get('bundle').fingerprint) { ]}
+                                            <li class="fingerprint-removal-item list-group-item">
+                                                <input type="checkbox" value="{{{device.get('id')}}}"
+                                                       aria-label="{{{o.__('Checkbox for selecting the following fingerprint')}}}">
+                                                <span class="fingerprint">{{{device.get('bundle').fingerprint}}}</span>
+                                            </li>
+                                            {[ } ]}
+                                        {[ }); ]}
+                                    </ul>
+                                {[ } ]}
+                                <div class="form-group">
+                                    <button type="submit" class="save-form btn btn-primary">{{{o.__('Remove checked devices and close')}}}</button>
+                                </div>
+                            </form>
+                        </div>
+                    {[ } ]}
                 </div>
-                <div class="modal-footer">
-                    <button type="submit" class="save-form btn btn-primary">{{{o.label_save}}}</button>
-                    <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.label_close}}}</button>
-                </div>
-            </form>
+            </div>
         </div>
     </div>
 </div>

+ 1 - 1
src/templates/user_details_modal.html

@@ -40,7 +40,6 @@
                                 {[ if (device.get('bundle') && device.get('bundle').fingerprint) { ]}
                                 <li class="list-group-item">
                                     <form class="fingerprint-trust">
-                                    <span class="fingerprint">{{{device.get('bundle').fingerprint}}}</span>
                                     <div class="btn-group btn-group-toggle">
                                         <label class="btn btn--small {[ if (device.get('trusted') !== -1) { ]} btn-primary active {[ } else { ]}  btn-secondary {[ } ]}">
                                             <input type="radio" name="{{{device.get('id')}}}" value="1"
@@ -51,6 +50,7 @@
                                                 {[ if (device.get('trusted') === -1) { ]} checked="checked" {[ } ]}>{{{o.__('Untrusted')}}}
                                         </label>
                                     </div>
+                                    <span class="fingerprint">{{{device.get('bundle').fingerprint}}}</span>
                                     </form>
                                 </li>
                                 {[ } ]}