Improve identity key conflict ux
Clicking on a key conflict message opens the message detail view, which displays the contact(s) in this conversation. If the message contains a key conflict with any of these contacts, a button is displayed which attempts to resolve that conflict and any other conflicts in the conversation that are related to that contact.
This commit is contained in:
parent
857eee5003
commit
897d391817
11 changed files with 415 additions and 81 deletions
|
@ -119,28 +119,18 @@
|
|||
<button class='back'></button>
|
||||
<span class='title-text'>Message Detail</span>
|
||||
</div>
|
||||
<div class='message-container'></div>
|
||||
<div class='info'>
|
||||
<table>
|
||||
<tr><td class='label'>Sent</td><td> {{ sent_at }}</td></tr>
|
||||
<tr><td class='label'>Received</td><td> {{ received_at }}</td></tr>
|
||||
<tr>
|
||||
<td class='tofrom label'>{{ tofrom }}</td>
|
||||
<td>
|
||||
<div class='contacts'>
|
||||
{{ #contacts }}
|
||||
<div>
|
||||
<span class='avatar'><img src='{{ avatar_url }}' /></span>
|
||||
<span class='name'>{{ name }}</div>
|
||||
{{ #conflict }}
|
||||
<button class='resolve'>Key Conflict</button>
|
||||
{{ /conflict }}
|
||||
</div>
|
||||
{{ /contacts }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class='container'>
|
||||
<div class='message-container'></div>
|
||||
<div class='info'>
|
||||
<table>
|
||||
<tr><td class='label'>Sent</td><td> {{ sent_at }}</td></tr>
|
||||
<tr><td class='label'>Received</td><td> {{ received_at }}</td></tr>
|
||||
<tr>
|
||||
<td class='tofrom label'>{{ tofrom }}</td>
|
||||
<td> <div class='contacts'></div> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='key-verification'>
|
||||
|
@ -200,6 +190,42 @@
|
|||
<div class='contacts'></div>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='incoming-key-conflict'>
|
||||
<p>
|
||||
Received message with unknown identity key.
|
||||
Click to process and display.
|
||||
</p>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='outgoing-key-conflict'>
|
||||
<p>
|
||||
This contact's identity key.has changed.
|
||||
Click to process and display.
|
||||
</p>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='generic-error'>
|
||||
<p>{{ message }}</p>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='contact-detail'>
|
||||
<div>
|
||||
<span class='avatar'><img src='{{ avatar_url }}' /></span>
|
||||
<span class='name'>{{ name }}</span>
|
||||
{{ #conflict }}
|
||||
<button class='conflict'><span>Verify</span></button>
|
||||
{{ /conflict }}
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='key-conflict-dialogue'>
|
||||
<div class='content'>
|
||||
<p> {{ message }} </p>
|
||||
<p>
|
||||
You may wish to <span class='verify'>verify</span> this contact.
|
||||
</p>
|
||||
<p>
|
||||
<button class='cancel'>Cancel</button>
|
||||
<button class='resolve'>Accept new key</button>
|
||||
</p>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/javascript" src="js/components.js"></script>
|
||||
<script type="text/javascript" src="js/libtextsecure.js"></script>
|
||||
|
||||
|
@ -223,6 +249,8 @@
|
|||
<script type="text/javascript" src="js/views/end_session_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/group_update_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/attachment_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/key_conflict_dialogue_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/error_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/message_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/key_verification_view.js"></script>
|
||||
<script type="text/javascript" src="js/views/message_detail_view.js"></script>
|
||||
|
|
|
@ -221,33 +221,24 @@
|
|||
return this.avatarUrl || '/images/default.png';
|
||||
},
|
||||
|
||||
resolveConflicts: function() {
|
||||
if (!this.isPrivate()) {
|
||||
throw "Can't call resolveConflicts on non-private conversation";
|
||||
resolveConflicts: function(number) {
|
||||
if (this.isPrivate()) {
|
||||
number = this.id;
|
||||
} else if (!_.find(this.get('members'), number)) {
|
||||
throw 'Tried to resolve conflicts for a unknown group member';
|
||||
}
|
||||
|
||||
if (!this.messageCollection.find(function(m) { return m.getKeyConflict(); })) {
|
||||
throw "No conflicts to resolve";
|
||||
if (!this.messageCollection.hasKeyConflicts()) {
|
||||
throw 'No conflicts to resolve';
|
||||
}
|
||||
|
||||
textsecure.storage.devices.removeIdentityKeyForNumber(this.get('id'));
|
||||
textsecure.storage.devices.removeIdentityKeyForNumber(number);
|
||||
this.messageCollection.each(function(message) {
|
||||
var conflict = message.getKeyConflict();
|
||||
if (conflict) {
|
||||
new textsecure.ReplayableError(conflict).replay().
|
||||
then(function(pushMessageContent) {
|
||||
message.save('errors', []);
|
||||
if (message.isIncoming()) {
|
||||
extension.trigger('message:decrypted', {
|
||||
message_id: message.id,
|
||||
data: pushMessageContent
|
||||
});
|
||||
}
|
||||
});
|
||||
if (message.hasKeyConflict(number)) {
|
||||
message.resolveConflict(number);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Whisper.ConversationCollection = Backbone.Collection.extend({
|
||||
|
|
|
@ -51,11 +51,53 @@
|
|||
isOutgoing: function() {
|
||||
return this.get('type') === 'outgoing';
|
||||
},
|
||||
getKeyConflict: function() {
|
||||
return _.find(this.get('errors'), function(e) {
|
||||
return ( e.name === 'IncomingIdentityKeyError' ||
|
||||
e.name === 'OutgoingIdentityKeyError');
|
||||
hasKeyConflicts: function() {
|
||||
return _.any(this.get('errors'), function(e) {
|
||||
return (e.name === 'IncomingIdentityKeyError' ||
|
||||
e.name === 'OutgoingIdentityKeyError');
|
||||
});
|
||||
},
|
||||
hasKeyConflict: function(number) {
|
||||
return _.any(this.get('errors'), function(e) {
|
||||
return (e.name === 'IncomingIdentityKeyError' ||
|
||||
e.name === 'OutgoingIdentityKeyError') &&
|
||||
e.number === number;
|
||||
});
|
||||
},
|
||||
getKeyConflict: function(number) {
|
||||
return _.find(this.get('errors'), function(e) {
|
||||
return (e.name === 'IncomingIdentityKeyError' ||
|
||||
e.name === 'OutgoingIdentityKeyError') &&
|
||||
e.number === number;
|
||||
});
|
||||
},
|
||||
resolveConflict: function(number) {
|
||||
var error = this.getKeyConflict(number);
|
||||
if (error) {
|
||||
var promise = new textsecure.ReplayableError(error).replay();
|
||||
if (this.isIncoming()) {
|
||||
promise.then(function(pushMessageContent) {
|
||||
extension.trigger('message:decrypted', {
|
||||
message_id: this.id,
|
||||
data: pushMessageContent
|
||||
});
|
||||
this.save('errors', []);
|
||||
}.bind(this)).catch(function(e) {
|
||||
//this.save('errors', [_.pick(e, ['name', 'message'])]);
|
||||
var errors = this.get('errors').concat(
|
||||
_.pick(e, ['name', 'message'])
|
||||
);
|
||||
this.save('errors', errors);
|
||||
}.bind(this));
|
||||
} else {
|
||||
promise.then(function() {
|
||||
this.save('errors', _.reject(this.get('errors'), function(e) {
|
||||
return e.name === 'OutgoingIdentityKeyError' &&
|
||||
e.number === number;
|
||||
}));
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -100,6 +142,10 @@
|
|||
// TODO pagination/infinite scroll
|
||||
// limit: 10, offset: page*10,
|
||||
return this.fetch(options);
|
||||
},
|
||||
|
||||
hasKeyConflicts: function() {
|
||||
return this.any(function(m) { return m.hasKeyConflicts(); });
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
this.listenTo(view, 'back', function() {
|
||||
view.remove();
|
||||
this.$el.show();
|
||||
}.bind(this));
|
||||
});
|
||||
},
|
||||
|
||||
closeMenu: function(e) {
|
||||
|
|
72
js/views/error_view.js
Normal file
72
js/views/error_view.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
var ErrorView = Backbone.View.extend({
|
||||
className: 'error',
|
||||
initialize: function() {
|
||||
this.template = $('#generic-error').html();
|
||||
Mustache.parse(this.template);
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(Mustache.render(this.template, this.model));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var KeyConflictView = ErrorView.extend({
|
||||
className: 'key-conflict',
|
||||
initialize: function(options) {
|
||||
this.message = options.message;
|
||||
if (this.message.isIncoming()) {
|
||||
this.template = $('#incoming-key-conflict').html();
|
||||
} else if (this.message.isOutgoing()) {
|
||||
this.template = $('#outgoing-key-conflict').html();
|
||||
}
|
||||
Mustache.parse(this.template);
|
||||
},
|
||||
events: {
|
||||
'click': 'select'
|
||||
},
|
||||
select: function() {
|
||||
this.$el.trigger('select', {message: this.message});
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.MessageErrorView = Backbone.View.extend({
|
||||
className: 'error',
|
||||
initialize: function(options) {
|
||||
if (this.model.name === 'IncomingIdentityKeyError' ||
|
||||
this.model.name === 'OutgoingIdentityKeyError') {
|
||||
this.view = new KeyConflictView({
|
||||
model : this.model,
|
||||
message : options.message
|
||||
});
|
||||
} else {
|
||||
this.view = new ErrorView({ model: this.model });
|
||||
}
|
||||
this.$el.append(this.view.el);
|
||||
this.view.render();
|
||||
},
|
||||
render: function() {
|
||||
this.view.render();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
})();
|
54
js/views/key_conflict_dialogue_view.js
Normal file
54
js/views/key_conflict_dialogue_view.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/* vim: ts=4:sw=4:expandtab
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.KeyConflictDialogueView = Backbone.View.extend({
|
||||
className: 'key-conflict-dialogue',
|
||||
initialize: function(options) {
|
||||
this.template = $('#key-conflict-dialogue').html();
|
||||
Mustache.parse(this.template);
|
||||
this.conversation = options.conversation;
|
||||
},
|
||||
events: {
|
||||
'click .verify': 'triggerVerify',
|
||||
'click .resolve': 'resolve',
|
||||
'click .cancel': 'remove',
|
||||
'click': 'clickOut'
|
||||
},
|
||||
triggerVerify: function() {
|
||||
this.trigger('verify', {number: this.model.number});
|
||||
},
|
||||
clickOut: function(e) {
|
||||
if (!$(e.target).closest('.content').length) {
|
||||
this.remove();
|
||||
}
|
||||
},
|
||||
resolve: function() {
|
||||
new Promise(function() {
|
||||
this.conversation.resolveConflicts(this.model.number);
|
||||
}.bind(this));
|
||||
this.trigger('resolve');
|
||||
this.remove();
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(Mustache.render(this.template, this.model));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -17,7 +17,30 @@
|
|||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.MessageDetailView = Whisper.View.extend({
|
||||
var ContactView = Backbone.View.extend({
|
||||
className: 'contact-detail',
|
||||
initialize: function(options) {
|
||||
this.template = $('#contact-detail').html();
|
||||
Mustache.parse(this.template);
|
||||
this.conflict = options.conflict;
|
||||
},
|
||||
events: {
|
||||
'click .conflict': 'triggerConflict'
|
||||
},
|
||||
triggerConflict: function() {
|
||||
this.$el.trigger('conflict', {conflict: this.conflict});
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(Mustache.render(this.template, {
|
||||
name : this.model.getTitle(),
|
||||
avatar_url : this.model.getAvatarUrl(),
|
||||
conflict : this.conflict
|
||||
}));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.MessageDetailView = Backbone.View.extend({
|
||||
className: 'message-detail',
|
||||
template: $('#message-detail').html(),
|
||||
initialize: function(options) {
|
||||
|
@ -26,7 +49,7 @@
|
|||
},
|
||||
events: {
|
||||
'click .back': 'goBack',
|
||||
'verify': 'verify'
|
||||
'conflict': 'conflictDialogue'
|
||||
},
|
||||
goBack: function() {
|
||||
this.trigger('back');
|
||||
|
@ -55,19 +78,33 @@
|
|||
return this.conversation.contactCollection.models;
|
||||
}
|
||||
},
|
||||
conflictDialogue: function(e, data) {
|
||||
var view = new Whisper.KeyConflictDialogueView({
|
||||
model: data.conflict,
|
||||
conversation: this.conversation
|
||||
});
|
||||
view.render().$el.appendTo(this.$el);
|
||||
this.listenTo(view, 'verify', function(data) {
|
||||
this.verify(data.number);
|
||||
});
|
||||
this.listenTo(view, 'resolve', function() {
|
||||
this.render();
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
this.$el.html(Mustache.render(this.template, {
|
||||
sent_at: moment(this.model.get('sent_at')).toString(),
|
||||
received_at: moment(this.model.get('received_at')).toString(),
|
||||
tofrom: this.model.isIncoming() ? 'From' : 'To',
|
||||
contacts: this.contacts().map(function(contact) {
|
||||
return {
|
||||
name : contact.getTitle(),
|
||||
avatar_url : contact.getAvatarUrl()
|
||||
};
|
||||
}.bind(this))
|
||||
sent_at : moment(this.model.get('sent_at')).toString(),
|
||||
received_at : moment(this.model.get('received_at')).toString(),
|
||||
tofrom : this.model.isIncoming() ? 'From' : 'To',
|
||||
}));
|
||||
this.view.render().$el.prependTo(this.$el.find('.message-container'));
|
||||
|
||||
this.conversation.contactCollection.each(function(contact) {
|
||||
var v = new ContactView({
|
||||
model: contact,
|
||||
conflict: this.model.getKeyConflict(contact.id)
|
||||
}).render().$el.appendTo(this.$el.find('.contacts'));
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -17,20 +17,6 @@
|
|||
'use strict';
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
var ErrorView = Backbone.View.extend({
|
||||
className: 'error',
|
||||
events: {
|
||||
'click' : 'replay'
|
||||
},
|
||||
replay: function() {
|
||||
new window.textsecure.ReplayableError(this.model).replay();
|
||||
},
|
||||
render: function() {
|
||||
this.$el.text(this.model.message);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var ContentMessageView = Whisper.View.extend({
|
||||
tagName: 'div',
|
||||
template: $('#message').html(),
|
||||
|
@ -75,10 +61,13 @@
|
|||
|
||||
var errors = this.model.get('errors');
|
||||
if (errors && errors.length) {
|
||||
this.$el.find('.bubble').append(
|
||||
this.$el.find('.bubble').prepend(
|
||||
errors.map(function(error) {
|
||||
return new ErrorView({model: error}).render().el;
|
||||
})
|
||||
return new Whisper.MessageErrorView({
|
||||
model: error,
|
||||
message: this.model
|
||||
}).render().el;
|
||||
}.bind(this))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
padding: $header-height 0 0;
|
||||
background: $grey_l;
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
background: white;
|
||||
padding: 1em 0;
|
||||
|
@ -47,6 +52,34 @@
|
|||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.conflict {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
font-weight: bold;
|
||||
background: #d00;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: url('/images/error.png') no-repeat center center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-update {
|
||||
|
@ -112,10 +145,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
left: -2px;
|
||||
|
@ -148,6 +177,9 @@
|
|||
.content a {
|
||||
word-break: break-all
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.incoming {
|
||||
|
@ -225,6 +257,40 @@
|
|||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.key-conflict {
|
||||
padding: 15px 10px;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key-conflict-dialogue {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: $header-height;
|
||||
|
||||
.content {
|
||||
padding: 1em;
|
||||
background: white;
|
||||
color: black;
|
||||
box-shadow: 0 0 5px 0 black;
|
||||
}
|
||||
|
||||
.verify {
|
||||
color: $blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
|
|
|
@ -7,6 +7,7 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
|
|
@ -19,6 +19,7 @@ html {
|
|||
height: 100%; }
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
@ -379,6 +380,9 @@ input.search {
|
|||
.message-detail {
|
||||
padding: 36px 0 0;
|
||||
background: #f3f3f3; }
|
||||
.message-detail .container {
|
||||
height: 100%;
|
||||
overflow: auto; }
|
||||
.message-detail .message-container {
|
||||
background: white;
|
||||
padding: 1em 0; }
|
||||
|
@ -390,6 +394,26 @@ input.search {
|
|||
text-align: right;
|
||||
font-weight: bold;
|
||||
padding-right: 1em; }
|
||||
.message-detail .avatar img {
|
||||
vertical-align: middle; }
|
||||
.message-detail .conflict {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
font-weight: bold;
|
||||
background: #d00; }
|
||||
.message-detail .conflict:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: url("/images/error.png") no-repeat center center;
|
||||
background-size: 100%; }
|
||||
.message-detail .conflict span {
|
||||
vertical-align: middle;
|
||||
padding-left: 5px; }
|
||||
|
||||
.group-update {
|
||||
font-size: smaller; }
|
||||
|
@ -437,9 +461,6 @@ input.search {
|
|||
content: " ";
|
||||
clear: both;
|
||||
height: 0; }
|
||||
.message-detail p,
|
||||
.message-list p {
|
||||
margin: 0; }
|
||||
.message-detail .bubble,
|
||||
.message-list .bubble {
|
||||
position: relative;
|
||||
|
@ -470,6 +491,9 @@ input.search {
|
|||
.message-detail .bubble .content a,
|
||||
.message-list .bubble .content a {
|
||||
word-break: break-all; }
|
||||
.message-detail .bubble p,
|
||||
.message-list .bubble p {
|
||||
margin: 0; }
|
||||
.message-detail .incoming .bubble,
|
||||
.message-list .incoming .bubble {
|
||||
color: #454545;
|
||||
|
@ -528,6 +552,32 @@ input.search {
|
|||
font: small;
|
||||
font-style: italic;
|
||||
opacity: 0.8; }
|
||||
.message-detail .error,
|
||||
.message-list .error {
|
||||
font-style: italic; }
|
||||
.message-detail .key-conflict,
|
||||
.message-list .key-conflict {
|
||||
padding: 15px 10px; }
|
||||
.message-detail .key-conflict button,
|
||||
.message-list .key-conflict button {
|
||||
margin-top: 5px; }
|
||||
|
||||
.key-conflict-dialogue {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 36px; }
|
||||
.key-conflict-dialogue .content {
|
||||
padding: 1em;
|
||||
background: white;
|
||||
color: black;
|
||||
box-shadow: 0 0 5px 0 black; }
|
||||
.key-conflict-dialogue .verify {
|
||||
color: #2a92e7;
|
||||
text-decoration: underline;
|
||||
cursor: pointer; }
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
|
|
Loading…
Reference in a new issue