Просмотр исходного кода

Show fingerprints in the user details modal

updates #497
JC Brand 7 лет назад
Родитель
Сommit
bcd6845756

+ 45 - 38
css/converse.css

@@ -2346,7 +2346,7 @@
   --primary: #387592;
   --secondary: #6c757d;
   --success: #3AA569;
-  --info: #17a2b8;
+  --info: #3AA569;
   --warning: #ffc107;
   --danger: #E77051;
   --light: #f8f9fa;
@@ -3594,24 +3594,24 @@
       box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
 #conversejs .btn-info {
   color: #fff;
-  background-color: #17a2b8;
-  border-color: #17a2b8; }
+  background-color: #3AA569;
+  border-color: #3AA569; }
   #conversejs .btn-info:hover {
     color: #fff;
-    background-color: #138496;
-    border-color: #117a8b; }
+    background-color: #308957;
+    border-color: #2d7f51; }
   #conversejs .btn-info:focus, #conversejs .btn-info.focus {
-    box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
   #conversejs .btn-info.disabled, #conversejs .btn-info:disabled {
     color: #fff;
-    background-color: #17a2b8;
-    border-color: #17a2b8; }
+    background-color: #3AA569;
+    border-color: #3AA569; }
   #conversejs .btn-info:not(:disabled):not(.disabled):active, #conversejs .btn-info:not(:disabled):not(.disabled).active, .show > #conversejs .btn-info.dropdown-toggle {
     color: #fff;
-    background-color: #117a8b;
-    border-color: #10707f; }
+    background-color: #2d7f51;
+    border-color: #29764b; }
     #conversejs .btn-info:not(:disabled):not(.disabled):active:focus, #conversejs .btn-info:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
 #conversejs .btn-warning {
   color: #212529;
   background-color: #ffc107;
@@ -3753,25 +3753,25 @@
     #conversejs .btn-outline-success:not(:disabled):not(.disabled):active:focus, #conversejs .btn-outline-success:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-outline-success.dropdown-toggle:focus {
       box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
 #conversejs .btn-outline-info {
-  color: #17a2b8;
+  color: #3AA569;
   background-color: transparent;
   background-image: none;
-  border-color: #17a2b8; }
+  border-color: #3AA569; }
   #conversejs .btn-outline-info:hover {
     color: #fff;
-    background-color: #17a2b8;
-    border-color: #17a2b8; }
+    background-color: #3AA569;
+    border-color: #3AA569; }
   #conversejs .btn-outline-info:focus, #conversejs .btn-outline-info.focus {
-    box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
+    box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
   #conversejs .btn-outline-info.disabled, #conversejs .btn-outline-info:disabled {
-    color: #17a2b8;
+    color: #3AA569;
     background-color: transparent; }
   #conversejs .btn-outline-info:not(:disabled):not(.disabled):active, #conversejs .btn-outline-info:not(:disabled):not(.disabled).active, .show > #conversejs .btn-outline-info.dropdown-toggle {
     color: #fff;
-    background-color: #17a2b8;
-    border-color: #17a2b8; }
+    background-color: #3AA569;
+    border-color: #3AA569; }
     #conversejs .btn-outline-info:not(:disabled):not(.disabled):active:focus, #conversejs .btn-outline-info:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-outline-info.dropdown-toggle:focus {
-      box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
+      box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
 #conversejs .btn-outline-warning {
   color: #ffc107;
   background-color: transparent;
@@ -4582,11 +4582,11 @@
     background-color: #2d7f51; }
 #conversejs .badge-info {
   color: #fff;
-  background-color: #17a2b8; }
+  background-color: #3AA569; }
   #conversejs .badge-info[href]:hover, #conversejs .badge-info[href]:focus {
     color: #fff;
     text-decoration: none;
-    background-color: #117a8b; }
+    background-color: #2d7f51; }
 #conversejs .badge-warning {
   color: #212529;
   background-color: #ffc107; }
@@ -4658,13 +4658,13 @@
   #conversejs .alert-success .alert-link {
     color: #11301f; }
 #conversejs .alert-info {
