Commit 39f1fc4e authored by JC Brand's avatar JC Brand

Show fingerprints in the user details modal

updates #497
parent ce447e40
...@@ -2346,7 +2346,7 @@ ...@@ -2346,7 +2346,7 @@
--primary: #387592; --primary: #387592;
--secondary: #6c757d; --secondary: #6c757d;
--success: #3AA569; --success: #3AA569;
--info: #17a2b8; --info: #3AA569;
--warning: #ffc107; --warning: #ffc107;
--danger: #E77051; --danger: #E77051;
--light: #f8f9fa; --light: #f8f9fa;
...@@ -3594,24 +3594,24 @@ ...@@ -3594,24 +3594,24 @@
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); } box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-info { #conversejs .btn-info {
color: #fff; color: #fff;
background-color: #17a2b8; background-color: #3AA569;
border-color: #17a2b8; } border-color: #3AA569; }
#conversejs .btn-info:hover { #conversejs .btn-info:hover {
color: #fff; color: #fff;
background-color: #138496; background-color: #308957;
border-color: #117a8b; } border-color: #2d7f51; }
#conversejs .btn-info:focus, #conversejs .btn-info.focus { #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 { #conversejs .btn-info.disabled, #conversejs .btn-info:disabled {
color: #fff; color: #fff;
background-color: #17a2b8; background-color: #3AA569;
border-color: #17a2b8; } 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 { #conversejs .btn-info:not(:disabled):not(.disabled):active, #conversejs .btn-info:not(:disabled):not(.disabled).active, .show > #conversejs .btn-info.dropdown-toggle {
color: #fff; color: #fff;
background-color: #117a8b; background-color: #2d7f51;
border-color: #10707f; } 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 { #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 { #conversejs .btn-warning {
color: #212529; color: #212529;
background-color: #ffc107; background-color: #ffc107;
...@@ -3753,25 +3753,25 @@ ...@@ -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 { #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); } box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-outline-info { #conversejs .btn-outline-info {
color: #17a2b8; color: #3AA569;
background-color: transparent; background-color: transparent;
background-image: none; background-image: none;
border-color: #17a2b8; } border-color: #3AA569; }
#conversejs .btn-outline-info:hover { #conversejs .btn-outline-info:hover {
color: #fff; color: #fff;
background-color: #17a2b8; background-color: #3AA569;
border-color: #17a2b8; } border-color: #3AA569; }
#conversejs .btn-outline-info:focus, #conversejs .btn-outline-info.focus { #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 { #conversejs .btn-outline-info.disabled, #conversejs .btn-outline-info:disabled {
color: #17a2b8; color: #3AA569;
background-color: transparent; } 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 { #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; color: #fff;
background-color: #17a2b8; background-color: #3AA569;
border-color: #17a2b8; } 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 { #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 { #conversejs .btn-outline-warning {
color: #ffc107; color: #ffc107;
background-color: transparent; background-color: transparent;
...@@ -4582,11 +4582,11 @@ ...@@ -4582,11 +4582,11 @@
background-color: #2d7f51; } background-color: #2d7f51; }
#conversejs .badge-info { #conversejs .badge-info {
color: #fff; color: #fff;
background-color: #17a2b8; } background-color: #3AA569; }
#conversejs .badge-info[href]:hover, #conversejs .badge-info[href]:focus { #conversejs .badge-info[href]:hover, #conversejs .badge-info[href]:focus {
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
background-color: #117a8b; } background-color: #2d7f51; }
#conversejs .badge-warning { #conversejs .badge-warning {
color: #212529; color: #212529;
background-color: #ffc107; } background-color: #ffc107; }
...@@ -4658,13 +4658,13 @@ ...@@ -4658,13 +4658,13 @@
#conversejs .alert-success .alert-link { #conversejs .alert-success .alert-link {
color: #11301f; } color: #11301f; }
#conversejs .alert-info { #conversejs .alert-info {
color: #0c5460; color: #1e5637;
background-color: #d1ecf1; background-color: #d8ede1;
border-color: #bee5eb; } border-color: #c8e6d5; }
#conversejs .alert-info hr { #conversejs .alert-info hr {
border-top-color: #abdde5; } border-top-color: #b6dec8; }
#conversejs .alert-info .alert-link { #conversejs .alert-info .alert-link {
color: #062c33; } color: #11301f; }
#conversejs .alert-warning { #conversejs .alert-warning {
color: #856404; color: #856404;
background-color: #fff3cd; background-color: #fff3cd;
...@@ -4782,15 +4782,15 @@ ...@@ -4782,15 +4782,15 @@
background-color: #1e5637; background-color: #1e5637;
border-color: #1e5637; } border-color: #1e5637; }
#conversejs .list-group-item-info { #conversejs .list-group-item-info {
color: #0c5460; color: #1e5637;
background-color: #bee5eb; } background-color: #c8e6d5; }
#conversejs .list-group-item-info.list-group-item-action:hover, #conversejs .list-group-item-info.list-group-item-action:focus { #conversejs .list-group-item-info.list-group-item-action:hover, #conversejs .list-group-item-info.list-group-item-action:focus {
color: #0c5460; color: #1e5637;
background-color: #abdde5; } background-color: #b6dec8; }
#conversejs .list-group-item-info.list-group-item-action.active { #conversejs .list-group-item-info.list-group-item-action.active {
color: #fff; color: #fff;
background-color: #0c5460; background-color: #1e5637;
border-color: #0c5460; } border-color: #1e5637; }
#conversejs .list-group-item-warning { #conversejs .list-group-item-warning {
color: #856404; color: #856404;
background-color: #ffeeba; } background-color: #ffeeba; }
...@@ -5179,11 +5179,11 @@ ...@@ -5179,11 +5179,11 @@
#conversejs button.bg-success:focus { #conversejs button.bg-success:focus {
background-color: #2d7f51 !important; } background-color: #2d7f51 !important; }
#conversejs .bg-info { #conversejs .bg-info {
background-color: #17a2b8 !important; } background-color: #3AA569 !important; }
#conversejs a.bg-info:hover, #conversejs a.bg-info:focus, #conversejs a.bg-info:hover, #conversejs a.bg-info:focus,
#conversejs button.bg-info:hover, #conversejs button.bg-info:hover,
#conversejs button.bg-info:focus { #conversejs button.bg-info:focus {
background-color: #117a8b !important; } background-color: #2d7f51 !important; }
#conversejs .bg-warning { #conversejs .bg-warning {
background-color: #ffc107 !important; } background-color: #ffc107 !important; }
#conversejs a.bg-warning:hover, #conversejs a.bg-warning:focus, #conversejs a.bg-warning:hover, #conversejs a.bg-warning:focus,
...@@ -5239,7 +5239,7 @@ ...@@ -5239,7 +5239,7 @@
#conversejs .border-success { #conversejs .border-success {
border-color: #3AA569 !important; } border-color: #3AA569 !important; }
#conversejs .border-info { #conversejs .border-info {
border-color: #17a2b8 !important; } border-color: #3AA569 !important; }
#conversejs .border-warning { #conversejs .border-warning {
border-color: #ffc107 !important; } border-color: #ffc107 !important; }
#conversejs .border-danger { #conversejs .border-danger {
...@@ -6792,9 +6792,9 @@ ...@@ -6792,9 +6792,9 @@
#conversejs a.text-success:hover, #conversejs a.text-success:focus { #conversejs a.text-success:hover, #conversejs a.text-success:focus {
color: #2d7f51 !important; } color: #2d7f51 !important; }
#conversejs .text-info { #conversejs .text-info {
color: #17a2b8 !important; } color: #3AA569 !important; }
#conversejs a.text-info:hover, #conversejs a.text-info:focus { #conversejs a.text-info:hover, #conversejs a.text-info:focus {
color: #117a8b !important; } color: #2d7f51 !important; }
#conversejs .text-warning { #conversejs .text-warning {
color: #ffc107 !important; } color: #ffc107 !important; }
#conversejs a.text-warning:hover, #conversejs a.text-warning:focus { #conversejs a.text-warning:hover, #conversejs a.text-warning:focus {
...@@ -7213,6 +7213,9 @@ body.reset { ...@@ -7213,6 +7213,9 @@ body.reset {
@media screen and (max-height: 450px) { @media screen and (max-height: 450px) {
#conversejs { #conversejs {
left: 0; } } left: 0; } }
#conversejs .btn--small {
font-size: 80%;
font-weight: normal; }
#conversejs form .form-group { #conversejs form .form-group {
margin-bottom: 2em; } margin-bottom: 2em; }
#conversejs form .form-check-label { #conversejs form .form-check-label {
...@@ -7291,6 +7294,10 @@ body.reset { ...@@ -7291,6 +7294,10 @@ body.reset {
#conversejs #user-profile-modal label { #conversejs #user-profile-modal label {
font-weight: bold; } font-weight: bold; }
#conversejs .fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%; }
#conversejs .chatbox-navback { #conversejs .chatbox-navback {
display: none; } display: none; }
......
...@@ -61425,7 +61425,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -61425,7 +61425,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
checkForReservedNick() { checkForReservedNick() {
/* Check if the user has a bookmark with a saved nickanme /* Check if the user has a bookmark with a saved nickanme
* for this room, and if so use it. * for this groupchat, and if so use it.
* Otherwise delegate to the super method. * Otherwise delegate to the super method.
*/ */
const _converse = this.__super__._converse; const _converse = this.__super__._converse;
...@@ -61460,7 +61460,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -61460,7 +61460,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
setBookmarkState() { setBookmarkState() {
/* Set whether the room is bookmarked or not. /* Set whether the groupchat is bookmarked or not.
*/ */
const _converse = this.__super__._converse; const _converse = this.__super__._converse;
...@@ -61612,10 +61612,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -61612,10 +61612,10 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
openBookmarkedRoom(bookmark) { openBookmarkedRoom(bookmark) {
if (bookmark.get('autojoin')) { if (bookmark.get('autojoin')) {
const room = _converse.api.rooms.create(bookmark.get('jid'), bookmark.get('nick')); const groupchat = _converse.api.rooms.create(bookmark.get('jid'), bookmark.get('nick'));
if (!room.get('hidden')) { if (!groupchat.get('hidden')) {
room.trigger('show'); groupchat.trigger('show');
} }
} }
...@@ -61715,18 +61715,18 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -61715,18 +61715,18 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
markRoomAsBookmarked(bookmark) { markRoomAsBookmarked(bookmark) {
const room = _converse.chatboxes.get(bookmark.get('jid')); const groupchat = _converse.chatboxes.get(bookmark.get('jid'));
if (!_.isUndefined(room)) { if (!_.isUndefined(groupchat)) {
room.save('bookmarked', true); groupchat.save('bookmarked', true);
} }
}, },
markRoomAsUnbookmarked(bookmark) { markRoomAsUnbookmarked(bookmark) {
const room = _converse.chatboxes.get(bookmark.get('jid')); const groupchat = _converse.chatboxes.get(bookmark.get('jid'));
if (!_.isUndefined(room)) { if (!_.isUndefined(groupchat)) {
room.save('bookmarked', false); groupchat.save('bookmarked', false);
} }
}, },
...@@ -63329,24 +63329,42 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -63329,24 +63329,42 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.BootstrapModal.prototype.initialize.apply(this, arguments); _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('contactAdded', this.registerContactEventHandlers, this); this.model.on('contactAdded', this.registerContactEventHandlers, this);
this.model.on('change', this.render, this); // XXX: Leaky abstraction from converse-omemo
// In part, we're hampered by the fact that we can't
// have sub-views inside a VDOMView.
// If we did, we could put the OMEMO part of this modal
// inside another view and have it render as a sub-view.
// However, for this we'd need some kind of registry and
// way to look up sub-views by tag from the template (which
// I assume is what for example vue.js does).
this.has_omemo = _converse.pluggable.plugins['converse-omemo'].enabled();
if (this.has_omemo) {
const jid = this.model.get('jid');
this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({
'jid': jid
});
this.devicelist.devices.on('change:fingerprint', this.render, this);
} else {
this.devicelist = {};
}
this.registerContactEventHandlers(); this.registerContactEventHandlers();
_converse.emit('userDetailsModalInitialized', this.model);
}, },
toHTML() { toHTML() {
return tpl_user_details_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), { return tpl_user_details_modal(_.extend(this.model.toJSON(), this.model.vcard.toJSON(), {
'_': _,
'__': __,
'has_omemo': this.has_omemo,
'devicelist': this.devicelist,
'allow_contact_removal': _converse.allow_contact_removal, 'allow_contact_removal': _converse.allow_contact_removal,
'alt_profile_image': __("The User's Profile Image"), 'alt_profile_image': __("The User's Profile Image"),
'display_name': this.model.getDisplayName(), 'display_name': this.model.getDisplayName(),
'is_roster_contact': !_.isUndefined(this.model.contact), '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')
})); }));
}, },
...@@ -69545,25 +69563,25 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69545,25 +69563,25 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
/* http://xmpp.org/extensions/xep-0045.html /* http://xmpp.org/extensions/xep-0045.html
* ---------------------------------------- * ----------------------------------------
* 100 message Entering a room Inform user that any occupant is allowed to see the user's full JID * 100 message Entering a groupchat Inform user that any occupant is allowed to see the user's full JID
* 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the room * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the groupchat
* 102 message Configuration change Inform occupants that room now shows unavailable members * 102 message Configuration change Inform occupants that groupchat now shows unavailable members
* 103 message Configuration change Inform occupants that room now does not show unavailable members * 103 message Configuration change Inform occupants that groupchat now does not show unavailable members
* 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred * 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred
* 110 presence Any room presence Inform user that presence refers to one of its own room occupants * 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants
* 170 message or initial presence Configuration change Inform occupants that room logging is now enabled * 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled
* 171 message Configuration change Inform occupants that room logging is now disabled * 171 message Configuration change Inform occupants that groupchat logging is now disabled
* 172 message Configuration change Inform occupants that the room is now non-anonymous * 172 message Configuration change Inform occupants that the groupchat is now non-anonymous
* 173 message Configuration change Inform occupants that the room is now semi-anonymous * 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous
* 174 message Configuration change Inform occupants that the room is now fully-anonymous * 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous
* 201 presence Entering a room Inform user that a new room has been created * 201 presence Entering a groupchat Inform user that a new groupchat has been created
* 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick * 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick
* 301 presence Removal from room Inform user that he or she has been banned from the room * 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat
* 303 presence Exiting a room Inform all occupants of new room nickname * 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname
* 307 presence Removal from room Inform user that he or she has been kicked from the room * 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat
* 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change * 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change
* 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member
* 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown
*/ */
...@@ -69611,12 +69629,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69611,12 +69629,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}; };
function insertRoomInfo(el, stanza) { function insertRoomInfo(el, stanza) {
/* Insert room info (based on returned #disco IQ stanza) /* Insert groupchat info (based on returned #disco IQ stanza)
* *
* Parameters: * Parameters:
* (HTMLElement) el: The HTML DOM element that should * (HTMLElement) el: The HTML DOM element that should
* contain the info. * contain the info.
* (XMLElement) stanza: The IQ stanza containing the room * (XMLElement) stanza: The IQ stanza containing the groupchat
* info. * info.
*/ */
// All MUC features found here: http://xmpp.org/registrar/disco-features.html // All MUC features found here: http://xmpp.org/registrar/disco-features.html
...@@ -69656,7 +69674,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69656,7 +69674,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
function toggleRoomInfo(ev) { function toggleRoomInfo(ev) {
/* Show/hide extra information about a room in a listing. */ /* Show/hide extra information about a groupchat in a listing. */
const parent_el = u.ancestor(ev.target, '.room-item'), const parent_el = u.ancestor(ev.target, '.room-item'),
div_el = parent_el.querySelector('div.room-info'); div_el = parent_el.querySelector('div.room-info');
...@@ -69689,7 +69707,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69689,7 +69707,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return tpl_list_chatrooms_modal(_.extend(this.model.toJSON(), { return tpl_list_chatrooms_modal(_.extend(this.model.toJSON(), {
'heading_list_chatrooms': __('Query for Groupchats'), 'heading_list_chatrooms': __('Query for Groupchats'),
'label_server_address': __('Server address'), 'label_server_address': __('Server address'),
'label_query': __('Show rooms'), 'label_query': __('Show groupchats'),
'server_placeholder': __('conference.example.org') 'server_placeholder': __('conference.example.org')
})); }));
}, },
...@@ -69722,12 +69740,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69722,12 +69740,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
}, },
roomStanzaItemToHTMLElement(room) { roomStanzaItemToHTMLElement(groupchat) {
const name = Strophe.unescapeNode(room.getAttribute('name') || room.getAttribute('jid')); const name = Strophe.unescapeNode(groupchat.getAttribute('name') || groupchat.getAttribute('jid'));
const div = document.createElement('div'); const div = document.createElement('div');
div.innerHTML = tpl_room_item({ div.innerHTML = tpl_room_item({
'name': Strophe.xmlunescape(name), 'name': Strophe.xmlunescape(name),
'jid': room.getAttribute('jid'), 'jid': groupchat.getAttribute('jid'),
'open_title': __('Click to open this groupchat'), 'open_title': __('Click to open this groupchat'),
'info_title': __('Show more information on this groupchat') 'info_title': __('Show more information on this groupchat')
}); });
...@@ -69741,7 +69759,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69741,7 +69759,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
informNoRoomsFound() { informNoRoomsFound() {
const chatrooms_el = this.el.querySelector('.available-chatrooms'); const chatrooms_el = this.el.querySelector('.available-chatrooms');
chatrooms_el.innerHTML = tpl_rooms_results({ chatrooms_el.innerHTML = tpl_rooms_results({
'feedback_text': __('No rooms found') 'feedback_text': __('No groupchats found')
}); });
const input_el = this.el.querySelector('input[name="server"]'); const input_el = this.el.querySelector('input[name="server"]');
input_el.classList.remove('hidden'); input_el.classList.remove('hidden');
...@@ -69750,7 +69768,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69750,7 +69768,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
onRoomsFound(iq) { onRoomsFound(iq) {
/* Handle the IQ stanza returned from the server, containing /* Handle the IQ stanza returned from the server, containing
* all its public rooms. * all its public groupchats.
*/ */
const available_chatrooms = this.el.querySelector('.available-chatrooms'); const available_chatrooms = this.el.querySelector('.available-chatrooms');
this.rooms = iq.querySelectorAll('query item'); this.rooms = iq.querySelectorAll('query item');
...@@ -69759,7 +69777,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69759,7 +69777,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// For translators: %1$s is a variable and will be // For translators: %1$s is a variable and will be
// replaced with the XMPP server name // replaced with the XMPP server name
available_chatrooms.innerHTML = tpl_rooms_results({ available_chatrooms.innerHTML = tpl_rooms_results({
'feedback_text': __('Rooms found:') 'feedback_text': __('groupchats found:')
}); });
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
...@@ -69777,7 +69795,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69777,7 +69795,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
updateRoomsList() { updateRoomsList() {
/* Send an IQ stanza to the server asking for all rooms /* Send an IQ stanza to the server asking for all groupchats
*/ */
_converse.connection.sendIQ($iq({ _converse.connection.sendIQ($iq({
to: this.model.get('muc_domain'), to: this.model.get('muc_domain'),
...@@ -69873,7 +69891,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69873,7 +69891,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
_converse.ChatRoomView = _converse.ChatBoxView.extend({ _converse.ChatRoomView = _converse.ChatBoxView.extend({
/* Backbone.NativeView which renders a chat room, based upon the view /* Backbone.NativeView which renders a groupchat, based upon the view
* for normal one-on-one chat boxes. * for normal one-on-one chat boxes.
*/ */
length: 300, length: 300,
...@@ -69965,12 +69983,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -69965,12 +69983,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
renderHeading() { renderHeading() {
/* Render the heading UI of the chat room. */ /* Render the heading UI of the groupchat. */
this.el.querySelector('.chat-head-chatroom').innerHTML = this.generateHeadingHTML(); this.el.querySelector('.chat-head-chatroom').innerHTML = this.generateHeadingHTML();
}, },
renderChatArea() { renderChatArea() {
/* Render the UI container in which chat room messages will appear. /* Render the UI container in which groupchat messages will appear.
*/ */
if (_.isNull(this.el.querySelector('.chat-area'))) { if (_.isNull(this.el.querySelector('.chat-area'))) {
const container_el = this.el.querySelector('.chatroom-body'); const container_el = this.el.querySelector('.chatroom-body');
...@@ -70098,7 +70116,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70098,7 +70116,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
close(ev) { close(ev) {
/* Close this chat box, which implies leaving the room as /* Close this chat box, which implies leaving the groupchat as
* well. * well.
*/ */
this.hide(); this.hide();
...@@ -70191,13 +70209,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70191,13 +70209,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
}, },
modifyRole(room, nick, role, reason, onSuccess, onError) { modifyRole(groupchat, nick, role, reason, onSuccess, onError) {
const item = $build("item", { const item = $build("item", {
nick, nick,
role role
}); });
const iq = $iq({ const iq = $iq({
to: room, to: groupchat,
type: "set" type: "set"
}).c("query", { }).c("query", {
xmlns: Strophe.NS.MUC_ADMIN xmlns: Strophe.NS.MUC_ADMIN
...@@ -70211,7 +70229,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70211,7 +70229,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
validateRoleChangeCommand(command, args) { validateRoleChangeCommand(command, args) {
/* Check that a command to change a chat room user's role or /* Check that a command to change a groupchat user's role or
* affiliation has anough arguments. * affiliation has anough arguments.
*/ */
// TODO check if first argument is valid // TODO check if first argument is valid
...@@ -70374,7 +70392,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70374,7 +70392,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
registerHandlers() { registerHandlers() {
/* Register presence and message handlers for this chat /* Register presence and message handlers for this chat
* room * groupchat
*/ */
// XXX: Ideally this can be refactored out so that we don't // XXX: Ideally this can be refactored out so that we don't
// need to do stanza processing inside the views in this // need to do stanza processing inside the views in this
...@@ -70414,12 +70432,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70414,12 +70432,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
join(nick, password) { join(nick, password) {
/* Join the chat room. /* Join the groupchat.
* *
* Parameters: * Parameters:
* (String) nick: The user's nickname * (String) nick: The user's nickname
* (String) password: Optional password, if required by * (String) password: Optional password, if required by
* the room. * the groupchat.
*/ */
if (!nick && !this.model.get('nick')) { if (!nick && !this.model.get('nick')) {
this.checkForReservedNick(); this.checkForReservedNick();
...@@ -70432,13 +70450,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70432,13 +70450,13 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
renderConfigurationForm(stanza) { renderConfigurationForm(stanza) {
/* Renders a form given an IQ stanza containing the current /* Renders a form given an IQ stanza containing the current
* room configuration. * groupchat configuration.
* *
* Returns a promise which resolves once the user has * Returns a promise which resolves once the user has
* either submitted the form, or canceled it. * either submitted the form, or canceled it.
* *
* Parameters: * Parameters:
* (XMLElement) stanza: The IQ stanza containing the room * (XMLElement) stanza: The IQ stanza containing the groupchat
* config. * config.
*/ */
const container_el = this.el.querySelector('.chatroom-body'); const container_el = this.el.querySelector('.chatroom-body');
...@@ -70491,7 +70509,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70491,7 +70509,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
getAndRenderConfigurationForm(ev) { getAndRenderConfigurationForm(ev) {
/* Start the process of configuring a chat room, either by /* Start the process of configuring a groupchat, either by
* rendering a configuration form, or by auto-configuring * rendering a configuration form, or by auto-configuring
* based on the "roomconfig" data stored on the * based on the "roomconfig" data stored on the
* Backbone.Model. * Backbone.Model.
...@@ -70511,7 +70529,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70511,7 +70529,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
submitNickname(ev) { submitNickname(ev) {
/* Get the nickname value from the form and then join the /* Get the nickname value from the form and then join the
* chat room with it. * groupchat with it.
*/ */
ev.preventDefault(); ev.preventDefault();
const nick_el = ev.target.nick; const nick_el = ev.target.nick;
...@@ -70530,7 +70548,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70530,7 +70548,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
checkForReservedNick() { checkForReservedNick() {
/* User service-discovery to ask the XMPP server whether /* User service-discovery to ask the XMPP server whether
* this user has a reserved nickname for this room. * this user has a reserved nickname for this groupchat.
* If so, we'll use that, otherwise we render the nickname form. * If so, we'll use that, otherwise we render the nickname form.
*/ */
this.showSpinner(); this.showSpinner();
...@@ -70541,7 +70559,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70541,7 +70559,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
/* We've received an IQ response from the server which /* We've received an IQ response from the server which
* might contain the user's reserved nickname. * might contain the user's reserved nickname.
* If no nickname is found we either render a form for * If no nickname is found we either render a form for
* them to specify one, or we try to join the room with the * them to specify one, or we try to join the groupchat with the
* node of the user's JID. * node of the user's JID.
* *
* Parameters: * Parameters:
...@@ -70923,7 +70941,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70923,7 +70941,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
showErrorMessageFromPresence(presence) { showErrorMessageFromPresence(presence) {
// We didn't enter the room, so we must remove it from the MUC add-on // We didn't enter the groupchat, so we must remove it from the MUC add-on
const error = presence.querySelector('error'); const error = presence.querySelector('error');
if (error.getAttribute('type') === 'auth') { if (error.getAttribute('type') === 'auth') {
...@@ -70940,7 +70958,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70940,7 +70958,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
} else if (error.getAttribute('type') === 'cancel') { } else if (error.getAttribute('type') === 'cancel') {
if (!_.isNull(error.querySelector('not-allowed'))) { if (!_.isNull(error.querySelector('not-allowed'))) {
this.showDisconnectMessages(__('You are not allowed to create new rooms.')); this.showDisconnectMessages(__('You are not allowed to create new groupchats.'));
} else if (!_.isNull(error.querySelector('not-acceptable'))) { } else if (!_.isNull(error.querySelector('not-acceptable'))) {
this.showDisconnectMessages(__("Your nickname doesn't conform to this groupchat's policies.")); this.showDisconnectMessages(__("Your nickname doesn't conform to this groupchat's policies."));
} else if (!_.isNull(error.querySelector('conflict'))) { } else if (!_.isNull(error.querySelector('conflict'))) {
...@@ -70964,7 +70982,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -70964,7 +70982,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
renderAfterTransition() { renderAfterTransition() {
/* Rerender the room after some kind of transition. For /* Rerender the groupchat after some kind of transition. For
* example after the spinner has been removed or after a * example after the spinner has been removed or after a
* form has been submitted and removed. * form has been submitted and removed.
*/ */
...@@ -71038,8 +71056,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71038,8 +71056,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
render() { render() {
this.el.innerHTML = tpl_room_panel({ this.el.innerHTML = tpl_room_panel({
'heading_chatrooms': __('Groupchats'), 'heading_chatrooms': __('Groupchats'),
'title_new_room': __('Add a new room'), 'title_new_room': __('Add a new groupchat'),
'title_list_rooms': __('Query for rooms') 'title_list_rooms': __('Query for groupchats')
}); });
return this; return this;
}, },
...@@ -71214,7 +71232,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71214,7 +71232,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
promptForInvite(suggestion) { promptForInvite(suggestion) {
const reason = prompt(__('You are about to invite %1$s to the chat room "%2$s". ' + 'You may optionally include a message, explaining the reason for the invitation.', suggestion.text.label, this.model.get('id'))); const reason = prompt(__('You are about to invite %1$s to the groupchat "%2$s". ' + 'You may optionally include a message, explaining the reason for the invitation.', suggestion.text.label, this.model.get('id')));
if (reason !== null) { if (reason !== null) {
this.chatroomview.model.directInvite(suggestion.text.value, reason); this.chatroomview.model.directInvite(suggestion.text.value, reason);
...@@ -71294,7 +71312,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71294,7 +71312,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
function setMUCDomainFromDisco(controlboxview) { function setMUCDomainFromDisco(controlboxview) {
/* Check whether service discovery for the user's domain /* Check whether service discovery for the user's domain
* returned MUC information and use that to automatically * returned MUC information and use that to automatically
* set the MUC domain for the "Rooms" panel of the controlbox. * set the MUC domain in the "Add groupchat" modal.
*/ */
function featureAdded(feature) { function featureAdded(feature) {
if (feature.get('var') === Strophe.NS.MUC && f.includes('conference', feature.entity.identities.pluck('category'))) { if (feature.get('var') === Strophe.NS.MUC && f.includes('conference', feature.entity.identities.pluck('category'))) {
...@@ -71344,7 +71362,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71344,7 +71362,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
function reconnectToChatRooms() { function reconnectToChatRooms() {
/* Upon a reconnection event from converse, join again /* Upon a reconnection event from converse, join again
* all the open chat rooms. * all the open groupchats.
*/ */
_converse.chatboxviews.each(function (view) { _converse.chatboxviews.each(function (view) {
if (view.model.get('type') === converse.CHATROOMS_TYPE) { if (view.model.get('type') === converse.CHATROOMS_TYPE) {
...@@ -71444,12 +71462,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71444,12 +71462,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
// //
// New functions which don't exist yet can also be added. // New functions which don't exist yet can also be added.
tearDown() { tearDown() {
const rooms = this.chatboxes.where({ const groupchats = this.chatboxes.where({
'type': converse.CHATROOMS_TYPE 'type': converse.CHATROOMS_TYPE
}); });
_.each(rooms, function (room) { _.each(groupchats, function (groupchat) {
u.safeSave(room, { u.safeSave(groupchat, {
'connection_status': converse.ROOMSTATUS.DISCONNECTED 'connection_status': converse.ROOMSTATUS.DISCONNECTED
}); });
}); });
...@@ -71513,7 +71531,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71513,7 +71531,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.router.route('converse/room?jid=:jid', openRoom); _converse.router.route('converse/room?jid=:jid', openRoom);
_converse.openChatRoom = function (jid, settings, bring_to_foreground) { _converse.openChatRoom = function (jid, settings, bring_to_foreground) {
/* Opens a chat room, making sure that certain attributes /* Opens a groupchat, making sure that certain attributes
* are correct, for example that the "type" is set to * are correct, for example that the "type" is set to
* "chatroom". * "chatroom".
*/ */
...@@ -71563,7 +71581,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71563,7 +71581,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
registerHandlers() { registerHandlers() {
/* Register presence and message handlers for this chat /* Register presence and message handlers for this chat
* room * groupchat
*/ */
const room_jid = this.get('jid'); const room_jid = this.get('jid');
this.removeHandlers(); this.removeHandlers();
...@@ -71588,7 +71606,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71588,7 +71606,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
removeHandlers() { removeHandlers() {
/* Remove the presence and message handlers that were /* Remove the presence and message handlers that were
* registered for this chat room. * registered for this groupchat.
*/ */
if (this.message_handler) { if (this.message_handler) {
_converse.connection.deleteHandler(this.message_handler); _converse.connection.deleteHandler(this.message_handler);
...@@ -71627,12 +71645,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71627,12 +71645,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
join(nick, password) { join(nick, password) {
/* Join the chat room. /* Join the groupchat.
* *
* Parameters: * Parameters:
* (String) nick: The user's nickname * (String) nick: The user's nickname
* (String) password: Optional password, if required by * (String) password: Optional password, if required by
* the room. * the groupchat.
*/ */
nick = nick ? nick : this.get('nick'); nick = nick ? nick : this.get('nick');
...@@ -71641,7 +71659,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71641,7 +71659,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) { if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) {
// We have restored a chat room from session storage, // We have restored a groupchat from session storage,
// so we don't send out a presence stanza again. // so we don't send out a presence stanza again.
return this; return this;
} }
...@@ -71667,7 +71685,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71667,7 +71685,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
leave(exit_msg) { leave(exit_msg) {
/* Leave the chat room. /* Leave the groupchat.
* *
* Parameters: * Parameters:
* (String) exit_msg: Optional message to indicate your * (String) exit_msg: Optional message to indicate your
...@@ -71716,14 +71734,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71716,14 +71734,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
getRoomFeatures() { getRoomFeatures() {
/* Fetch the room disco info, parse it and then save it. /* Fetch the groupchat disco info, parse it and then save it.
*/ */
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_converse.api.disco.info(this.get('jid'), null).then(stanza => { _converse.api.disco.info(this.get('jid'), null).then(stanza => {
this.parseRoomFeatures(stanza); this.parseRoomFeatures(stanza);
resolve(); resolve();
}).catch(err => { }).catch(err => {
_converse.log("Could not parse the room features", Strophe.LogLevel.WARN); _converse.log("Could not parse the groupchat features", Strophe.LogLevel.WARN);
_converse.log(err, Strophe.LogLevel.WARN); _converse.log(err, Strophe.LogLevel.WARN);
...@@ -71734,12 +71752,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71734,12 +71752,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
getRoomJIDAndNick(nick) { getRoomJIDAndNick(nick) {
/* Utility method to construct the JID for the current user /* Utility method to construct the JID for the current user
* as occupant of the room. * as occupant of the groupchat.
* *
* This is the room JID, with the user's nick added at the * This is the groupchat JID, with the user's nick added at the
* end. * end.
* *
* For example: room@conference.example.org/nickname * For example: groupchat@conference.example.org/nickname
*/ */
if (nick) { if (nick) {
this.save({ this.save({
...@@ -71749,8 +71767,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71749,8 +71767,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
nick = this.get('nick'); nick = this.get('nick');
} }
const room = this.get('jid'); const groupchat = this.get('jid');
const jid = Strophe.getBareJidFromJid(room); const jid = Strophe.getBareJidFromJid(groupchat);
return jid + (nick !== null ? `/${nick}` : ""); return jid + (nick !== null ? `/${nick}` : "");
}, },
...@@ -71790,7 +71808,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71790,7 +71808,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
* (String) reason - Optional reason for the invitation * (String) reason - Optional reason for the invitation
*/ */
if (this.get('membersonly')) { if (this.get('membersonly')) {
// When inviting to a members-only room, we first add // When inviting to a members-only groupchat, we first add
// the person to the member list by giving them an // the person to the member list by giving them an
// affiliation of 'member' (if they're not affiliated // affiliation of 'member' (if they're not affiliated
// already), otherwise they won't be able to join. // already), otherwise they won't be able to join.
...@@ -71835,7 +71853,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71835,7 +71853,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
parseRoomFeatures(iq) { parseRoomFeatures(iq) {
/* Parses an IQ stanza containing the room's features. /* Parses an IQ stanza containing the groupchat's features.
* *
* See http://xmpp.org/extensions/xep-0045.html#disco-roominfo * See http://xmpp.org/extensions/xep-0045.html#disco-roominfo
* *
...@@ -71882,7 +71900,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71882,7 +71900,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
requestMemberList(affiliation) { requestMemberList(affiliation) {
/* Send an IQ stanza to the server, asking it for the /* Send an IQ stanza to the server, asking it for the
* member-list of this room. * member-list of this groupchat.
* *
* See: http://xmpp.org/extensions/xep-0045.html#modifymember * See: http://xmpp.org/extensions/xep-0045.html#modifymember
* *
...@@ -71941,7 +71959,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71941,7 +71959,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
saveConfiguration(form) { saveConfiguration(form) {
/* Submit the room configuration form by sending an IQ /* Submit the groupchat configuration form by sending an IQ
* stanza to the server. * stanza to the server.
* *
* Returns a promise which resolves once the XMPP server * Returns a promise which resolves once the XMPP server
...@@ -71961,7 +71979,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -71961,7 +71979,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
autoConfigureChatRoom() { autoConfigureChatRoom() {
/* Automatically configure room based on this model's /* Automatically configure groupchat based on this model's
* 'roomconfig' data. * 'roomconfig' data.
* *
* Returns a promise which resolves once a response IQ has * Returns a promise which resolves once a response IQ has
...@@ -72008,7 +72026,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72008,7 +72026,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
fetchRoomConfiguration() { fetchRoomConfiguration() {
/* Send an IQ stanza to fetch the room configuration data. /* Send an IQ stanza to fetch the groupchat configuration data.
* Returns a promise which resolves once the response IQ * Returns a promise which resolves once the response IQ
* has been received. * has been received.
*/ */
...@@ -72023,17 +72041,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72023,17 +72041,17 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
sendConfiguration(config, callback, errback) { sendConfiguration(config, callback, errback) {
/* Send an IQ stanza with the room configuration. /* Send an IQ stanza with the groupchat configuration.
* *
* Parameters: * Parameters:
* (Array) config: The room configuration * (Array) config: The groupchat configuration
* (Function) callback: Callback upon succesful IQ response * (Function) callback: Callback upon succesful IQ response
* The first parameter passed in is IQ containing the * The first parameter passed in is IQ containing the
* room configuration. * groupchat configuration.
* The second is the response IQ from the server. * The second is the response IQ from the server.
* (Function) errback: Callback upon error IQ response * (Function) errback: Callback upon error IQ response
* The first parameter passed in is IQ containing the * The first parameter passed in is IQ containing the
* room configuration. * groupchat configuration.
* The second is the response IQ from the server. * The second is the response IQ from the server.
*/ */
const iq = $iq({ const iq = $iq({
...@@ -72113,7 +72131,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72113,7 +72131,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
setAffiliations(members) { setAffiliations(members) {
/* Send IQ stanzas to the server to modify the /* Send IQ stanzas to the server to modify the
* affiliations in this room. * affiliations in this groupchat.
* *
* See: http://xmpp.org/extensions/xep-0045.html#modifymember * See: http://xmpp.org/extensions/xep-0045.html#modifymember
* *
...@@ -72165,7 +72183,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72165,7 +72183,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
checkForReservedNick(callback, errback) { checkForReservedNick(callback, errback) {
/* Use service-discovery to ask the XMPP server whether /* Use service-discovery to ask the XMPP server whether
* this user has a reserved nickname for this room. * this user has a reserved nickname for this groupchat.
* If so, we'll use that, otherwise we render the nickname form. * If so, we'll use that, otherwise we render the nickname form.
* *
* Parameters: * Parameters:
...@@ -72302,7 +72320,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72302,7 +72320,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}, },
onMessage(stanza) { onMessage(stanza) {
/* Handler for all MUC messages sent to this chat room. /* Handler for all MUC messages sent to this groupchat.
* *
* Parameters: * Parameters:
* (XMLElement) stanza: The message stanza. * (XMLElement) stanza: The message stanza.
...@@ -72380,14 +72398,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72380,14 +72398,14 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
/* Handles a received presence relating to the current /* Handles a received presence relating to the current
* user. * user.
* *
* For locked rooms (which are by definition "new"), the * For locked groupchats (which are by definition "new"), the
* room will either be auto-configured or created instantly * groupchat will either be auto-configured or created instantly
* (with default config) or a configuration room will be * (with default config) or a configuration groupchat will be
* rendered. * rendered.
* *
* If the room is not locked, then the room will be * If the groupchat is not locked, then the groupchat will be
* auto-configured only if applicable and if the current * auto-configured only if applicable and if the current
* user is the room's owner. * user is the groupchat's owner.
* *
* Parameters: * Parameters:
* (XMLElement) pres: The stanza * (XMLElement) pres: The stanza
...@@ -72403,11 +72421,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72403,11 +72421,11 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
this.saveConfiguration().then(this.getRoomFeatures.bind(this)); this.saveConfiguration().then(this.getRoomFeatures.bind(this));
} else { } else {
this.trigger('configurationNeeded'); this.trigger('configurationNeeded');
return; // We haven't yet entered the room, so bail here. return; // We haven't yet entered the groupchat, so bail here.
} }
} else if (!this.get('features_fetched')) { } else if (!this.get('features_fetched')) {
// The features for this room weren't fetched. // The features for this groupchat weren't fetched.
// That must mean it's a new room without locking // That must mean it's a new groupchat without locking
// (in which case Prosody doesn't send a 201 status), // (in which case Prosody doesn't send a 201 status),
// otherwise the features would have been fetched in // otherwise the features would have been fetched in
// the "initialize" method already. // the "initialize" method already.
...@@ -72598,7 +72616,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72598,7 +72616,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
_converse.onDirectMUCInvitation = function (message) { _converse.onDirectMUCInvitation = function (message) {
/* A direct MUC invitation to join a room has been received /* A direct MUC invitation to join a groupchat has been received
* See XEP-0249: Direct MUC invitations. * See XEP-0249: Direct MUC invitations.
* *
* Parameters: * Parameters:
...@@ -72620,9 +72638,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72620,9 +72638,9 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
contact = contact ? contact.get('fullname') : Strophe.getNodeFromJid(from); contact = contact ? contact.get('fullname') : Strophe.getNodeFromJid(from);
if (!reason) { if (!reason) {
result = confirm(__("%1$s has invited you to join a chat room: %2$s", contact, room_jid)); result = confirm(__("%1$s has invited you to join a groupchat: %2$s", contact, room_jid));
} else { } else {
result = confirm(__('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"', contact, room_jid, reason)); result = confirm(__('%1$s has invited you to join a groupchat: %2$s, and left the following reason: "%3$s"', contact, room_jid, reason));
} }
} }
...@@ -72664,24 +72682,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72664,24 +72682,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}; };
function autoJoinRooms() { function autoJoinRooms() {
/* Automatically join chat rooms, based on the /* Automatically join groupchats, based on the
* "auto_join_rooms" configuration setting, which is an array * "auto_join_rooms" configuration setting, which is an array
* of strings (room JIDs) or objects (with room JID and other * of strings (groupchat JIDs) or objects (with groupchat JID and other
* settings). * settings).
*/ */
_.each(_converse.auto_join_rooms, function (room) { _.each(_converse.auto_join_rooms, function (groupchat) {
if (_converse.chatboxes.where({ if (_converse.chatboxes.where({
'jid': room 'jid': groupchat
}).length) { }).length) {
return; return;
} }
if (_.isString(room)) { if (_.isString(groupchat)) {
_converse.api.rooms.open(room); _converse.api.rooms.open(groupchat);
} else if (_.isObject(room)) { } else if (_.isObject(groupchat)) {
_converse.api.rooms.open(room.jid, room.nick); _converse.api.rooms.open(groupchat.jid, groupchat.nick);
} else { } else {
_converse.log('Invalid room criteria specified for "auto_join_rooms"', Strophe.LogLevel.ERROR); _converse.log('Invalid groupchat criteria specified for "auto_join_rooms"', Strophe.LogLevel.ERROR);
} }
}); });
...@@ -72689,7 +72707,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72689,7 +72707,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
} }
function disconnectChatRooms() { function disconnectChatRooms() {
/* When disconnecting, mark all chat rooms as /* When disconnecting, mark all groupchats as
* disconnected, so that they will be properly entered again * disconnected, so that they will be properly entered again
* when fetched from session storage. * when fetched from session storage.
*/ */
...@@ -72734,7 +72752,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -72734,7 +72752,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
/************************ END Event Handlers ************************/ /************************ END Event Handlers ************************/
/************************ BEGIN API ************************/ /************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms. // We extend the default converse.js API to add methods specific to MUC groupchats.
_.extend(_converse.api, { _.extend(_converse.api, {
...@@ -73220,7 +73238,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73220,7 +73238,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
return { return {
'identity_key': parseInt(bundle_el.querySelector('identityKey').textContent, 10), 'identity_key': bundle_el.querySelector('identityKey').textContent,
'signed_prekey': { 'signed_prekey': {
'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10), 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
'public_key': signed_prekey_public_el.textContent, 'public_key': signed_prekey_public_el.textContent,
...@@ -73373,6 +73391,25 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73373,6 +73391,25 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.NUM_PREKEYS = 100; // Set here so that tests can override _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);
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) { _converse.getDevicesForContact = function (jid) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
_converse.api.waitUntil('OMEMOInitialized').then(() => { _converse.api.waitUntil('OMEMOInitialized').then(() => {
...@@ -73610,14 +73647,15 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73610,14 +73647,15 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}).c('pubsub', { }).c('pubsub', {
'xmlns': Strophe.NS.PUBSUB 'xmlns': Strophe.NS.PUBSUB
}).c('items', { }).c('items', {
'xmlns': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}` 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`
}); });
_converse.connection.sendIQ(stanza, iq => { _converse.connection.sendIQ(stanza, iq => {
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, stanza).pop(); const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(); bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
this.save(parseBundle(bundle_el)); bundle = parseBundle(bundle_el);
resolve(); this.save('bundle', bundle);
resolve(bundle);
}, reject, _converse.IQ_TIMEOUT); }, reject, _converse.IQ_TIMEOUT);
}); });
}, },
...@@ -73677,7 +73715,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73677,7 +73715,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.connection.sendIQ(stanza, iq => { _converse.connection.sendIQ(stanza, iq => {
_.forEach(iq.querySelectorAll('device'), dev => this.devices.create({ _.forEach(iq.querySelectorAll('device'), dev => this.devices.create({
'id': dev.getAttribute('id') 'id': dev.getAttribute('id'),
'jid': this.get('jid')
})); }));
resolve(); resolve();
...@@ -73691,7 +73730,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73691,7 +73730,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
* https://xmpp.org/extensions/xep-0384.html#usecases-announcing * https://xmpp.org/extensions/xep-0384.html#usecases-announcing
*/ */
this.devices.create({ this.devices.create({
'id': device_id 'id': device_id,
'jid': this.get('jid')
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stanza = $iq({ const stanza = $iq({
...@@ -73807,7 +73847,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73807,7 +73847,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
'jid': jid 'jid': jid
}), }),
device = devicelist.devices.get(device_id) || devicelist.devices.create({ device = devicelist.devices.get(device_id) || devicelist.devices.create({
'id': device_id 'id': device_id,
'jid': jid
}); });
device.save({ device.save({
...@@ -73842,7 +73883,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73842,7 +73883,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
}); });
} else { } else {
devices.create({ devices.create({
'id': device_id 'id': device_id,
'jid': jid
}); });
} }
}); // Make sure our own device is on the list (i.e. if it was }); // Make sure our own device is on the list (i.e. if it was
...@@ -73888,6 +73930,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -73888,6 +73930,12 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
_converse.api.listen.on('statusInitialized', initOMEMO); _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('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));
});
} }
}); });
...@@ -78860,7 +78908,7 @@ __e(o.__('Name')) + ...@@ -78860,7 +78908,7 @@ __e(o.__('Name')) +
'</strong>: ' + '</strong>: ' +
__e(o.name) + __e(o.name) +
'</p>\n <p class="room-info"><strong>' + '</p>\n <p class="room-info"><strong>' +
__e(o.__('Room address (JID)')) + __e(o.__('Groupchat address (JID)')) +
'</strong>: ' + '</strong>: ' +
__e(o.jid) + __e(o.jid) +
'</p>\n <p class="room-info"><strong>' + '</p>\n <p class="room-info"><strong>' +
...@@ -78890,7 +78938,7 @@ __e(o.__('Features')) + ...@@ -78890,7 +78938,7 @@ __e(o.__('Features')) +
__p += '\n <li class="feature" ><span class="fa fa-lock"></span>' + __p += '\n <li class="feature" ><span class="fa fa-lock"></span>' +
__e( o.__('Password protected') ) + __e( o.__('Password protected') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room requires a password before entry') ) + __e( o.__('This groupchat requires a password before entry') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78898,7 +78946,7 @@ __p += '\n '; ...@@ -78898,7 +78946,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-unlock"></span>' + __p += '\n <li class="feature" ><span class="fa fa-unlock"></span>' +
__e( o.__('No password required') ) + __e( o.__('No password required') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room does not require a password upon entry') ) + __e( o.__('This groupchat does not require a password upon entry') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78906,7 +78954,7 @@ __p += '\n '; ...@@ -78906,7 +78954,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-eye-slash"></span>' + __p += '\n <li class="feature" ><span class="fa fa-eye-slash"></span>' +
__e( o.__('Hidden') ) + __e( o.__('Hidden') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room is not publicly searchable') ) + __e( o.__('This groupchat is not publicly searchable') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78914,7 +78962,7 @@ __p += '\n '; ...@@ -78914,7 +78962,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-eye"></span>' + __p += '\n <li class="feature" ><span class="fa fa-eye"></span>' +
__e( o.__('Public') ) + __e( o.__('Public') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room is publicly searchable') ) + __e( o.__('This groupchat is publicly searchable') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78922,7 +78970,7 @@ __p += '\n '; ...@@ -78922,7 +78970,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-address-book"></span>' + __p += '\n <li class="feature" ><span class="fa fa-address-book"></span>' +
__e( o.__('Members only') ) + __e( o.__('Members only') ) +
' - <em>' + ' - <em>' +
__e( o.__('this room is restricted to members only') ) + __e( o.__('This groupchat is restricted to members only') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78930,7 +78978,7 @@ __p += '\n '; ...@@ -78930,7 +78978,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-globe"></span>' + __p += '\n <li class="feature" ><span class="fa fa-globe"></span>' +
__e( o.__('Open') ) + __e( o.__('Open') ) +
' - <em>' + ' - <em>' +
__e( o.__('Anyone can join this room') ) + __e( o.__('Anyone can join this groupchat') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78938,7 +78986,7 @@ __p += '\n '; ...@@ -78938,7 +78986,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-save"></span>' + __p += '\n <li class="feature" ><span class="fa fa-save"></span>' +
__e( o.__('Persistent') ) + __e( o.__('Persistent') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room persists even if it\'s unoccupied') ) + __e( o.__('This groupchat persists even if it\'s unoccupied') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78946,7 +78994,7 @@ __p += '\n '; ...@@ -78946,7 +78994,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-snowflake-o"></span>' + __p += '\n <li class="feature" ><span class="fa fa-snowflake-o"></span>' +
__e( o.__('Temporary') ) + __e( o.__('Temporary') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room will disappear once the last person leaves') ) + __e( o.__('This groupchat will disappear once the last person leaves') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78954,7 +79002,7 @@ __p += '\n '; ...@@ -78954,7 +79002,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-id-card"></span>' + __p += '\n <li class="feature" ><span class="fa fa-id-card"></span>' +
__e( o.__('Not anonymous') ) + __e( o.__('Not anonymous') ) +
' - <em>' + ' - <em>' +
__e( o.__('All other room occupants can see your XMPP username') ) + __e( o.__('All other groupchat participants can see your XMPP username') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78970,7 +79018,7 @@ __p += '\n '; ...@@ -78970,7 +79018,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-gavel"></span>' + __p += '\n <li class="feature" ><span class="fa fa-gavel"></span>' +
__e( o.__('Moderated') ) + __e( o.__('Moderated') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room is being moderated') ) + __e( o.__('This groupchat is being moderated') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -78978,7 +79026,7 @@ __p += '\n '; ...@@ -78978,7 +79026,7 @@ __p += '\n ';
__p += '\n <li class="feature" ><span class="fa fa-info-circle"></span>' + __p += '\n <li class="feature" ><span class="fa fa-info-circle"></span>' +
__e( o.__('Not moderated') ) + __e( o.__('Not moderated') ) +
' - <em>' + ' - <em>' +
__e( o.__('This room is not being moderated') ) + __e( o.__('This groupchat is not being moderated') ) +
'</em></li>\n '; '</em></li>\n ';
} ; } ;
__p += '\n '; __p += '\n ';
...@@ -81332,10 +81380,10 @@ var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./no ...@@ -81332,10 +81380,10 @@ var _ = {escape:__webpack_require__(/*! ./node_modules/lodash/escape.js */ "./no
module.exports = function(o) { module.exports = function(o) {
var __t, __p = '', __e = _.escape, __j = Array.prototype.join; var __t, __p = '', __e = _.escape, __j = Array.prototype.join;
function print() { __p += __j.call(arguments, '') } function print() { __p += __j.call(arguments, '') }
__p += '<!-- src/templates/user_details_modal.html -->\n<div class="modal fade" id="user-profile-modal" tabindex="-1" role="dialog" aria-labelledby="user-profile-modal-label" aria-hidden="true">\n <div class="modal-dialog" role="document">\n <div class="modal-content">\n <div class="modal-header">\n <h5 class="modal-title" id="user-profile-modal-label">' + __p += '<!-- src/templates/user_details_modal.html -->\n<div class="modal fade" id="user-details-modal" tabindex="-1" role="dialog" aria-labelledby="user-details-modal-label" aria-hidden="true">\n <div class="modal-dialog" role="document">\n <div class="modal-content">\n <div class="modal-header">\n <h5 class="modal-title" id="user-details-modal-label">' +
__e(o.display_name) + __e(o.display_name) +
'</h5>\n <button type="button" class="close" data-dismiss="modal" aria-label="' + '</h5>\n <button type="button" class="close" data-dismiss="modal" aria-label="' +
__e(o.label_close) + __e(o.__('Close')) +
'"><span aria-hidden="true">&times;</span></button>\n </div>\n <div class="modal-body">\n '; '"><span aria-hidden="true">&times;</span></button>\n </div>\n <div class="modal-body">\n ';
if (o.image) { ; if (o.image) { ;
__p += '\n <img alt="' + __p += '\n <img alt="' +
...@@ -81349,19 +81397,19 @@ __e(o.image) + ...@@ -81349,19 +81397,19 @@ __e(o.image) +
__p += '\n '; __p += '\n ';
if (o.fullname) { ; if (o.fullname) { ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_fullname) + __e(o.__('Full Name')) +
':</label>&nbsp;' + ':</label>&nbsp;' +
__e(o.fullname) + __e(o.fullname) +
'</p>\n '; '</p>\n ';
} ; } ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_jid) + __e(o.__('XMPP Address')) +
':</label>&nbsp;' + ':</label>&nbsp;' +
__e(o.jid) + __e(o.jid) +
'</p>\n '; '</p>\n ';
if (o.nickname) { ; if (o.nickname) { ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_nickname) + __e(o.__('Nickname')) +
':</label>&nbsp;' + ':</label>&nbsp;' +
__e(o.nickname) + __e(o.nickname) +
'</p>\n '; '</p>\n ';
...@@ -81369,7 +81417,7 @@ __e(o.nickname) + ...@@ -81369,7 +81417,7 @@ __e(o.nickname) +
__p += '\n '; __p += '\n ';
if (o.url) { ; if (o.url) { ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_url) + __e(o.__('URL')) +
':</label>&nbsp;<a target="_blank" rel="noopener" href="' + ':</label>&nbsp;<a target="_blank" rel="noopener" href="' +
__e(o.url) + __e(o.url) +
'">' + '">' +
...@@ -81379,7 +81427,7 @@ __e(o.url) + ...@@ -81379,7 +81427,7 @@ __e(o.url) +
__p += '\n '; __p += '\n ';
if (o.email) { ; if (o.email) { ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_email) + __e(o.__('Email')) +
':</label>&nbsp;<a href="mailto:' + ':</label>&nbsp;<a href="mailto:' +
__e(o.email) + __e(o.email) +
'">' + '">' +
...@@ -81389,21 +81437,69 @@ __e(o.email) + ...@@ -81389,21 +81437,69 @@ __e(o.email) +
__p += '\n '; __p += '\n ';
if (o.role) { ; if (o.role) { ;
__p += '\n <p><label>' + __p += '\n <p><label>' +
__e(o.label_role) + __e(o.__('Role')) +
':</label>&nbsp;' + ':</label>&nbsp;' +
__e(o.role) + __e(o.role) +
'</p>\n '; '</p>\n ';
} ; } ;
__p += '\n\n ';
if (o.has_omemo) { ;
__p += '\n <hr>\n <ul class="list-group fingerprints">\n <li class="list-group-item active">' +
__e(o.__('OMEMO Fingerprints')) +
'</li>\n ';
if (!o.devicelist.devices) { ;
__p += '\n <li class="list-group-item"><span class="spinner fa fa-spinner centered"/></li>\n ';
} ;
__p += '\n ';
if (o.devicelist.devices) { ;
__p += '\n ';
o.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-primary btn--small" ';
if (device.get('trusted') != -1) { ;
__p += ' active ';
} ;
__p += '>\n <input type="radio" name="trust-' +
__e(device.get('id')) +
'" value="trusted"\n ';
if (device.get('trusted') != -1) { ;
__p += ' checked ';
} ;
__p += '>' +
__e(o.__('Trusted')) +
'\n </label>\n <label class="btn btn-secondary btn--small" ';
if (device.get('trusted') != -1) { ;
__p += ' active ';
} ;
__p += '>\n <input type="radio" name="trust-' +
__e(device.get('id')) +
'" value="untrusted"\n ';
if (device.get('trusted') == -1) { ;
__p += ' checked ';
} ;
__p += '>' +
__e(o.__('Untrusted')) +
'\n </label>\n </div>\n </form>\n </li>\n ';
} ;
__p += '\n ';
}); ;
__p += '\n ';
} ;
__p += '\n </ul>\n ';
} ;
__p += '\n </div>\n <div class="modal-footer">\n '; __p += '\n </div>\n <div class="modal-footer">\n ';
if (o.allow_contact_removal && o.is_roster_contact) { ; if (o.allow_contact_removal && o.is_roster_contact) { ;
__p += '\n <button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>' + __p += '\n <button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>' +
__e(o.label_remove) + __e(o.__('Remove as contact')) +
'</button>\n '; '</button>\n ';
} ; } ;
__p += '\n <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>' + __p += '\n <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>' +
__e(o.label_refresh) + __e(o.__('Refresh')) +
'</button>\n <button type="button" class="btn btn-secondary" data-dismiss="modal">' + '</button>\n <button type="button" class="btn btn-secondary" data-dismiss="modal">' +
__e(o.label_close) + __e(o.__('Close')) +
'</button>\n </div>\n </div>\n </div>\n</div>\n'; '</button>\n </div>\n </div>\n </div>\n</div>\n';
return __p return __p
}; };
...@@ -82302,6 +82398,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ ...@@ -82302,6 +82398,24 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_
return result; 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) { u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8"); var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab)); return enc.decode(new Uint8Array(ab));
...@@ -8,7 +8,7 @@ msgstr "" ...@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 0.4\n" "Project-Id-Version: Converse.js 0.4\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 11:17+0200\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" "Last-Translator: JC Brand <jc@opkode.com>\n"
"Language-Team: Afrikaans <https://hosted.weblate.org/projects/conversejs/" "Language-Team: Afrikaans <https://hosted.weblate.org/projects/conversejs/"
"translations/af/>\n" "translations/af/>\n"
......
#conversejs { #conversejs {
.btn--small {
font-size: 80%;
font-weight: normal;
}
form { form {
.form-group { .form-group {
margin-bottom: 2em; margin-bottom: 2em;
......
...@@ -4,4 +4,9 @@ ...@@ -4,4 +4,9 @@
font-weight: bold; font-weight: bold;
} }
} }
.fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%;
}
} }
...@@ -34,6 +34,8 @@ $green: #3AA569; ...@@ -34,6 +34,8 @@ $green: #3AA569;
$dark-green: #1E9652; $dark-green: #1E9652;
$darkest-green: #0E763B; $darkest-green: #0E763B;
$info: $green !default;
$lightest-green: #E7FBF0; $lightest-green: #E7FBF0;
$light-green: #5CBC86; $light-green: #5CBC86;
$green: #3AA569; $green: #3AA569;
......
...@@ -396,6 +396,15 @@ ...@@ -396,6 +396,15 @@
`</publish>`+ `</publish>`+
`</pubsub>`+ `</pubsub>`+
`</iq>`) `</iq>`)
spyOn(_converse, 'emit');
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));
expect(_converse.emit).toHaveBeenCalledWith('OMEMOInitialized');
done(); done();
}); });
})); }));
...@@ -494,7 +503,6 @@ ...@@ -494,7 +503,6 @@
'type': 'result'}); 'type': 'result'});
_converse.connection._dataRecv(test_utils.createRequest(stanza)); _converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => { return test_utils.waitUntil(() => {
return _.filter( return _.filter(
_converse.connection.IQ_stanzas, _converse.connection.IQ_stanzas,
...@@ -565,6 +573,101 @@ ...@@ -565,6 +573,101 @@
done(); done();
}).catch(_.partial(console.error, _)); }).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');
done();
});
}));
}); });
describe("A chatbox with an active OMEMO session", function() { describe("A chatbox with an active OMEMO session", function() {
......
...@@ -389,7 +389,6 @@ ...@@ -389,7 +389,6 @@
function (done, _converse) { function (done, _converse) {
_converse.roster_groups = true; _converse.roster_groups = true;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
_converse.rosterview.render(); _converse.rosterview.render();
test_utils.openControlBox(); test_utils.openControlBox();
...@@ -430,7 +429,6 @@ ...@@ -430,7 +429,6 @@
function (done, _converse) { function (done, _converse) {
_converse.roster_groups = true; _converse.roster_groups = true;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
_converse.rosterview.render(); _converse.rosterview.render();
...@@ -477,7 +475,6 @@ ...@@ -477,7 +475,6 @@
_converse.roster_groups = true; _converse.roster_groups = true;
var groups = ['colleagues', 'friends']; var groups = ['colleagues', 'friends'];
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox(); test_utils.openControlBox();
_converse.rosterview.render(); _converse.rosterview.render();
...@@ -576,7 +573,6 @@ ...@@ -576,7 +573,6 @@
null, ['rosterGroupsFetched'], {}, null, ['rosterGroupsFetched'], {},
function (done, _converse) { function (done, _converse) {
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox(); test_utils.openControlBox();
_converse.roster.create({ _converse.roster.create({
...@@ -726,7 +722,6 @@ ...@@ -726,7 +722,6 @@
var i, t; var i, t;
test_utils.openControlBox(); test_utils.openControlBox();
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
for (i=0; i<mock.pend_names.length; i++) { for (i=0; i<mock.pend_names.length; i++) {
_converse.roster.create({ _converse.roster.create({
...@@ -908,7 +903,6 @@ ...@@ -908,7 +903,6 @@
test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700) test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700)
.then(function () { .then(function () {
var jid, t; var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el); var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) { for (var i=0; i<mock.cur_names.length; i++) {
...@@ -935,7 +929,6 @@ ...@@ -935,7 +929,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length; return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () { }, 700).then(function () {
var jid, t; var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el); var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) { for (var i=0; i<mock.cur_names.length; i++) {
...@@ -963,7 +956,6 @@ ...@@ -963,7 +956,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length; return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () { }, 700).then(function () {
var jid, t; var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el); var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) { for (var i=0; i<mock.cur_names.length; i++) {
...@@ -991,7 +983,6 @@ ...@@ -991,7 +983,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length; return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () { }, 700).then(function () {
var jid, t; var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el); var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) { for (var i=0; i<mock.cur_names.length; i++) {
...@@ -1020,7 +1011,6 @@ ...@@ -1020,7 +1011,6 @@
}, 500) }, 500)
.then(function () { .then(function () {
var jid, t; var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el); var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) { for (var i=0; i<mock.cur_names.length; i++) {
...@@ -1151,7 +1141,6 @@ ...@@ -1151,7 +1141,6 @@
names.push($(item).text().replace(/^\s+|\s+$/g, '')); names.push($(item).text().replace(/^\s+|\s+$/g, ''));
} }
}; };
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough(); spyOn(_converse.rosterview, 'update').and.callThrough();
spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough(); spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough();
for (i=0; i<mock.req_names.length; i++) { for (i=0; i<mock.req_names.length; i++) {
......
...@@ -245,26 +245,41 @@ ...@@ -245,26 +245,41 @@
initialize () { initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments); _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('contactAdded', this.registerContactEventHandlers, this); this.model.on('contactAdded', this.registerContactEventHandlers, this);
this.model.on('change', this.render, this);
// XXX: Leaky abstraction from converse-omemo
// In part, we're hampered by the fact that we can't
// have sub-views inside a VDOMView.
// If we did, we could put the OMEMO part of this modal
// inside another view and have it render as a sub-view.
// However, for this we'd need some kind of registry and
// way to look up sub-views by tag from the template (which
// I assume is what for example vue.js does).
this.has_omemo = _converse.pluggable.plugins['converse-omemo'].enabled();
if (this.has_omemo) {
const jid = this.model.get('jid');
this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
this.devicelist.devices.on('change:fingerprint', this.render, this);
} else {
this.devicelist = {};
}
this.registerContactEventHandlers(); this.registerContactEventHandlers();
_converse.emit('userDetailsModalInitialized', this.model);
}, },
toHTML () { toHTML () {
return tpl_user_details_modal(_.extend( return tpl_user_details_modal(_.extend(
this.model.toJSON(), this.model.toJSON(),
this.model.vcard.toJSON(), { this.model.vcard.toJSON(), {
'_': _,
'__': __,
'has_omemo': this.has_omemo,
'devicelist': this.devicelist,
'allow_contact_removal': _converse.allow_contact_removal, 'allow_contact_removal': _converse.allow_contact_removal,
'alt_profile_image': __("The User's Profile Image"), 'alt_profile_image': __("The User's Profile Image"),
'display_name': this.model.getDisplayName(), 'display_name': this.model.getDisplayName(),
'is_roster_contact': !_.isUndefined(this.model.contact), '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')
})); }));
}, },
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
} }
}); });
return { return {
'identity_key': parseInt(bundle_el.querySelector('identityKey').textContent, 10), 'identity_key': bundle_el.querySelector('identityKey').textContent,
'signed_prekey': { 'signed_prekey': {
'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10), 'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
'public_key': signed_prekey_public_el.textContent, 'public_key': signed_prekey_public_el.textContent,
...@@ -77,7 +77,6 @@ ...@@ -77,7 +77,6 @@
this.buildSessions(devices) this.buildSessions(devices)
.then(() => resolve(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));
}).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 +193,26 @@ ...@@ -194,6 +193,26 @@
_converse.NUM_PREKEYS = 100; // Set here so that tests can override _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);
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) { _converse.getDevicesForContact = function (jid) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
...@@ -405,14 +424,15 @@ ...@@ -405,14 +424,15 @@
'from': _converse.bare_jid, 'from': _converse.bare_jid,
'to': this.get('jid') 'to': this.get('jid')
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB}) }).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( _converse.connection.sendIQ(
stanza, stanza,
(iq) => { (iq) => {
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, stanza).pop(); const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(); bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
this.save(parseBundle(bundle_el)); bundle = parseBundle(bundle_el);
resolve(); this.save('bundle', bundle);
resolve(bundle);
}, },
reject, reject,
_converse.IQ_TIMEOUT _converse.IQ_TIMEOUT
...@@ -479,7 +499,7 @@ ...@@ -479,7 +499,7 @@
(iq) => { (iq) => {
_.forEach( _.forEach(
iq.querySelectorAll('device'), iq.querySelectorAll('device'),
(dev) => this.devices.create({'id': dev.getAttribute('id')}) (dev) => this.devices.create({'id': dev.getAttribute('id'), 'jid': this.get('jid')})
); );
resolve(); resolve();
}, },
...@@ -493,7 +513,7 @@ ...@@ -493,7 +513,7 @@
* server. * server.
* https://xmpp.org/extensions/xep-0384.html#usecases-announcing * 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) => { return new Promise((resolve, reject) => {
const stanza = $iq({ const stanza = $iq({
'from': _converse.bare_jid, 'from': _converse.bare_jid,
...@@ -589,7 +609,7 @@ ...@@ -589,7 +609,7 @@
jid = stanza.getAttribute('from'), jid = stanza.getAttribute('from'),
bundle_el = sizzle(`item > bundle`, items_el).pop(), bundle_el = sizzle(`item > bundle`, items_el).pop(),
devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid}), 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)}); device.save({'bundle': parseBundle(bundle_el)});
} }
...@@ -613,7 +633,7 @@ ...@@ -613,7 +633,7 @@
if (dev) { if (dev) {
dev.save({'active': true}); dev.save({'active': true});
} else { } 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 // Make sure our own device is on the list (i.e. if it was
...@@ -661,6 +681,11 @@ ...@@ -661,6 +681,11 @@
_converse.api.listen.on('statusInitialized', initOMEMO); _converse.api.listen.on('statusInitialized', initOMEMO);
_converse.api.listen.on('addClientFeatures', _converse.api.listen.on('addClientFeatures',
() => _converse.api.disco.own.features.add(Strophe.NS.OMEMO_DEVICELIST+"notify")); () => _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));
});
} }
}); });
})); }));
<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-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="user-profile-modal-label">{{{o.display_name}}}</h5> <h5 class="modal-title" id="user-details-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> <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.__('Close')}}}"><span aria-hidden="true">&times;</span></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
{[ if (o.image) { ]} {[ if (o.image) { ]}
...@@ -12,28 +12,59 @@ ...@@ -12,28 +12,59 @@
height="100" width="100" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/> height="100" width="100" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
{[ } ]} {[ } ]}
{[ if (o.fullname) { ]} {[ 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) { ]} {[ 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) { ]} {[ 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) { ]} {[ 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) { ]} {[ if (o.role) { ]}
<p><label>{{{o.label_role}}}:</label>&nbsp;{{{o.role}}}</p> <p><label>{{{o.__('Role')}}}:</label>&nbsp;{{{o.role}}}</p>
{[ } ]}
{[ if (o.has_omemo) { ]}
<hr>
<ul class="list-group fingerprints">
<li class="list-group-item active">{{{o.__('OMEMO Fingerprints')}}}</li>
{[ if (!o.devicelist.devices) { ]}
<li class="list-group-item"><span class="spinner fa fa-spinner centered"/></li>
{[ } ]}
{[ if (o.devicelist.devices) { ]}
{[ o.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-primary btn--small" {[ if (device.get('trusted') != -1) { ]} active {[ } ]}>
<input type="radio" name="trust-{{{device.get('id')}}}" value="trusted"
{[ if (device.get('trusted') != -1) { ]} checked {[ } ]}>{{{o.__('Trusted')}}}
</label>
<label class="btn btn-secondary btn--small" {[ if (device.get('trusted') != -1) { ]} active {[ } ]}>
<input type="radio" name="trust-{{{device.get('id')}}}" value="untrusted"
{[ if (device.get('trusted') == -1) { ]} checked {[ } ]}>{{{o.__('Untrusted')}}}
</label>
</div>
</form>
</li>
{[ } ]}
{[ }); ]}
{[ } ]}
</ul>
{[ } ]} {[ } ]}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{[ if (o.allow_contact_removal && o.is_roster_contact) { ]} {[ 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-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.__('Refresh')}}}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.label_close}}}</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.__('Close')}}}</button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -846,6 +846,22 @@ ...@@ -846,6 +846,22 @@
return result; 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) { u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8"); var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab)); return enc.decode(new Uint8Array(ab));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment