diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index aa6776c59..52cf1a998 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -62,6 +62,25 @@
"disconnected": {
"message": "Disconnected"
},
+ "connecting": {
+ "message": "Connecting"
+ },
+ "offline": {
+ "message": "Offline"
+ },
+ "checkNetworkConnection": {
+ "message": "Check your network connection.",
+ "description": "Obvious instructions for when a user's computer loses its network connection"
+ },
+ "attemptingReconnection": {
+ "message": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds",
+ "placeholders": {
+ "reconnect_duration_in_seconds": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
+ },
"submitDebugLog": {
"message": "Submit debug log",
"description": "Menu item and header text for debug log modal, title case."
diff --git a/background.html b/background.html
index 897c12c9f..167682ee5 100644
--- a/background.html
+++ b/background.html
@@ -4,6 +4,7 @@
+
+
+
@@ -537,6 +550,7 @@
+
diff --git a/images/error_red.png b/images/error_red.png
deleted file mode 100644
index c6f86ea4c..000000000
Binary files a/images/error_red.png and /dev/null differ
diff --git a/images/error_red.svg b/images/error_red.svg
new file mode 100644
index 000000000..cdfb9176d
--- /dev/null
+++ b/images/error_red.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/js/background.js b/js/background.js
index 730617f3c..8d0314f1a 100644
--- a/js/background.js
+++ b/js/background.js
@@ -232,6 +232,10 @@
if (navigator.onLine) {
console.log('retrying in 1 minute');
setTimeout(init, 60000);
+
+ if (owsDesktopApp.inboxView) {
+ owsDesktopApp.inboxView.networkStatusView.setSocketReconnectInterval(60000);
+ }
} else {
console.log('offline');
messageReceiver.close();
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 7a2ff6e2d..68e7e5d90 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -32,12 +32,18 @@
return { unreadCount : 0 };
},
+ handleMessageError: function(message, errors) {
+ this.trigger('messageError', message, errors);
+ },
+
initialize: function() {
this.contactCollection = new Backbone.Collection();
this.messageCollection = new Whisper.MessageCollection([], {
conversation: this
});
+ this.messageCollection.on('change:errors', this.handleMessageError, this);
+
this.on('change:avatar', this.updateAvatarUrl);
this.on('destroy', this.revokeAvatarUrl);
this.on('read', this.onReadMessage);
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index a27bf4f8d..6c129018b 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -6,37 +6,6 @@
window.Whisper = window.Whisper || {};
- var SocketView = Whisper.View.extend({
- className: 'status',
- initialize: function() {
- setInterval(this.updateStatus.bind(this), 5000);
- },
- updateStatus: function() {
- var className, message = '';
- if (typeof getSocketStatus === 'function') {
- switch(getSocketStatus()) {
- case WebSocket.CONNECTING:
- className = 'connecting';
- break;
- case WebSocket.OPEN:
- className = 'open';
- break;
- case WebSocket.CLOSING:
- className = 'closing';
- break;
- case WebSocket.CLOSED:
- className = 'closed';
- message = i18n('disconnected');
- break;
- }
- if (!this.$el.hasClass(className)) {
- this.$el.attr('class', className);
- this.$el.text(message);
- }
- }
- }
- });
-
Whisper.ConversationStack = Whisper.View.extend({
className: 'conversation-stack',
open: function(conversation) {
@@ -112,6 +81,11 @@
});
var inboxCollection = getInboxCollection();
+
+ inboxCollection.on('messageError', function() {
+ this.networkStatusView.render();
+ });
+
this.inboxListView = new Whisper.ConversationListView({
el : this.$('.inbox'),
collection : inboxCollection
@@ -139,7 +113,8 @@
this.listenTo(this.searchView, 'open',
this.openConversation.bind(this, null));
- new SocketView().render().$el.appendTo(this.$('.socket-status'));
+ this.networkStatusView = new Whisper.NetworkStatusView();
+ this.$el.find('.network-status-container').append(this.networkStatusView.render().el);
extension.windows.onClosed(function() {
this.inboxListView.stopListening();
diff --git a/js/views/network_status_view.js b/js/views/network_status_view.js
new file mode 100644
index 000000000..48e95e80d
--- /dev/null
+++ b/js/views/network_status_view.js
@@ -0,0 +1,92 @@
+(function () {
+ 'use strict';
+
+ window.Whisper = window.Whisper || {};
+
+ Whisper.NetworkStatusView = Whisper.View.extend({
+ className: 'network-status',
+ initialize: function() {
+ this.$el.hide();
+
+ var renderIntervalHandle = setInterval(this.render.bind(this), 5000);
+ extension.windows.onClosed(function () { clearInterval(renderIntervalHandle); });
+
+ setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
+
+ this.withinConnectingGracePeriod = true;
+ this.setSocketReconnectInterval(null);
+
+ window.addEventListener('online', this.render.bind(this));
+ window.addEventListener('offline', this.render.bind(this));
+ },
+ finishConnectingGracePeriod: function() {
+ this.withinConnectingGracePeriod = false;
+ },
+ setSocketReconnectInterval: function(millis) {
+ this.socketReconnectWaitDuration = moment.duration(millis);
+ },
+ navigatorOnLine: function() { return navigator.onLine; },
+ getSocketStatus: function() { return window.getSocketStatus(); },
+ getNetworkStatus: function() {
+
+ var message = '';
+ var instructions = '';
+ var hasInterruption = false;
+
+ var socketStatus = this.getSocketStatus();
+ switch(socketStatus) {
+ case WebSocket.CONNECTING:
+ message = i18n('connecting');
+ this.setSocketReconnectInterval(null);
+ break;
+ case WebSocket.OPEN:
+ this.setSocketReconnectInterval(null);
+ break;
+ case WebSocket.CLOSING:
+ message = i18n('disconnected');
+ instructions = i18n('checkNetworkConnection');
+ hasInterruption = true;
+ break;
+ case WebSocket.CLOSED:
+ message = i18n('disconnected');
+ instructions = i18n('checkNetworkConnection');
+ hasInterruption = true;
+ break;
+ }
+
+ if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) {
+ hasInterruption = true;
+ }
+ if (this.socketReconnectWaitDuration.asSeconds() > 0) {
+ instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]);
+ }
+ if (!this.navigatorOnLine()) {
+ hasInterruption = true;
+ message = i18n('offline');
+ instructions = i18n('checkNetworkConnection');
+ }
+
+ return {
+ message: message,
+ instructions: instructions,
+ hasInterruption: hasInterruption
+ };
+ },
+ render: function() {
+ var status = this.getNetworkStatus();
+
+ if (status.hasInterruption) {
+ this.$el.slideDown();
+ }
+ else {
+ this.$el.hide();
+ }
+ var template = Whisper.View.Templates['networkStatus'];
+ this.$el.html(Mustache.render(template, status, Whisper.View.Templates));
+ return this;
+ }
+ });
+
+
+
+})();
diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss
index ee4515c98..d68bd272b 100644
--- a/stylesheets/_index.scss
+++ b/stylesheets/_index.scss
@@ -28,25 +28,33 @@
height: 100%;
width: 100%;
}
-}
-.socket-status {
- float: right;
- line-height: $button-height;
+ .network-status-container {
+
+ .network-status {
+
+ height:2 * $button-height;
+ background: url('/images/error_red.svg') no-repeat left 10px center;
+ background-size: 25px 25px;
+ background-color: #fcd156;
+ padding-top: 0.5 * $button-height;
+ padding-left: 2 * $button-height;
+ display: none;
+
+ .network-status-message{
+ h3{
+ padding: 0px;
+ margin: 0px;
+ margin-bottom: 4px;
+ font-size: 14px;
+ }
+ span{
+ font-size: 12px;
+ }
+ }
+
+ }
- * {
- display: inline;
- padding-left: 20px;
- vertical-align: middle;
- }
- .connecting .icon {
- background-color: $blue;
- }
- .closing {
- background-color: $blue_l;
- }
- .closed {
- background: url('/images/error_red.png') no-repeat left center;
}
}
diff --git a/stylesheets/_ios.scss b/stylesheets/_ios.scss
index a6b84e4c2..d56dbd79b 100644
--- a/stylesheets/_ios.scss
+++ b/stylesheets/_ios.scss
@@ -158,10 +158,4 @@ $ios-border-color: rgba(0,0,0,0.1);
.hourglass {
@include hourglass(#999);
}
- .socket-status {
- position: absolute;
- padding-top:-3px;
- top:0;
- right:5px;
- }
}
diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css
index 23a1186ba..23916d01c 100644
--- a/stylesheets/manifest.css
+++ b/stylesheets/manifest.css
@@ -747,20 +747,21 @@ img.emoji {
overflow-y: scroll;
height: 100%;
width: 100%; }
-
-.socket-status {
- float: right;
- line-height: 24px; }
- .socket-status * {
- display: inline;
- padding-left: 20px;
- vertical-align: middle; }
- .socket-status .connecting .icon {
- background-color: #2090ea; }
- .socket-status .closing {
- background-color: #a2d2f4; }
- .socket-status .closed {
- background: url("/images/error_red.png") no-repeat left center; }
+ .gutter .network-status-container .network-status {
+ height: 48px;
+ background: url("/images/error_red.svg") no-repeat left 10px center;
+ background-size: 25px 25px;
+ background-color: #fcd156;
+ padding-top: 12px;
+ padding-left: 48px;
+ display: none; }
+ .gutter .network-status-container .network-status .network-status-message h3 {
+ padding: 0px;
+ margin: 0px;
+ margin-bottom: 4px;
+ font-size: 14px; }
+ .gutter .network-status-container .network-status .network-status-message span {
+ font-size: 12px; }
.conversation-stack {
padding-left: 300px; }
@@ -1559,11 +1560,6 @@ li.entry .error-icon-container {
-webkit-mask: url("/images/hourglass_empty.svg") no-repeat center;
-webkit-mask-size: 100%;
background-color: #999; }
-.ios .socket-status {
- position: absolute;
- padding-top: -3px;
- top: 0;
- right: 5px; }
.android #header {
background-color: #2090ea;
diff --git a/test/index.html b/test/index.html
index 430638830..f5f36b2ea 100644
--- a/test/index.html
+++ b/test/index.html
@@ -18,6 +18,7 @@
+
+
+
@@ -529,7 +542,9 @@
+
+
@@ -542,6 +557,7 @@
+
diff --git a/test/views/network_status_view_test.js b/test/views/network_status_view_test.js
new file mode 100644
index 000000000..2fd09e927
--- /dev/null
+++ b/test/views/network_status_view_test.js
@@ -0,0 +1,125 @@
+
+describe('NetworkStatusView', function() {
+ describe('getNetworkStatus', function() {
+ var networkStatusView;
+ var socketStatus = WebSocket.OPEN;
+
+ var oldGetMessage;
+ var oldGetSocketStatus;
+
+ /* BEGIN stubbing globals */
+ before(function() {
+ oldGetSocketStatus = window.getSocketStatus;
+ /* chrome i18n support is missing in 'regular' webpages */
+ window.chrome.i18n = { getMessage: function(message, args) {
+ // translationMessageName-arg1-arg2
+ return _([message, args]).chain().flatten().compact().value().join('-');
+ }
+ };
+ window.getSocketStatus = function() { return socketStatus; };
+ });
+
+ after(function() {
+ window.getSocketStatus = oldGetSocketStatus;
+ });
+ /* END stubbing globals */
+
+ beforeEach(function(done) {
+
+ networkStatusView = new Whisper.NetworkStatusView();
+ $('.network-status-container').append(networkStatusView.el);
+ // stubbing global
+ done();
+ });
+ describe('initialization', function() {
+ it('should have an empty interval', function() {
+ assert.equal(networkStatusView.socketReconnectWaitDuration.asSeconds(), 0);
+ });
+ });
+ describe('network status with no connection', function() {
+ beforeEach(function() {
+ networkStatusView.navigatorOnLine = function() { return false; };
+ });
+ it('should be interrupted', function() {
+ networkStatusView.render();
+ var status = networkStatusView.getNetworkStatus();
+ assert(status.hasInterruption);
+ assert.equal(status.instructions, "checkNetworkConnection");
+ });
+ it('should display an offline message', function() {
+ networkStatusView.render();
+ assert.match(networkStatusView.$el.text(), /offline/);
+ });
+ it('should override socket status', function() {
+ _([WebSocket.CONNECTING,
+ WebSocket.OPEN,
+ WebSocket.CLOSING,
+ WebSocket.CLOSED]).map(function(socketStatusVal) {
+ socketStatus = socketStatusVal;
+ networkStatusView.render();
+ assert.match(networkStatusView.$el.text(), /offline/);
+ });
+ });
+ });
+ describe('network status when socket is connecting', function() {
+ beforeEach(function() {
+ socketStatus = WebSocket.CONNECTING;
+ networkStatusView.render();
+ });
+ it('it should display a connecting string if connecting and not in the connecting grace period', function() {
+ networkStatusView.withinConnectingGracePeriod = false;
+ var status = networkStatusView.getNetworkStatus();
+
+ assert.match(networkStatusView.$el.text(), /connecting/);
+ });
+ it('it should not be interrupted if in connecting grace period', function() {
+ assert(networkStatusView.withinConnectingGracePeriod);
+ var status = networkStatusView.getNetworkStatus();
+
+ assert.match(networkStatusView.$el.text(), /connecting/);
+ assert(!status.hasInterruption);
+ });
+ it('it should be interrupted if connecting grace period is over', function() {
+ networkStatusView.withinConnectingGracePeriod = false;
+ var status = networkStatusView.getNetworkStatus();
+
+ assert(status.hasInterruption);
+ });
+ });
+ describe('network status when socket is open', function() {
+ before(function() {
+ socketStatus = WebSocket.OPEN;
+ });
+ it('should not be interrupted', function() {
+ var status = networkStatusView.getNetworkStatus();
+ assert(!status.hasInterruption);
+ assert.match(networkStatusView.$el.find('.network-status-message').text().trim(), /^$/);
+ });
+ });
+ describe('network status when socket is closed or closing', function() {
+ _([WebSocket.CLOSED, WebSocket.CLOSING]).map(function(socketStatusVal) {
+ it('should be interrupted', function() {
+ socketStatus = socketStatusVal;
+ networkStatusView.render();
+ var status = networkStatusView.getNetworkStatus();
+ assert(status.hasInterruption);
+ });
+
+ });
+ });
+ describe('the socket reconnect interval', function() {
+ beforeEach(function() {
+ socketStatus = WebSocket.CLOSED;
+ networkStatusView.setSocketReconnectInterval(61000);
+ networkStatusView.render();
+ });
+ it('should format the message based on the socketReconnectWaitDuration property', function() {
+ assert.equal(networkStatusView.socketReconnectWaitDuration.asSeconds(), 61);
+ assert.match(networkStatusView.$('.network-status-message:last').text(), /attemptingReconnection-61/);
+ });
+ it('should be reset by changing the socketStatus to CONNECTING', function() {
+
+ });
+ });
+ });
+});