-  color: #0c5460;
-  background-color: #d1ecf1;
-  border-color: #bee5eb; }
+  color: #1e5637;
+  background-color: #d8ede1;
+  border-color: #c8e6d5; }
   #conversejs .alert-info hr {
-    border-top-color: #abdde5; }
+    border-top-color: #b6dec8; }
   #conversejs .alert-info .alert-link {
-    color: #062c33; }
+    color: #11301f; }
 #conversejs .alert-warning {
   color: #856404;
   background-color: #fff3cd;
@@ -4782,15 +4782,15 @@
     background-color: #1e5637;
     border-color: #1e5637; }
 #conversejs .list-group-item-info {
-  color: #0c5460;
-  background-color: #bee5eb; }
+  color: #1e5637;
+  background-color: #c8e6d5; }
   #conversejs .list-group-item-info.list-group-item-action:hover, #conversejs .list-group-item-info.list-group-item-action:focus {
-    color: #0c5460;
-    background-color: #abdde5; }
+    color: #1e5637;
+    background-color: #b6dec8; }
   #conversejs .list-group-item-info.list-group-item-action.active {
     color: #fff;
-    background-color: #0c5460;
-    border-color: #0c5460; }
+    background-color: #1e5637;
+    border-color: #1e5637; }
 #conversejs .list-group-item-warning {
   color: #856404;
   background-color: #ffeeba; }
@@ -5179,11 +5179,11 @@
 #conversejs button.bg-success:focus {
   background-color: #2d7f51 !important; }
 #conversejs .bg-info {
-  background-color: #17a2b8 !important; }
+  background-color: #3AA569 !important; }
 #conversejs a.bg-info:hover, #conversejs a.bg-info:focus,
 #conversejs button.bg-info:hover,
 #conversejs button.bg-info:focus {
-  background-color: #117a8b !important; }
+  background-color: #2d7f51 !important; }
 #conversejs .bg-warning {
   background-color: #ffc107 !important; }
 #conversejs a.bg-warning:hover, #conversejs a.bg-warning:focus,
@@ -5239,7 +5239,7 @@
 #conversejs .border-success {
   border-color: #3AA569 !important; }
 #conversejs .border-info {
-  border-color: #17a2b8 !important; }
+  border-color: #3AA569 !important; }
 #conversejs .border-warning {
   border-color: #ffc107 !important; }
 #conversejs .border-danger {
@@ -6792,9 +6792,9 @@
 #conversejs a.text-success:hover, #conversejs a.text-success:focus {
   color: #2d7f51 !important; }
 #conversejs .text-info {
-  color: #17a2b8 !important; }
+  color: #3AA569 !important; }
 #conversejs a.text-info:hover, #conversejs a.text-info:focus {
-  color: #117a8b !important; }
+  color: #2d7f51 !important; }
 #conversejs .text-warning {
   color: #ffc107 !important; }
 #conversejs a.text-warning:hover, #conversejs a.text-warning:focus {
@@ -7213,6 +7213,9 @@ body.reset {
 @media screen and (max-height: 450px) {
   #conversejs {
     left: 0; } }
+#conversejs .btn--small {
+  font-size: 80%;
+  font-weight: normal; }
 #conversejs form .form-group {
   margin-bottom: 2em; }
 #conversejs form .form-check-label {
@@ -7291,6 +7294,10 @@ body.reset {
 
 #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; }

Разница между файлами не показана из-за своего большого размера
+ 229 - 255
dist/converse.js


+ 1 - 1
locale/af/LC_MESSAGES/converse.po

@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: Converse.js 0.4\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2018-07-22 11:17+0200\n"
-"PO-Revision-Date: 2018-07-22 12:12+0200\n"
+"PO-Revision-Date: 2018-07-22 15:37+0200\n"
 "Last-Translator: JC Brand <jc@opkode.com>\n"
 "Language-Team: Afrikaans <https://hosted.weblate.org/projects/conversejs/"
 "translations/af/>\n"

+ 5 - 0
sass/_forms.scss

@@ -1,4 +1,9 @@
 #conversejs {
+    .btn--small {
+        font-size: 80%;
+        font-weight: normal;
+    }
+
     form {
         .form-group {
             margin-bottom: 2em;

+ 5 - 0
sass/_profile.scss

@@ -4,4 +4,9 @@
             font-weight: bold;
         }
     }
+    .fingerprint-trust {
+        display: flex;
+        justify-content: space-between;
+        font-size: 95%;
+    }
 }

