Browse Source

Initial UI integration.

- Show own fingerprint in settings.
- Show notification in chat while peer identity is requested.
- Show lock icon on encrypted chat messages.
- Show button in chat menu to open dialog with fingerprint of remote peer.

Needs rebuilding of styles after merging.
pull/225/head
Joachim Bauch 11 years ago
parent
commit
72a5d60f9a
  1. 27
      src/styles/components/_chat.scss
  2. 2
      src/styles/global/_variables.scss
  3. 26
      static/js/controllers/chatroomcontroller.js
  4. 9
      static/js/controllers/uicontroller.js
  5. 5
      static/js/directives/buddylist.js
  6. 47
      static/js/directives/chat.js
  7. 10
      static/js/mediastream/api.js
  8. 7
      static/js/services/buddylist.js
  9. 73
      static/js/services/endtoendencryption.js
  10. 4
      static/partials/chatroom.html
  11. 9
      static/partials/settings.html

27
src/styles/components/_chat.scss

@ -220,6 +220,19 @@
} }
} }
.identityhint {
color: $chat-typing;
font-size: .8em;
height: 16px;
overflow: hidden;
padding: 0 6px;
white-space: nowrap;
@include breakpt($breakpoint-chat-small, max-height) {
display: none;
}
}
.inputbox { .inputbox {
position: relative; position: relative;
@ -349,6 +362,15 @@
width: 12px; width: 12px;
} }
&:after {
font-family: FontAwesome;
left: 0;
float: right;
text-align: center;
width: 12px;
margin-left: 1em;
}
&.unread:before { &.unread:before {
color: $chat-msg-unread-icon-color; color: $chat-msg-unread-icon-color;
content: $chat-msg-unread-icon; content: $chat-msg-unread-icon;
@ -378,6 +400,11 @@
color: $chat-msg-read-icon-color; color: $chat-msg-read-icon-color;
content: $chat-msg-read-icon; content: $chat-msg-read-icon;
} }
&.encrypted:after {
color: $chat-msg-encrypted-icon-color;
content: $chat-msg-encrypted-icon;
}
} }
.buddyPicture { .buddyPicture {

2
src/styles/global/_variables.scss

@ -123,6 +123,7 @@ $chat-msg-sent-icon-color: #5882fa !default;
$chat-msg-delivered-icon-color: #5882fa !default; $chat-msg-delivered-icon-color: #5882fa !default;
$chat-msg-received-icon-color: #84b819 !default; $chat-msg-received-icon-color: #84b819 !default;
$chat-msg-read-icon-color: $chat-msg-default-icon-color !default; $chat-msg-read-icon-color: $chat-msg-default-icon-color !default;
$chat-msg-encrypted-icon-color: $chat-msg-default-icon-color !default;
$chat-msg-unread-icon: '\f0eb' !default; $chat-msg-unread-icon: '\f0eb' !default;
$chat-msg-sending-icon: '\f0ec' !default; $chat-msg-sending-icon: '\f0ec' !default;
@ -130,6 +131,7 @@ $chat-msg-sent-icon: '\f003' !default;
$chat-msg-delivered-icon: '\f019' !default; $chat-msg-delivered-icon: '\f019' !default;
$chat-msg-received-icon: '\f06e' !default; $chat-msg-received-icon: '\f06e' !default;
$chat-msg-read-icon: '\f00c' !default; $chat-msg-read-icon: '\f00c' !default;
$chat-msg-encrypted-icon: '\f023' !default;
$chat-msg-self-background: #fff !default; $chat-msg-self-background: #fff !default;
$chat-msg-self-color: $font-color !default; $chat-msg-self-color: $font-color !default;

26
static/js/controllers/chatroomcontroller.js

@ -214,7 +214,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
} }
$scope.display = function(s, nodes, extra_css, title, picture) { $scope.display = function(s, nodes, extra_css, title, picture, encrypted) {
var container; var container;
var element; var element;
@ -235,6 +235,9 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
lastMessageContainer = $("<ul>").appendTo(container); lastMessageContainer = $("<ul>").appendTo(container);
if ($.trim(s)) { if ($.trim(s)) {
element = $("<li>").html(s); element = $("<li>").html(s);
if (encrypted) {
element.addClass("encrypted");
}
element.prepend('<div class="timestamp-space">'); element.prepend('<div class="timestamp-space">');
element.appendTo(lastMessageContainer); element.appendTo(lastMessageContainer);
} }
@ -267,7 +270,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
$scope.display(s, nodes); $scope.display(s, nodes);
}); });
$scope.append = function(s, nodes) { $scope.append = function(s, nodes, encrypted) {
if (!lastMessageContainer) { if (!lastMessageContainer) {
return; return;
@ -276,6 +279,9 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
var scroll = this.canScroll(); var scroll = this.canScroll();
var li = $("<li>"); var li = $("<li>");
if (encrypted) {
li.addClass("encrypted");
}
li.html(s) li.html(s)
lastMessageContainer.append(li) lastMessageContainer.append(li)
@ -325,7 +331,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
}; };
$scope.showmessage = function(from, timestamp, message, nodes) { $scope.showmessage = function(from, timestamp, message, nodes, encrypted) {
var sessonid = $scope.$parent.$parent.id; var sessonid = $scope.$parent.$parent.id;
@ -354,7 +360,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
var strMessage = s.join(" "); var strMessage = s.join(" ");
if (!is_new_message) { if (!is_new_message) {
var element = this.append(strMessage, nodes); var element = this.append(strMessage, nodes, encrypted);
if (element) { if (element) {
return element; return element;
} }
@ -375,7 +381,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
nodes = ts; nodes = ts;
} }
} }
return $scope.display(strMessage, nodes, msg.extra_css, msg.title, msg.picture); return $scope.display(strMessage, nodes, msg.extra_css, msg.title, msg.picture, encrypted);
}; };
@ -406,7 +412,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
$scope.focus(); $scope.focus();
}); });
$scope.$on("received", function(event, from, data) { $scope.$on("received", function(event, from, data, encrypted) {
var sessionid = $scope.$parent.$parent.id; var sessionid = $scope.$parent.$parent.id;
var mid = data.Mid || null; var mid = data.Mid || null;
@ -482,7 +488,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
subscope.from = from; subscope.from = from;
fileInfo(subscope, function(clonedElement, scope) { fileInfo(subscope, function(clonedElement, scope) {
var text = fromself ? translation._("You share file:") : translation._("Incoming file:"); var text = fromself ? translation._("You share file:") : translation._("Incoming file:");
element = $scope.showmessage(from, timestamp, text, clonedElement); element = $scope.showmessage(from, timestamp, text, clonedElement, encrypted);
}); });
noop = true; noop = true;
} }
@ -494,7 +500,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
subscope.from = from; subscope.from = from;
geoLocation(subscope, function(clonedElement, scope) { geoLocation(subscope, function(clonedElement, scope) {
var text = fromself ? translation._("You shared your location:") : translation._("Location received:"); var text = fromself ? translation._("You shared your location:") : translation._("Location received:");
element = $scope.showmessage(from, timestamp, text, clonedElement); element = $scope.showmessage(from, timestamp, text, clonedElement, encrypted);
}); });
noop = true; noop = true;
} }
@ -527,7 +533,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
} }
} }
} }
element = $scope.showmessage(from, timestamp, text, clonedElement); element = $scope.showmessage(from, timestamp, text, clonedElement, encrypted);
}); });
noop = true; noop = true;
} }
@ -550,7 +556,7 @@ define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!p
message = safeMessage(data.Message); message = safeMessage(data.Message);
} }
// Show the beast. // Show the beast.
element = $scope.showmessage(from, timestamp, message, nodes); element = $scope.showmessage(from, timestamp, message, nodes, encrypted);
} }
if (element && mid && !$scope.isgroupchat) { if (element && mid && !$scope.isgroupchat) {

9
static/js/controllers/uicontroller.js

@ -116,6 +116,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
$scope.id = $scope.myid = null; $scope.id = $scope.myid = null;
$scope.userid = $scope.myuserid = null; $scope.userid = $scope.myuserid = null;
$scope.suserid = null; $scope.suserid = null;
$scope.fingerprint = null;
$scope.peer = null; $scope.peer = null;
$scope.dialing = null; $scope.dialing = null;
$scope.conference = null; $scope.conference = null;
@ -536,6 +537,14 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web
} }
}); });
appData.e.on("identity.own", function(event, identity) {
if (identity) {
$scope.fingerprint = identity.getFingerprint();
} else {
$scope.fingerprint = null;
}
});
// Start heartbeat timer. // Start heartbeat timer.
$window.setInterval(function() { $window.setInterval(function() {
mediaStream.api.heartbeat(5000, 11500) mediaStream.api.heartbeat(5000, 11500)

5
static/js/directives/buddylist.js

@ -23,7 +23,7 @@
define(['underscore', 'text!partials/buddylist.html'], function(_, template) { define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList // buddyList
return ["buddyList", "api", "webrtc", "contacts", function(buddyList, api, webrtc, contacts) { return ["buddyList", "api", "webrtc", "contacts", "appData", function(buddyList, api, webrtc, contacts, appData) {
//console.log("buddyList directive"); //console.log("buddyList directive");
@ -125,6 +125,9 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
onContactUpdated(data); onContactUpdated(data);
}); });
appData.e.on("identity.received", function(event, peer, identity) {
buddylist.onIdentityReceived(peer, identity);
});
}]; }];
var link = function(scope, iElement, iAttrs, controller) { var link = function(scope, iElement, iAttrs, controller) {

47
static/js/directives/chat.js

@ -22,7 +22,7 @@
"use strict"; "use strict";
define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function($, _, templateChat, templateChatroom) { define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function($, _, templateChat, templateChatroom) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", "geolocation", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout, geolocation) { return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "alertify", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", "geolocation", function($compile, safeDisplayName, mediaStream, safeApply, alertify, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout, geolocation) {
var displayName = safeDisplayName; var displayName = safeDisplayName;
var groupChatId = ""; var groupChatId = "";
@ -57,7 +57,7 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
return res; return res;
}; };
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) { mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p, encrypted) {
//console.log("received", data, id, from); //console.log("received", data, id, from);
@ -102,7 +102,7 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
} }
safeApply(room); safeApply(room);
room.$broadcast("received", from, data); room.$broadcast("received", from, data, encrypted);
}); });
@ -130,6 +130,28 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
} }
}); });
appData.e.on("identity.request", function(event, peer) {
var room = rooms[peer];
if (room) {
room.$apply(function(scope) {
scope.requestPeerIdentity = true;
});
}
});
appData.e.on("identity.received", function(event, peer, identity) {
var room = rooms[peer];
if (room) {
room.$apply(function(scope) {
scope.requestPeerIdentity = false;
if (identity) {
scope.fingerprint = identity.getFingerprint();
} else {
scope.fingerprint = null;
}
});
}
});
$scope.$parent.$on("startchat", function(event, id, options) { $scope.$parent.$on("startchat", function(event, id, options) {
//console.log("startchat requested", event, id); //console.log("startchat requested", event, id);
@ -205,6 +227,10 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
subscope = controller.rooms[id] = scope.$new(); subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope); translation.inject(subscope);
subscope.id = id; subscope.id = id;
var bd = buddyData.get(id);
var identity = bd ? bd.identity : null;
subscope.requestPeerIdentity = false;
subscope.fingerprint = identity ? identity.getFingerprint() : null;
subscope.isgroupchat = !!settings.group; subscope.isgroupchat = !!settings.group;
subscope.index = index; subscope.index = index;
subscope.settings = settings; subscope.settings = settings;
@ -269,12 +295,13 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
send: function(type, data, origType, origData) { send: function(type, data, origType, origData) {
// We also send to self, to display our own stuff. // We also send to self, to display our own stuff.
if (!noloop) { if (!noloop) {
var encrypted = (type !== origType);
mediaStream.api.received({ mediaStream.api.received({
Type: origData.Type, Type: origData.Type,
Data: origData, Data: origData,
From: mediaStream.api.id, From: mediaStream.api.id,
To: peercall.id To: peercall.id
}); }, encrypted);
} }
return peercall.peerconnection.send(data); return peercall.peerconnection.send(data);
} }
@ -289,7 +316,7 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
}); });
} }
_.delay(function() { _.delay(function() {
mediaStream.api.send2("sendChat", function(type, data) { mediaStream.api.send2("sendChat", function(type, data, encrypted) {
if (!noloop) { if (!noloop) {
//console.log("looped to self", type, data); //console.log("looped to self", type, data);
mediaStream.api.received({ mediaStream.api.received({
@ -297,7 +324,7 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
Data: data, Data: data,
From: mediaStream.api.id, From: mediaStream.api.id,
To: to To: to
}); }, encrypted);
} }
})(to, message, status, mid); })(to, message, status, mid);
}, 100); }, 100);
@ -329,6 +356,14 @@ define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatro
console.error("Failed to receive geolocation", err); console.error("Failed to receive geolocation", err);
}); });
}; };
subscope.showFingerprint = function() {
if (subscope.fingerprint) {
// TODO(fancycode): Show a nicer notification.
var msg = translation._("%1$s has an identity with the fingerprint %2$s.",
displayName(subscope.id), subscope.fingerprint);
alertify.dialog.notify(translation._("Fingerprint"), msg);
}
};
subscope.doClear = function() { subscope.doClear = function() {
subscope.$broadcast("clear"); subscope.$broadcast("clear");
}; };

10
static/js/mediastream/api.js

@ -102,7 +102,9 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
}, this), }, this),
sendEncrypted: _.bind(function(type, data) { sendEncrypted: _.bind(function(type, data) {
if (cb) { if (cb) {
cb(type, data); var to = data.To;
var encrypted = (to && this.endToEndEncryption);
cb(type, data, encrypted);
} }
this.sendEncrypted(type, data); this.sendEncrypted(type, data);
}, this) }, this)
@ -160,7 +162,7 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
return _.bind(f, obj); return _.bind(f, obj);
}; };
Api.prototype.received = function(d) { Api.prototype.received = function(d, encrypted) {
// Store received timestamp. // Store received timestamp.
var now = new Date().getTime(); var now = new Date().getTime();
@ -189,11 +191,13 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
return; return;
} }
this.endToEndEncryption.decrypt(d.From, data, _.bind(function(decrypted) { this.endToEndEncryption.decrypt(d.From, data, _.bind(function(decrypted) {
d.encrypted = true;
this.processReceived(d, decrypted.Type, decrypted[decrypted.Type]); this.processReceived(d, decrypted.Type, decrypted[decrypted.Type]);
}, this)); }, this));
return; return;
} }
d.encrypted = !!encrypted;
this.processReceived(d, dataType, data); this.processReceived(d, dataType, data);
} }
@ -239,7 +243,7 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
break; break;
case "Chat": case "Chat":
//console.log("chat received", dataType, data); //console.log("chat received", dataType, data);
this.e.triggerHandler("received.chat", [data.To, d.From, data.Chat, d.p2p]); this.e.triggerHandler("received.chat", [data.To, d.From, data.Chat, d.p2p, d.encrypted]);
break; break;
case "Conference": case "Conference":
this.e.triggerHandler("received.conference", [data.Id, data.Conference, data.Type, d.To, d.From]); this.e.triggerHandler("received.conference", [data.Id, data.Conference, data.Type, d.To, d.From]);

