diff --git a/background.html b/background.html
index 04f0c291..c87572b3 100644
--- a/background.html
+++ b/background.html
@@ -119,28 +119,18 @@
Message Detail
-
-
- Sent | {{ sent_at }} |
- Received | {{ received_at }} |
-
- {{ tofrom }} |
-
-
- {{ /contacts }}
-
- |
-
-
+
+
+
+
+ Sent | {{ sent_at }} |
+ Received | {{ received_at }} |
+
+ {{ tofrom }} |
+ |
+
+
+
+
+
+
+
+
@@ -223,6 +249,8 @@
+
+
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 5512955b..4d66f9c7 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -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({
diff --git a/js/models/messages.js b/js/models/messages.js
index 2b13e2ec..5183082a 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -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(); });
}
});
})();
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index 268ae3b7..907f1367 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -103,7 +103,7 @@
this.listenTo(view, 'back', function() {
view.remove();
this.$el.show();
- }.bind(this));
+ });
},
closeMenu: function(e) {
diff --git a/js/views/error_view.js b/js/views/error_view.js
new file mode 100644
index 00000000..ed2b6f14
--- /dev/null
+++ b/js/views/error_view.js
@@ -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
.
+ */
+(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;
+ }
+ });
+})();
diff --git a/js/views/key_conflict_dialogue_view.js b/js/views/key_conflict_dialogue_view.js
new file mode 100644
index 00000000..065f4a6d
--- /dev/null
+++ b/js/views/key_conflict_dialogue_view.js
@@ -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
.
+ */
+(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;
+ }
+ });
+})();
diff --git a/js/views/message_detail_view.js b/js/views/message_detail_view.js
index a6c27502..7a70643b 100644
--- a/js/views/message_detail_view.js
+++ b/js/views/message_detail_view.js
@@ -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));
}
});
diff --git a/js/views/message_view.js b/js/views/message_view.js
index 54a3fe1d..214248bc 100644
--- a/js/views/message_view.js
+++ b/js/views/message_view.js
@@ -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))
);
}
}
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
index 6c940f7c..5c9dbd12 100644
--- a/stylesheets/_conversation.scss
+++ b/stylesheets/_conversation.scss
@@ -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 {
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index ac8d7253..d443b98d 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -7,6 +7,7 @@ html {
}
body {
+ position: relative;
height: 100%;
width: 100%;
margin: 0;
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index 77dea6cc..0d649d82 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -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;