+ 2 - 0
sass/_variables.scss

@@ -34,6 +34,8 @@ $green: #3AA569;
 $dark-green: #1E9652;
 $darkest-green: #0E763B;
 
+$info:  $green !default;
+
 $lightest-green: #E7FBF0;
 $light-green: #5CBC86;
 $green: #3AA569;

+ 123 - 6
spec/omemo.js

@@ -245,7 +245,7 @@
                 devicelist = _converse.devicelists.get(contact_jid);
                 expect(devicelist.devices.length).toBe(1);
                 let device = devicelist.devices.at(0);
-                expect(device.get('bundle').identity_key).toBe(3333);
+                expect(device.get('bundle').identity_key).toBe('3333');
                 expect(device.get('bundle').signed_prekey.public_key).toBe('1111');
                 expect(device.get('bundle').signed_prekey.id).toBe(4223);
                 expect(device.get('bundle').signed_prekey.signature).toBe('2222');
@@ -276,7 +276,7 @@
                 devicelist = _converse.devicelists.get(contact_jid);
                 expect(devicelist.devices.length).toBe(1);
                 device = devicelist.devices.at(0);
-                expect(device.get('bundle').identity_key).toBe(7777);
+                expect(device.get('bundle').identity_key).toBe('7777');
                 expect(device.get('bundle').signed_prekey.public_key).toBe('5555');
                 expect(device.get('bundle').signed_prekey.id).toBe(4223);
                 expect(device.get('bundle').signed_prekey.signature).toBe('6666');
@@ -309,7 +309,7 @@
                 expect(devicelist.devices.at(0).get('id')).toBe('555');
                 expect(devicelist.devices.at(1).get('id')).toBe('123456789');
                 device = devicelist.devices.at(1);
-                expect(device.get('bundle').identity_key).toBe(1111);
+                expect(device.get('bundle').identity_key).toBe('1111');
                 expect(device.get('bundle').signed_prekey.public_key).toBe('8888');
                 expect(device.get('bundle').signed_prekey.id).toBe(9999);
                 expect(device.get('bundle').signed_prekey.signature).toBe('3333');
@@ -396,8 +396,15 @@
                             `</publish>`+
                         `</pubsub>`+
                     `</iq>`)
-                done();
-            });
+
+                const stanza = $iq({
+                    'from': _converse.bare_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result'});
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+                return _converse.api.waitUntil('OMEMOInitialized');
+            }).then(done).catch(_.partial(console.error, _));
         }));
 
         it("adds a toolbar button for starting an encrypted chat session",
@@ -494,7 +501,6 @@
                     'type': 'result'});
                 _converse.connection._dataRecv(test_utils.createRequest(stanza));
 
-
                 return test_utils.waitUntil(() => {
                     return _.filter(
                         _converse.connection.IQ_stanzas,
@@ -565,6 +571,117 @@
                 done();
             }).catch(_.partial(console.error, _));
         }));