7
static/js/services/buddylist.js

@ -647,6 +647,13 @@ define(['jquery', 'angular', 'underscore', 'modernizr', 'avltree', 'text!partial
}; };
Buddylist.prototype.onIdentityReceived = function(id, identity) {
var scope = buddyData.get(id);
if (scope) {
scope.identity = identity;
}
};
Buddylist.prototype.click = function(buddyElement, target) { Buddylist.prototype.click = function(buddyElement, target) {
var be = buddyElement[0]; var be = buddyElement[0];

73
static/js/services/endtoendencryption.js

@ -36,9 +36,11 @@ define([
return [ return [
"$window", "$window",
"$q", "$q",
"appData",
function( function(
$window, $window,
$q $q,
appData
) { ) {
// Bitflags for the different components that need to be ready for // Bitflags for the different components that need to be ready for
@ -231,6 +233,24 @@ define([
return this.save("signed_pre_key_" + this.id + "_" + key.id, data); return this.save("signed_pre_key_" + this.id + "_" + key.id, data);
}; };
var PeerIdentity = function(id, identity_key) {
this.id = id;
this.identity_key = identity_key;
};
PeerIdentity.prototype.getFingerprint = function() {
// TODO(jojo): Change this to be the SHA-1 hash of a three-byte key
// id and the public key.
// See https://github.com/WhisperSystems/TextSecure/blob/08ed90c5ece49c92e35c492afb4e60160983015a/src/org/thoughtcrime/securesms/crypto/PublicKey.java#L95
var fingerprint = ByteBuffer.wrap(this.identity_key).toHex();
var pos;
for (pos = fingerprint.length - 4; pos >= 4; pos -= 4) {
fingerprint = fingerprint.substr(0, pos) + "-" +
fingerprint.substr(pos);
}
return fingerprint.substr(2);
};
var EndToEndEncryption = function(api) { var EndToEndEncryption = function(api) {
this.e = $({}); this.e = $({});
this.api = api; this.api = api;
@ -292,36 +312,32 @@ define([
} }
}; };
EndToEndEncryption.prototype.getIdentityFingerprint = function() { EndToEndEncryption.prototype.setOwnIdentity = function(public_key) {
return this.formatIdentityFingerprint(this.identity_keypair.public); var identity = new PeerIdentity(null, public_key);
}; this.own_identity = identity;
appData.e.triggerHandler("identity.own", [identity]);
EndToEndEncryption.prototype.formatIdentityFingerprint = function(public_key) {
// TODO(jojo): Change this to be the SHA-1 hash of a three-byte key
// id and the public key.
// See https://github.com/WhisperSystems/TextSecure/blob/08ed90c5ece49c92e35c492afb4e60160983015a/src/org/thoughtcrime/securesms/crypto/PublicKey.java#L95
var fingerprint = ByteBuffer.wrap(public_key).toHex();
var pos;
for (pos = fingerprint.length - 4; pos >= 4; pos -= 4) {
fingerprint = fingerprint.substr(0, pos) + "-" +
fingerprint.substr(pos);
}
return fingerprint.substr(2);
}; };
EndToEndEncryption.prototype.storePeerIdentity = function(peer, public_key) { EndToEndEncryption.prototype.storePeerIdentity = function(peer, public_key) {
var fingerprint = this.formatIdentityFingerprint(public_key); var identity = new PeerIdentity(peer, public_key);
var fingerprint = identity.getFingerprint();
var existing = this.peer_identities[peer]; var existing = this.peer_identities[peer];
if (existing === fingerprint) { if (existing) {
return; if (existing.getFingerprint() === fingerprint) {
} else if (existing && existing !== fingerprint) { // No change.
console.warn("Fingerprint changed", { return;
"peer": peer, } else {
"existing": existing, // Uh oh, remote peer has a new identity, this is something
"fingerprint": fingerprint // the user should know about!
}); appData.e.triggerHandler("identity.changed", [
peer,
existing,
identity
]);
}
} }
this.peer_identities[peer] = fingerprint; this.peer_identities[peer] = identity;
appData.e.triggerHandler("identity.received", [peer, identity]);
}; };
EndToEndEncryption.prototype.getReadyPromise = function() { EndToEndEncryption.prototype.getReadyPromise = function() {
@ -360,6 +376,9 @@ define([
var deferred = $q.defer(); var deferred = $q.defer();
var doLoadData = _.bind(function() { var doLoadData = _.bind(function() {
this.identity_keypair = this.store.loadKeypair(); this.identity_keypair = this.store.loadKeypair();
if (this.identity_keypair) {
this.setOwnIdentity(this.identity_keypair.public);
}
this.registration_id = this.store.loadRegistrationId(); this.registration_id = this.store.loadRegistrationId();
this.last_resort_pre_key = this.last_resort_pre_key =
this.store.loadSignedPreKey(LAST_RESORT_PRE_KEY_ID); this.store.loadSignedPreKey(LAST_RESORT_PRE_KEY_ID);
@ -390,6 +409,7 @@ define([
.then(_.bind(function(keypair) { .then(_.bind(function(keypair) {
if (this.identity_keypair === null) { if (this.identity_keypair === null) {
this.identity_keypair = keypair; this.identity_keypair = keypair;
this.setOwnIdentity(this.identity_keypair.public);
this.store.saveKeypair(keypair); this.store.saveKeypair(keypair);
} }
deferred.resolve(this.identity_keypair); deferred.resolve(this.identity_keypair);
@ -593,6 +613,7 @@ define([
"message": message, "message": message,
"callback": callback "callback": callback
}); });
appData.e.triggerHandler("identity.request", [peer]);
this.apiSend("EncryptionRequestKeyBundle", {"To": peer}); this.apiSend("EncryptionRequestKeyBundle", {"To": peer});
return; return;
} }

4
static/partials/chatroom.html

@ -5,6 +5,7 @@
<button ng-if="!isgroupchat" class="btn btn-sm btn-default" title="{{_('Start video call')}}" ng-click="doCall()"><i class="fa fa-phone fa-fw"></i></button> <button ng-if="!isgroupchat" class="btn btn-sm btn-default" title="{{_('Start video call')}}" ng-click="doCall()"><i class="fa fa-phone fa-fw"></i></button>
<button class="btn btn-sm btn-default btn-fileupload" title="{{_('Upload files')}}"><i class="fa fa-upload fa-fw"></i></button> <button class="btn btn-sm btn-default btn-fileupload" title="{{_('Upload files')}}"><i class="fa fa-upload fa-fw"></i></button>
<button class="btn btn-sm btn-default btn-locationshare" title="{{_('Share my location')}}" ng-click="shareGeolocation()"><i class="fa fa-location-arrow fa-fw"></i></button> <button class="btn btn-sm btn-default btn-locationshare" title="{{_('Share my location')}}" ng-click="shareGeolocation()"><i class="fa fa-location-arrow fa-fw"></i></button>
<button ng-if="!isgroupchat" ng-show="fingerprint" class="btn btn-sm btn-default btn-fingerprint" title="{{_('Show peer fingerprint')}}" ng-click="showFingerprint()"><i class="fa fa-lock fa-fw"></i></button>
</div> </div>
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button class="btn btn-sm btn-default" title="{{_('Clear chat')}}" ng-click="doClear()"><i class="fa fa-eraser fa-fw"></i></button> <button class="btn btn-sm btn-default" title="{{_('Clear chat')}}" ng-click="doClear()"><i class="fa fa-eraser fa-fw"></i></button>
@ -19,6 +20,9 @@
<span ng-switch-when="start"><i class="fa fa-pencil"></i> {{id|displayName}} {{_('is typing...')}}</span> <span ng-switch-when="start"><i class="fa fa-pencil"></i> {{id|displayName}} {{_('is typing...')}}</span>
<span ng-switch-when="stop"><i class="fa fa-pencil"></i> {{id|displayName}} {{_('has stopped typing...')}}</span> <span ng-switch-when="stop"><i class="fa fa-pencil"></i> {{id|displayName}} {{_('has stopped typing...')}}</span>
</div> </div>
<div class="identityhint" ng-show="requestPeerIdentity">
<i class="fa fa-lock"></i> {{_('Preparing encrypted session...')}}
</div>
<div class="inputbox"> <div class="inputbox">
<div> <div>
<textarea class="input nicescroll form-control" maxlength="{{maxMessageSize}}" ng-disabled="!(enabled)" on-enter="submit()" ng-model="input" placeholder="{{_('Type here to chat...')}}"/> <textarea class="input nicescroll form-control" maxlength="{{maxMessageSize}}" ng-disabled="!(enabled)" on-enter="submit()" ng-model="input" placeholder="{{_('Type here to chat...')}}"/>

9
static/partials/settings.html

@ -40,6 +40,13 @@
<p class="help-block">{{_('Your picture, name and status message identify yourself in calls, chats and rooms.')}}</p> <p class="help-block">{{_('Your picture, name and status message identify yourself in calls, chats and rooms.')}}</p>
</div> </div>
</div> </div>
<div class="form-group profile-fingerprint" ng-show="fingerprint">
<label class="col-xs-4 control-label">{{_('Fingerprint')}}</label>
<div class="col-xs-8">
<pre class="help-block">{{fingerprint}}</pre>
<p class="help-block">{{_('You can compare the fingerprint with other peers to confirm your identity.')}}</p>
</div>
</div>
<div ng-if="(withUsers && withUsersRegistration) || userid"> <div ng-if="(withUsers && withUsersRegistration) || userid">
<div class="form-group profile-yourid"> <div class="form-group profile-yourid">
<label class="col-xs-4 control-label">{{_('Your ID')}}</label> <label class="col-xs-4 control-label">{{_('Your ID')}}</label>
@ -314,4 +321,4 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save