From ed4991974bb1f996ac7e8172ea8bc7fb1ffd98ef Mon Sep 17 00:00:00 2001 From: Sam Vevang Date: Tue, 3 Jan 2017 21:37:56 -0600 Subject: [PATCH] set up a new view for displaying the network status // FREEBIE --- _locales/en/messages.json | 19 ++++ background.html | 16 +++- images/error_red.png | Bin 500 -> 0 bytes images/error_red.svg | 1 + js/background.js | 4 + js/models/conversations.js | 6 ++ js/views/inbox_view.js | 39 ++------ js/views/network_status_view.js | 92 ++++++++++++++++++ stylesheets/_index.scss | 42 +++++---- stylesheets/_ios.scss | 6 -- stylesheets/manifest.css | 34 +++---- test/index.html | 18 +++- test/views/network_status_view_test.js | 125 +++++++++++++++++++++++++ 13 files changed, 326 insertions(+), 76 deletions(-) delete mode 100644 images/error_red.png create mode 100644 images/error_red.svg create mode 100644 js/views/network_status_view.js create mode 100644 test/views/network_status_view_test.js 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 c6f86ea4c27a5a3805cbbf77931dd2d143534dac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|wj^(N7lx+{G7QS=bZ%?|if|Tq zL>4nJa0`PlBg3pY5H=O_WRr%Vg^Dhds^=@FfcZHx;TbdoK8+yAe3-K?Z?8E zx-ZV(-OXG7?oROU@1I1inSo$(%MX9C)TOg^v`&>WZJjFpQR>Ue@)c ziS@Kv)AyU7ui*iLGnaz)M}~yv mZC;_VmE%gNe)- 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() { + + }); + }); + }); +});