+
+        it("shows OMEMO device fingerprints in the user details modal",
+            mock.initConverseWithPromises(
+                null, ['rosterGroupsFetched'], {},
+                function (done, _converse) {
+
+            let iq_stanza;
+            test_utils.createContacts(_converse, 'current', 1);
+            const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
+            test_utils.openChatBoxFor(_converse, contact_jid);
+
+            // We simply emit, to avoid doing all the setup work
+            _converse.emit('OMEMOInitialized');
+
+            const view = _converse.chatboxviews.get(contact_jid);
+            const show_modal_button = view.el.querySelector('.show-user-details-modal');
+            show_modal_button.click();
+            const modal = view.user_details_modal;
+
+            test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(() => {
+                return test_utils.waitUntil(() => {
+                    return _.filter(
+                        _converse.connection.IQ_stanzas,
+                        (iq) => {
+                            const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
+                            if (node) { iq_stanza = iq.nodeTree; }
+                            return node;
+                        }).length;});
+            }).then(() => {
+                iq_stanza;
+                expect(iq_stanza.outerHTML).toBe(
+                    `<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
+                        `<query xmlns="http://jabber.org/protocol/disco#items" node="eu.siacs.conversations.axolotl.devicelist"/>`+
+                    `</iq>`);
+                
+                const stanza = $iq({
+                    'from': contact_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result',
+                }).c('query', {
+                    'xmlns': 'http://jabber.org/protocol/disco#items',
+                    'node': 'eu.siacs.conversations.axolotl.devicelist'
+                }).c('device', {'id': '555'}).up()
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                return test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(function () {
+                    return test_utils.waitUntil(() => {
+                        return _.filter(
+                            _converse.connection.IQ_stanzas,
+                            (iq) => {
+                                const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]');
+                                if (node) { iq_stanza = iq.nodeTree; }
+                                return node;
+                            }).length;});
+                });
+            }).then(() => {
+                expect(iq_stanza.outerHTML).toBe(
+                    `<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
+                        `<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
+                            `<items node="eu.siacs.conversations.axolotl.bundles:555"/>`+
+                        `</pubsub>`+
+                    `</iq>`);
+
+                const stanza = $iq({
+                    'from': contact_jid,
+                    'id': iq_stanza.getAttribute('id'),
+                    'to': _converse.bare_jid,
+                    'type': 'result',
+                }).c('pubsub', {
+                    'xmlns': 'http://jabber.org/protocol/pubsub'
+                    }).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
+                        .c('item')
+                            .c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
+                                .c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
+                                .c('signedPreKeySignature').t(btoa('2222')).up()
+                                .c('identityKey').t(btoa('3333')).up()
+                                .c('prekeys')
+                                    .c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
+                                    .c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
+                                    .c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
+                _converse.connection._dataRecv(test_utils.createRequest(stanza));
+
+                const view = _converse.chatboxviews.get(contact_jid);
+                const modal = view.user_details_modal;
+                return test_utils.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
+            }).then(() => {
+                const view = _converse.chatboxviews.get(contact_jid);
+                const modal = view.user_details_modal;
+                expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
+                const el = modal.el.querySelector('.fingerprints .fingerprint');
+                expect(el.textContent).toBe('f56d6351aa71cff0debea014d13525e42036187a');
+
+                expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
+
+                let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
+                expect(trusted_radio.checked).toBe(true);
+
+                let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
+                expect(untrusted_radio.checked).toBe(false);
+
+                // Test that the device can be set to untrusted
+                untrusted_radio.click();
+                trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]');
+                expect(trusted_radio.hasAttribute('checked')).toBe(false);
+
+                untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]');
+                expect(untrusted_radio.hasAttribute('checked')).toBe(true);
+                done();
+            });
+        }));
     });
 
     describe("A chatbox with an active OMEMO session", function() {

+ 0 - 11
spec/roster.js

@@ -389,7 +389,6 @@
                     function (done, _converse) {
 
                 _converse.roster_groups = true;
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 _converse.rosterview.render();
                 test_utils.openControlBox();
@@ -430,7 +429,6 @@
                     function (done, _converse) {
 
                 _converse.roster_groups = true;
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 _converse.rosterview.render();
 
@@ -477,7 +475,6 @@
 
                 _converse.roster_groups = true;
                 var groups = ['colleagues', 'friends'];
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 test_utils.openControlBox();
                 _converse.rosterview.render();
@@ -576,7 +573,6 @@
                     null, ['rosterGroupsFetched'], {},
                     function (done, _converse) {
 
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 test_utils.openControlBox();
                 _converse.roster.create({
@@ -726,7 +722,6 @@
 
                 var i, t;
                 test_utils.openControlBox();
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 for (i=0; i<mock.pend_names.length; i++) {
                     _converse.roster.create({
@@ -908,7 +903,6 @@
                 test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700)
                 .then(function () {
                     var jid, t;
-                    spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
@@ -935,7 +929,6 @@
                     return $(_converse.rosterview.el).find('.roster-group li').length;
                 }, 700).then(function () {
                     var jid, t;
-                    spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
@@ -963,7 +956,6 @@
                     return $(_converse.rosterview.el).find('.roster-group li').length;
                 }, 700).then(function () {
                     var jid, t;
-                    spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
@@ -991,7 +983,6 @@
                         return $(_converse.rosterview.el).find('.roster-group li').length;
                 }, 700).then(function () {
                     var jid, t;
-                    spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
@@ -1020,7 +1011,6 @@
                     }, 500)
                 .then(function () {
                     var jid, t;
-                    spyOn(_converse, 'emit');
                     spyOn(_converse.rosterview, 'update').and.callThrough();
                     var $roster = $(_converse.rosterview.el);
                     for (var i=0; i<mock.cur_names.length; i++) {
@@ -1151,7 +1141,6 @@
                         names.push($(item).text().replace(/^\s+|\s+$/g, ''));
                     }
                 };
-                spyOn(_converse, 'emit');
                 spyOn(_converse.rosterview, 'update').and.callThrough();
                 spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough();
                 for (i=0; i<mock.req_names.length; i++) {

+ 9 - 12
src/converse-chatview.js

@@ -239,32 +239,29 @@
 
                 events: {
                     'click button.remove-contact': 'removeContact',
-                    'click button.refresh-contact': 'refreshContact'
+                    'click button.refresh-contact': 'refreshContact',
+                    'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
                 },
 
                 initialize () {
                     _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
                     this.model.on('contactAdded', this.registerContactEventHandlers, this);
+                    this.model.on('change', this.render, this);
                     this.registerContactEventHandlers();
+                    _converse.emit('userDetailsModalInitialized', this.model);
                 },
 
                 toHTML () {
                     return tpl_user_details_modal(_.extend(
                         this.model.toJSON(),
                         this.model.vcard.toJSON(), {
+                        '_': _,
+                        '__': __,
+                        'view': this,
+                        '_converse': _converse,
                         'allow_contact_removal': _converse.allow_contact_removal,
-                        'alt_profile_image': __("The User's Profile Image"),
                         'display_name': this.model.getDisplayName(),
-                        'is_roster_contact': !_.isUndefined(this.model.contact),
-                        'label_close': __('Close'),
-                        'label_email': __('Email'),
-                        'label_fullname': __('Full Name'),
-                        'label_jid': __('Jabber ID'),
-                        'label_nickname': __('Nickname'),
-                        'label_remove': __('Remove as contact'),
-                        'label_refresh': __('Refresh'),
-                        'label_role': __('Role'),
-                        'label_url': __('URL')
+                        'is_roster_contact': !_.isUndefined(this.model.contact)
                     }));
                 },
 

+ 58 - 11
src/converse-omemo.js

@@ -44,7 +44,7 @@
                 }
             });
         return {
-            'identity_key': parseInt(bundle_el.querySelector('identityKey').textContent, 10),
+            'identity_key': bundle_el.querySelector('identityKey').textContent,
             'signed_prekey': {
                 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
                 'public_key': signed_prekey_public_el.textContent,
@@ -65,6 +65,27 @@
 
         overrides: {
 
+            UserDetailsModal: {
+                events: {
+                    'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
+                },
+
+                initialize () {
+                    const { _converse } = this.__super__;
+                    const jid = this.model.get('jid');
+                    this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
+                    this.devicelist.devices.on('change:bundle', this.render, this);
+                    this.devicelist.devices.on('change:trusted', this.render, this);
+                    return this.__super__.initialize.apply(this, arguments);
+                },
+
+                toggleDeviceTrust (ev) {
+                    const radio = ev.target;
+                    const device = this.devicelist.devices.get(radio.getAttribute('name'));
+                    device.save('trusted', parseInt(radio.value, 10));
+                }
+            },
+
             ChatBox: {
 
                 getBundlesAndBuildSessions () {
@@ -77,7 +98,6 @@
                                     this.buildSessions(devices)
                                         .then(() => resolve(devices))
                                         .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
-
                                 }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
                             }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
                     }).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
@@ -194,6 +214,27 @@
 
             _converse.NUM_PREKEYS = 100; // Set here so that tests can override
 
+            function generateFingerprint (device) {
+                return new Promise((resolve, reject) => {
+                    device.getBundle().then((bundle) => {
+                        // 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);
+                                device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
+                                resolve();
+                            }).catch(reject);
+                    });
+                });
+            }
+
+            _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.getDevicesForContact = function (jid) {
                 return new Promise((resolve, reject) => {
@@ -405,14 +446,15 @@
                             'from': _converse.bare_jid,
                             'to': this.get('jid')
                         }).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
-                            .c('items', {'xmlns': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`});
+                            .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')}"]`, stanza).pop();
-                                const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop();
-                                this.save(parseBundle(bundle_el));
-                                resolve();
+                                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
@@ -479,7 +521,7 @@
                             (iq) => {
                                 _.forEach(
                                     iq.querySelectorAll('device'),
-                                    (dev) => this.devices.create({'id': dev.getAttribute('id')})
+                                    (dev) => this.devices.create({'id': dev.getAttribute('id'), 'jid': this.get('jid')})
                                 );
                                 resolve();
                             },
@@ -493,7 +535,7 @@
                      * server.
                      * https://xmpp.org/extensions/xep-0384.html#usecases-announcing
                      */
-                    this.devices.create({'id': device_id});
+                    this.devices.create({'id': device_id, 'jid': this.get('jid')});
                     return new Promise((resolve, reject) => {
                         const stanza = $iq({
                             'from': _converse.bare_jid,
@@ -589,7 +631,7 @@
                       jid = stanza.getAttribute('from'),
                       bundle_el = sizzle(`item > bundle`, items_el).pop(),
                       devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid}),
-                      device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id});
+                      device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id, 'jid': jid});
                 device.save({'bundle': parseBundle(bundle_el)});
             }
 
@@ -613,7 +655,7 @@
                     if (dev) {
                         dev.save({'active': true});
                     } else {
-                        devices.create({'id': device_id})
+                        devices.create({'id': device_id, 'jid': jid})
                     }
                 });
                 // Make sure our own device is on the list (i.e. if it was
