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 }} -
- - {{ name }}
- {{ #conflict }} - - {{ /conflict }} -
- {{ /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;