diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e52d15b23..61a5424c6 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -3,6 +3,10 @@
"message": "Me",
"description": "The label for yourself when shown in a group member list"
},
+ "view": {
+ "message": "View",
+ "description": "Used as a label on a button allowing user to see more information"
+ },
"youLeftTheGroup": {
"message": "You left the group",
"description": "Displayed when a user can't send a message because they have left the group"
@@ -105,9 +109,15 @@
}
}
},
- "retryDescription": {
- "message": "You can retry sending this message to each of the failed recipients with these buttons:",
- "description": "Shows on the message details view when it's a message error which can be retried."
+ "identityKeyErrorOnSend": {
+ "message": "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your saftey number with this contact.",
+ "description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
+ "placeholders": {
+ "name": {
+ "content": "$1",
+ "example": "Bob"
+ }
+ }
},
"sendAnyway": {
"message": "Send Anyway",
diff --git a/background.html b/background.html
index fa26a92ef..17bac7fbd 100644
--- a/background.html
+++ b/background.html
@@ -297,14 +297,6 @@
+
-
-
+
+
+
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 9d9e56ab3..ffa667d6f 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -202,6 +202,14 @@
}.bind(this)));
}
},
+ setTrusted: function() {
+ if (!this.isPrivate()) {
+ throw new Error('You cannot set a group conversation as trusted. ' +
+ 'You must set individual contacts as trusted.');
+ }
+
+ return textsecure.storage.protocol.setApproval(this.id, true);
+ },
isUntrusted: function() {
if (this.isPrivate()) {
return textsecure.storage.protocol.isUntrusted(this.id);
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index daad70db7..f25dc9416 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -202,6 +202,13 @@
}.bind(this)));
},
+
+ markAllAsApproved: function(untrusted) {
+ return Promise.all(untrusted.map(function(contact) {
+ return contact.setApproved();
+ }.bind(this)));
+ },
+
openSafetyNumberScreens: function(unverified) {
if (unverified.length === 1) {
this.showSafetyNumber(null, unverified.at(0));
@@ -620,7 +627,10 @@
messageDetail: function(e, data) {
var view = new Whisper.MessageDetailView({
model: data.message,
- conversation: this.model
+ conversation: this.model,
+ // we pass these in to allow nested panels
+ listenBack: this.listenBack.bind(this),
+ resetPanel: this.resetPanel.bind(this)
});
this.listenBack(view);
view.render();
@@ -749,10 +759,17 @@
_.defaults(options, {force: false});
this.model.getUntrusted().then(function(contacts) {
- if (!contacts.length || options.force) {
+
+ if (!contacts.length) {
return this.sendMessage(e);
}
+ if (options.force) {
+ return this.markAllAsApproved(contacts).then(function() {
+ this.sendMessage(e);
+ }.bind(this));
+ }
+
this.showSendConfirmationDialog(e, contacts);
}.bind(this));
},
diff --git a/js/views/identity_key_send_error_view.js b/js/views/identity_key_send_error_view.js
new file mode 100644
index 000000000..99c5d0666
--- /dev/null
+++ b/js/views/identity_key_send_error_view.js
@@ -0,0 +1,51 @@
+/*
+ * vim: ts=4:sw=4:expandtab
+ */
+(function () {
+ 'use strict';
+ window.Whisper = window.Whisper || {};
+
+ Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({
+ className: 'identity-key-send-error panel',
+ templateName: 'identity-key-send-error',
+ initialize: function(options) {
+ this.listenBack = options.listenBack;
+ this.resetPanel = options.resetPanel;
+
+ this.wasUnverified = this.model.isUnverified();
+ this.listenTo(this.model, 'change', this.render);
+ },
+ events: {
+ 'click .show-safety-number': 'showSafetyNumber',
+ 'click .send-anyway': 'sendAnyway',
+ 'click .cancel': 'cancel'
+ },
+ showSafetyNumber: function() {
+ var view = new Whisper.KeyVerificationPanelView({
+ model: this.model
+ });
+ this.listenBack(view);
+ },
+ sendAnyway: function() {
+ this.resetPanel();
+ this.trigger('send-anyway');
+ },
+ cancel: function() {
+ this.resetPanel();
+ },
+ render_attributes: function() {
+ var send = i18n('sendAnyway');
+ if (this.wasUnverified && !this.model.isUnverified()) {
+ send = i18n('resend');
+ }
+
+ var errorExplanation = i18n('identityKeyErrorOnSend', this.model.getTitle(), this.model.getTitle());
+ return {
+ errorExplanation : errorExplanation,
+ showSafetyNumber : i18n('showSafetyNumber'),
+ sendAnyway : send,
+ cancel : i18n('cancel')
+ };
+ }
+ });
+})();
diff --git a/js/views/message_detail_view.js b/js/views/message_detail_view.js
index b9de420da..fccc19f30 100644
--- a/js/views/message_detail_view.js
+++ b/js/views/message_detail_view.js
@@ -9,18 +9,68 @@
className: 'contact-detail',
templateName: 'contact-detail',
initialize: function(options) {
- this.errors = _.reject(options.errors, function(e) {
- return (e.name === 'OutgoingIdentityKeyError' ||
- e.name === 'OutgoingMessageError' ||
- e.name === 'SendMessageNetworkError');
+ this.listenBack = options.listenBack;
+ this.resetPanel = options.resetPanel;
+ this.message = options.message;
+
+ var newIdentity = i18n('newIdentity');
+ this.errors = _.map(options.errors, function(error) {
+ if (error.name === 'OutgoingIdentityKeyError') {
+ error.message = newIdentity;
+ }
+ return error;
+ });
+ this.outgoingKeyError = _.find(this.errors, function(error) {
+ return error.name === 'OutgoingIdentityKeyError';
});
+ },
+ events: {
+ 'click': 'onClick'
+ },
+ onClick: function() {
+ if (this.outgoingKeyError) {
+ var view = new Whisper.IdentityKeySendErrorPanelView({
+ model: this.model,
+ listenBack: this.listenBack,
+ resetPanel: this.resetPanel
+ });
+
+ this.listenTo(view, 'send-anyway', this.onSendAnyway);
+ view.render();
+ this.listenBack(view);
+ }
+ // TODO: is there anything we might want to do here? Pop a confirmation dialog? Ideally it would always have error-specific help.
+ },
+ forceSend: function() {
+ this.model.updateVerified().then(function() {
+ if (this.model.isUnverified()) {
+ return this.model.setVerifiedDefault();
+ }
+ }.bind(this)).then(function() {
+ return this.model.isUntrusted();
+ }.bind(this)).then(function(untrusted) {
+ if (untrusted) {
+ return this.model.setTrusted();
+ }
+ }.bind(this)).then(function() {
+ this.message.resend(this.outgoingKeyError.number);
+ }.bind(this));
+ },
+ onSendAnyway: function() {
+ if (this.outgoingKeyError) {
+ this.forceSend();
+ }
},
render_attributes: function() {
+ var showButton = Boolean(this.outgoingKeyError);
+
return {
- name : this.model.getTitle(),
- avatar : this.model.getAvatar(),
- errors : this.errors
+ name : this.model.getTitle(),
+ avatar : this.model.getAvatar(),
+ errors : this.errors,
+ showErrorButton : showButton,
+ errorButtonLabel : i18n('view')
};
}
});
@@ -29,23 +79,15 @@
className: 'message-detail panel',
templateName: 'message-detail',
initialize: function(options) {
+ this.listenBack = options.listenBack;
+ this.resetPanel = options.resetPanel;
+
this.view = new Whisper.MessageView({model: this.model});
this.view.render();
this.conversation = options.conversation;
this.listenTo(this.model, 'change', this.render);
},
- events: {
- 'click button.retry': 'onRetry'
- },
- onRetry: function(e) {
- var number = _.find(e.target.attributes, function(attribute) {
- return attribute.name === 'data-number';
- });
- if (number) {
- this.model.resend(number.value);
- }
- },
getContact: function(number) {
var c = ConversationController.get(number);
return {
@@ -53,15 +95,6 @@
title: c ? c.getTitle() : number
};
},
- buildRetryTargetList: function() {
- var targets = _.filter(this.model.get('errors'), function(e) {
- return e.number && e.name === 'OutgoingIdentityKeyError';
- });
-
- return _.map(targets, function(e) {
- return this.getContact(e.number);
- }.bind(this));
- },
contacts: function() {
if (this.model.isIncoming()) {
var number = this.model.get('source');
@@ -71,25 +104,25 @@
}
},
renderContact: function(contact) {
- var grouped = _.groupBy(this.model.get('errors'), 'number');
-
var view = new ContactView({
model: contact,
- errors: grouped[contact.id]
+ errors: this.grouped[contact.id],
+ listenBack: this.listenBack,
+ resetPanel: this.resetPanel,
+ message: this.model
}).render();
this.$('.contacts').append(view.el);
},
render: function() {
- var retryTargets = this.buildRetryTargetList();
- var allowRetry = retryTargets.length > 0;
+ var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) {
+ return Boolean(error.number);
+ });
this.$el.html(Mustache.render(_.result(this, 'template', ''), {
sent_at : moment(this.model.get('sent_at')).format('LLLL'),
received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null,
tofrom : this.model.isIncoming() ? i18n('from') : i18n('to'),
- errors : this.model.get('errors'),
- allowRetry : allowRetry,
- retryTargets : retryTargets,
+ errors : errorsWithoutNumber,
title : i18n('messageDetail'),
sent : i18n('sent'),
received : i18n('received'),
@@ -98,14 +131,21 @@
}));
this.view.$el.prependTo(this.$('.message-container'));
+ this.grouped = _.groupBy(this.model.get('errors'), 'number');
if (this.model.isOutgoing()) {
- this.conversation.contactCollection.reject(function(c) {
+ var contacts = this.conversation.contactCollection.reject(function(c) {
return c.isMe();
- }).forEach(this.renderContact.bind(this));
+ });
+
+ _.sortBy(contacts, function(c) {
+ var prefix = this.grouped[c.id] ? '0' : '1';
+ // this prefix ensures that contacts with errors are listed first;
+ // otherwise it's alphabetical
+ return prefix + c.getTitle();
+ }.bind(this)).forEach(this.renderContact.bind(this));
} else {
- this.renderContact(
- this.conversation.contactCollection.get(this.model.get('source'))
- );
+ var c = this.conversation.contactCollection.get(this.model.get('source'));
+ this.renderContact(c);
}
}
});
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
index 96526899a..4bc0593ac 100644
--- a/stylesheets/_conversation.scss
+++ b/stylesheets/_conversation.scss
@@ -39,7 +39,7 @@
.container {
padding-top: 20px;
- max-width: 950px;
+ max-width: 750px;
margin: 0 auto;
padding: 20px;
}
@@ -77,10 +77,6 @@
}
.key-verification {
- .container {
- max-width: 750px;
- }
-
label {
display: block;
margin: 10px 0;
@@ -150,6 +146,24 @@
}
}
+.identity-key-send-error {
+ button {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+ .explanation {
+ margin-top: 20px;
+ }
+ .safety-number {
+ margin-top: 30px;
+ text-align: center;
+ }
+ .actions {
+ margin-top: 30px;
+ text-align: center;
+ }
+}
+
.message-detail {
background-color: #eee;
@@ -198,6 +212,20 @@
float: right;
}
+ button.error {
+ background-color: red;
+ color: white;
+
+ span.icon.error {
+ display: inline-block;
+ width: 1.25em;
+ height: 1.25em;
+ position: relative;
+ vertical-align: middle;
+ @include color-svg('/images/warning.svg', white);
+ }
+ }
+
.error-message {
margin: 6px 0 0;
font-size: $font-size-small;
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index ca684fa11..45842eb7c 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -1057,7 +1057,7 @@ input.search {
overflow-y: scroll; }
.conversation .panel .container {
padding-top: 20px;
- max-width: 950px;
+ max-width: 750px;
margin: 0 auto;
padding: 20px; }
.conversation .main.panel {
@@ -1083,8 +1083,6 @@ input.search {
.discussion-container {
background-color: #eee; }
-.key-verification .container {
- max-width: 750px; }
.key-verification label {
display: block;
margin: 10px 0;
@@ -1139,6 +1137,18 @@ input.search {
padding: 10px;
margin: 0; }
+.identity-key-send-error button {
+ margin-top: 0px;
+ margin-bottom: 0px; }
+.identity-key-send-error .explanation {
+ margin-top: 20px; }
+.identity-key-send-error .safety-number {
+ margin-top: 30px;
+ text-align: center; }
+.identity-key-send-error .actions {
+ margin-top: 30px;
+ text-align: center; }
+
.message-detail {
background-color: #eee; }
.message-detail .message-container {
@@ -1168,6 +1178,18 @@ input.search {
margin-bottom: 5px; }
.message-detail .contacts .contact-detail .error-icon-container {
float: right; }
+ .message-detail .contacts .contact-detail button.error {
+ background-color: red;
+ color: white; }
+ .message-detail .contacts .contact-detail button.error span.icon.error {
+ display: inline-block;
+ width: 1.25em;
+ height: 1.25em;
+ position: relative;
+ vertical-align: middle;
+ -webkit-mask: url("/images/warning.svg") no-repeat center;
+ -webkit-mask-size: 100%;
+ background-color: white; }
.message-detail .contacts .contact-detail .error-message {
margin: 6px 0 0;
font-size: 0.92857em;
diff --git a/test/index.html b/test/index.html
index 17b9f724b..676bd198d 100644
--- a/test/index.html
+++ b/test/index.html
@@ -316,6 +316,20 @@
+
+