@@ -661,6 +703,11 @@
             _converse.api.listen.on('statusInitialized', initOMEMO);
             _converse.api.listen.on('addClientFeatures',
                 () => _converse.api.disco.own.features.add(Strophe.NS.OMEMO_DEVICELIST+"notify"));
+
+            _converse.api.listen.on('userDetailsModalInitialized', (contact) => {
+                const jid = contact.get('jid');
+                _converse.getFingerprintsForContact(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
+            });
         }
     });
 }));

+ 44 - 13
src/templates/user_details_modal.html

@@ -1,39 +1,70 @@
-<div class="modal fade" id="user-profile-modal" tabindex="-1" role="dialog" aria-labelledby="user-profile-modal-label" aria-hidden="true">
+<div class="modal fade" id="user-details-modal" tabindex="-1" role="dialog" aria-labelledby="user-details-modal-label" aria-hidden="true">
     <div class="modal-dialog" role="document">
         <div class="modal-content">
             <div class="modal-header">
-                <h5 class="modal-title" id="user-profile-modal-label">{{{o.display_name}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">&times;</span></button>
+                <h5 class="modal-title" id="user-details-modal-label">{{{o.display_name}}}</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.__('Close')}}}"><span aria-hidden="true">&times;</span></button>
             </div>
             <div class="modal-body">
                 {[ if (o.image) { ]}
-                <img alt="{{{o.alt_profile_image}}}"
+                <img alt="{{{o.__('The User\'s Profile Image')}}}"
                     class="img-thumbnail avatar align-self-center mb-3"
                     height="100" width="100" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
                 {[ } ]}
                 {[ if (o.fullname) { ]}
-                <p><label>{{{o.label_fullname}}}:</label>&nbsp;{{{o.fullname}}}</p>
+                <p><label>{{{o.__('Full Name')}}}:</label>&nbsp;{{{o.fullname}}}</p>
                 {[ } ]}
-                <p><label>{{{o.label_jid}}}:</label>&nbsp;{{{o.jid}}}</p>
+                <p><label>{{{o.__('XMPP Address')}}}:</label>&nbsp;{{{o.jid}}}</p>
                 {[ if (o.nickname) { ]}
-                <p><label>{{{o.label_nickname}}}:</label>&nbsp;{{{o.nickname}}}</p>
+                <p><label>{{{o.__('Nickname')}}}:</label>&nbsp;{{{o.nickname}}}</p>
                 {[ } ]}
                 {[ if (o.url) { ]}
-                <p><label>{{{o.label_url}}}:</label>&nbsp;<a target="_blank" rel="noopener" href="{{{o.url}}}">{{{o.url}}}</a></p>
+                <p><label>{{{o.__('URL')}}}:</label>&nbsp;<a target="_blank" rel="noopener" href="{{{o.url}}}">{{{o.url}}}</a></p>
                 {[ } ]}
                 {[ if (o.email) { ]}
-                <p><label>{{{o.label_email}}}:</label>&nbsp;<a href="mailto:{{{o.email}}}">{{{o.email}}}</a></p>
+                <p><label>{{{o.__('Email')}}}:</label>&nbsp;<a href="mailto:{{{o.email}}}">{{{o.email}}}</a></p>
                 {[ } ]}
                 {[ if (o.role) { ]}
-                <p><label>{{{o.label_role}}}:</label>&nbsp;{{{o.role}}}</p>
+                <p><label>{{{o.__('Role')}}}:</label>&nbsp;{{{o.role}}}</p>
+                {[ } ]}
+
+                {[ if (o._converse.pluggable.plugins['converse-omemo'].enabled()) { ]}
+                    <hr>
+                    <ul class="list-group fingerprints">
+                        <li class="list-group-item active">{{{o.__('OMEMO Fingerprints')}}}</li>
+                        {[ if (!o.view.devicelist.devices) { ]}
+                            <li class="list-group-item"><span class="spinner fa fa-spinner centered"/></li>
+                        {[ } ]}
+                        {[ if (o.view.devicelist.devices) { ]}
+                            {[ o.view.devicelist.devices.each(function (device) { ]}
+                                {[ 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"
+                                                {[ if (device.get('trusted') !== -1) { ]} checked="checked" {[ } ]}>{{{o.__('Trusted')}}}
+                                        </label>
+                                        <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"
+                                                {[ if (device.get('trusted') === -1) { ]} checked="checked" {[ } ]}>{{{o.__('Untrusted')}}}
+                                        </label>
+                                    </div>
+                                    </form>
+                                </li>
+                                {[ } ]}
+                            {[ }); ]}
+                        {[ } ]}
+                    </ul>
                 {[ } ]}
             </div>
             <div class="modal-footer">
                 {[ if (o.allow_contact_removal && o.is_roster_contact) { ]}
-                    <button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>{{{o.label_remove}}}</button>
+                    <button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>{{{o.__('Remove as contact')}}}</button>
                 {[ } ]}
-                <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.label_refresh}}}</button>
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.label_close}}}</button>
+                <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.__('Refresh')}}}</button>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.__('Close')}}}</button>
             </div>
         </div>
     </div>

+ 16 - 0
src/utils/core.js

@@ -846,6 +846,22 @@
         return result;
     };
 
+    u.arrayBufferToHex = function (ab) {
+        const hexCodes = [];
+        const padding = '00000000';
+        const view = new window.DataView(ab);
+        for (var i = 0; i < view.byteLength; i += 4) {
+            // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
+            const value = view.getUint32(i)
+            // toString(16) will give the hex representation of the number without padding
+            const stringValue = value.toString(16)
+            // We use concatenation and slice for padding
+            const paddedValue = (padding + stringValue).slice(-padding.length)
+            hexCodes.push(paddedValue);
+        }
+        return hexCodes.join("");
+    };
+
     u.arrayBufferToString = function (ab) {
         var enc = new TextDecoder("utf-8");
         return enc.decode(new Uint8Array(ab));

Некоторые файлы не были показаны из-за большого количества измененных файлов