move offline network view to react
parent
f9ab90fb71
commit
977569cde0
@ -1,133 +0,0 @@
|
|||||||
/* global Whisper, extension, Backbone, moment, i18n */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
const DISCONNECTED_DELAY = 30000;
|
|
||||||
|
|
||||||
Whisper.NetworkStatusView = Whisper.View.extend({
|
|
||||||
className: 'network-status',
|
|
||||||
templateName: 'networkStatus',
|
|
||||||
initialize() {
|
|
||||||
this.$el.hide();
|
|
||||||
|
|
||||||
this.renderIntervalHandle = setInterval(this.update.bind(this), 5000);
|
|
||||||
extension.windows.onClosed(() => {
|
|
||||||
clearInterval(this.renderIntervalHandle);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(this.finishConnectingGracePeriod.bind(this), 5000);
|
|
||||||
|
|
||||||
this.withinConnectingGracePeriod = true;
|
|
||||||
this.setSocketReconnectInterval(null);
|
|
||||||
|
|
||||||
window.addEventListener('online', this.update.bind(this));
|
|
||||||
window.addEventListener('offline', this.update.bind(this));
|
|
||||||
|
|
||||||
this.model = new Backbone.Model();
|
|
||||||
this.listenTo(this.model, 'change', this.onChange);
|
|
||||||
this.connectedTimer = null;
|
|
||||||
},
|
|
||||||
onReconnectTimer() {
|
|
||||||
this.setSocketReconnectInterval(60000);
|
|
||||||
},
|
|
||||||
finishConnectingGracePeriod() {
|
|
||||||
this.withinConnectingGracePeriod = false;
|
|
||||||
},
|
|
||||||
setSocketReconnectInterval(millis) {
|
|
||||||
this.socketReconnectWaitDuration = moment.duration(millis);
|
|
||||||
},
|
|
||||||
navigatorOnLine() {
|
|
||||||
return navigator.onLine;
|
|
||||||
},
|
|
||||||
getSocketStatus() {
|
|
||||||
return window.getSocketStatus();
|
|
||||||
},
|
|
||||||
getNetworkStatus(shortCircuit = false) {
|
|
||||||
let message = '';
|
|
||||||
let instructions = '';
|
|
||||||
let hasInterruption = false;
|
|
||||||
|
|
||||||
const socketStatus = this.getSocketStatus();
|
|
||||||
switch (socketStatus) {
|
|
||||||
case WebSocket.CONNECTING:
|
|
||||||
message = i18n('connecting');
|
|
||||||
this.setSocketReconnectInterval(null);
|
|
||||||
window.clearTimeout(this.connectedTimer);
|
|
||||||
this.connectedTimer = null;
|
|
||||||
break;
|
|
||||||
case WebSocket.OPEN:
|
|
||||||
this.setSocketReconnectInterval(null);
|
|
||||||
window.clearTimeout(this.connectedTimer);
|
|
||||||
this.connectedTimer = null;
|
|
||||||
break;
|
|
||||||
case WebSocket.CLOSED:
|
|
||||||
// Intentional fallthrough
|
|
||||||
case WebSocket.CLOSING:
|
|
||||||
// Intentional fallthrough
|
|
||||||
default: {
|
|
||||||
const markOffline = () => {
|
|
||||||
message = i18n('offline');
|
|
||||||
instructions = i18n('checkNetworkConnection');
|
|
||||||
hasInterruption = true;
|
|
||||||
};
|
|
||||||
if (shortCircuit) {
|
|
||||||
// Used to skip the timer for testing
|
|
||||||
markOffline();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!this.connectedTimer) {
|
|
||||||
// Mark offline if disconnected for 30 seconds
|
|
||||||
this.connectedTimer = window.setTimeout(() => {
|
|
||||||
markOffline();
|
|
||||||
}, DISCONNECTED_DELAY);
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
instructions,
|
|
||||||
hasInterruption,
|
|
||||||
action: null,
|
|
||||||
buttonClass: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
const status = this.getNetworkStatus();
|
|
||||||
this.model.set(status);
|
|
||||||
},
|
|
||||||
render_attributes() {
|
|
||||||
return this.model.attributes;
|
|
||||||
},
|
|
||||||
onChange() {
|
|
||||||
this.render();
|
|
||||||
if (this.model.attributes.hasInterruption) {
|
|
||||||
this.$el.slideDown();
|
|
||||||
} else {
|
|
||||||
this.$el.hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
@ -1,160 +0,0 @@
|
|||||||
/* global _, $, Whisper */
|
|
||||||
|
|
||||||
describe('NetworkStatusView', () => {
|
|
||||||
describe('getNetworkStatus', () => {
|
|
||||||
let networkStatusView;
|
|
||||||
let socketStatus = WebSocket.OPEN;
|
|
||||||
|
|
||||||
let oldGetSocketStatus;
|
|
||||||
|
|
||||||
/* BEGIN stubbing globals */
|
|
||||||
before(() => {
|
|
||||||
oldGetSocketStatus = window.getSocketStatus;
|
|
||||||
window.getSocketStatus = () => socketStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
window.getSocketStatus = oldGetSocketStatus;
|
|
||||||
|
|
||||||
// It turns out that continued calls to window.getSocketStatus happen
|
|
||||||
// because we host NetworkStatusView in three mock interfaces, and the view
|
|
||||||
// checks every N seconds. That results in infinite errors unless there is
|
|
||||||
// something to call.
|
|
||||||
window.getSocketStatus = () => WebSocket.OPEN;
|
|
||||||
});
|
|
||||||
/* END stubbing globals */
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
networkStatusView = new Whisper.NetworkStatusView();
|
|
||||||
$('.network-status-container').append(networkStatusView.el);
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
// prevents huge number of errors on console after running tests
|
|
||||||
clearInterval(networkStatusView.renderIntervalHandle);
|
|
||||||
networkStatusView = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('initialization', () => {
|
|
||||||
it('should have an empty interval', () => {
|
|
||||||
assert.equal(
|
|
||||||
networkStatusView.socketReconnectWaitDuration.asSeconds(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('network status with no connection', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
networkStatusView.navigatorOnLine = () => false;
|
|
||||||
});
|
|
||||||
it('should be interrupted', () => {
|
|
||||||
networkStatusView.update();
|
|
||||||
const status = networkStatusView.getNetworkStatus();
|
|
||||||
assert(status.hasInterruption);
|
|
||||||
assert.equal(status.instructions, 'Check your network connection.');
|
|
||||||
});
|
|
||||||
it('should display an offline message', () => {
|
|
||||||
networkStatusView.update();
|
|
||||||
assert.match(networkStatusView.$el.text(), /Offline/);
|
|
||||||
});
|
|
||||||
it('should override socket status', () => {
|
|
||||||
_([
|
|
||||||
WebSocket.CONNECTING,
|
|
||||||
WebSocket.OPEN,
|
|
||||||
WebSocket.CLOSING,
|
|
||||||
WebSocket.CLOSED,
|
|
||||||
]).forEach(socketStatusVal => {
|
|
||||||
socketStatus = socketStatusVal;
|
|
||||||
networkStatusView.update();
|
|
||||||
assert.match(networkStatusView.$el.text(), /Offline/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should override registration status', () => {
|
|
||||||
Whisper.Registration.remove();
|
|
||||||
networkStatusView.update();
|
|
||||||
assert.match(networkStatusView.$el.text(), /Offline/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('network status when registration is done', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
networkStatusView.navigatorOnLine = () => true;
|
|
||||||
Whisper.Registration.markDone();
|
|
||||||
networkStatusView.update();
|
|
||||||
});
|
|
||||||
it('should not display an unlinked message', () => {
|
|
||||||
networkStatusView.update();
|
|
||||||
assert.notMatch(networkStatusView.$el.text(), /Relink/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('network status when socket is connecting', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Whisper.Registration.markDone();
|
|
||||||
socketStatus = WebSocket.CONNECTING;
|
|
||||||
networkStatusView.update();
|
|
||||||
});
|
|
||||||
it('it should display a connecting string if connecting and not in the connecting grace period', () => {
|
|
||||||
networkStatusView.withinConnectingGracePeriod = false;
|
|
||||||
networkStatusView.getNetworkStatus();
|
|
||||||
|
|
||||||
assert.match(networkStatusView.$el.text(), /Connecting/);
|
|
||||||
});
|
|
||||||
it('it should not be interrupted if in connecting grace period', () => {
|
|
||||||
assert(networkStatusView.withinConnectingGracePeriod);
|
|
||||||
const status = networkStatusView.getNetworkStatus();
|
|
||||||
|
|
||||||
assert.match(networkStatusView.$el.text(), /Connecting/);
|
|
||||||
assert(!status.hasInterruption);
|
|
||||||
});
|
|
||||||
it('it should be interrupted if connecting grace period is over', () => {
|
|
||||||
networkStatusView.withinConnectingGracePeriod = false;
|
|
||||||
const status = networkStatusView.getNetworkStatus();
|
|
||||||
|
|
||||||
assert(status.hasInterruption);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('network status when socket is open', () => {
|
|
||||||
before(() => {
|
|
||||||
socketStatus = WebSocket.OPEN;
|
|
||||||
});
|
|
||||||
it('should not be interrupted', () => {
|
|
||||||
const 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', () => {
|
|
||||||
_([WebSocket.CLOSED, WebSocket.CLOSING]).forEach(socketStatusVal => {
|
|
||||||
it('should be interrupted', () => {
|
|
||||||
socketStatus = socketStatusVal;
|
|
||||||
networkStatusView.update();
|
|
||||||
const shortCircuit = true;
|
|
||||||
const status = networkStatusView.getNetworkStatus(shortCircuit);
|
|
||||||
assert(status.hasInterruption);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('the socket reconnect interval', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
socketStatus = WebSocket.CLOSED;
|
|
||||||
networkStatusView.setSocketReconnectInterval(61000);
|
|
||||||
networkStatusView.update();
|
|
||||||
});
|
|
||||||
it('should format the message based on the socketReconnectWaitDuration property', () => {
|
|
||||||
assert.equal(
|
|
||||||
networkStatusView.socketReconnectWaitDuration.asSeconds(),
|
|
||||||
61
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
networkStatusView.$('.network-status-message:last').text(),
|
|
||||||
/Attempting reconnect/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should be reset by changing the socketStatus to CONNECTING', () => {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useNetwork } from './useNetwork';
|
||||||
|
|
||||||
|
type ContainerProps = {
|
||||||
|
show: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OfflineContainer = styled.div<ContainerProps>`
|
||||||
|
background: ${props => props.theme.colors.accent};
|
||||||
|
color: ${props => props.theme.colors.textColor};
|
||||||
|
padding: ${props => (props.show ? props.theme.common.margins.sm : '0px')};
|
||||||
|
margin: ${props => (props.show ? props.theme.common.margins.xs : '0px')};
|
||||||
|
height: ${props => (props.show ? 'auto' : '0px')};
|
||||||
|
overflow: hidden;
|
||||||
|
transition: ${props => props.theme.common.animations.defaultDuration};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OfflineTitle = styled.h3`
|
||||||
|
padding-top: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OfflineMessage = styled.div``;
|
||||||
|
|
||||||
|
export const SessionOffline = () => {
|
||||||
|
const isOnline = useNetwork();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OfflineContainer show={!isOnline}>
|
||||||
|
<OfflineTitle>{window.i18n('offline')}</OfflineTitle>
|
||||||
|
<OfflineMessage>{window.i18n('checkNetworkConnection')}</OfflineMessage>
|
||||||
|
</OfflineContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useNetwork() {
|
||||||
|
const [isOnline, setNetwork] = useState(window.navigator.onLine);
|
||||||
|
const updateNetwork = () => {
|
||||||
|
setNetwork(window.navigator.onLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('offline', updateNetwork);
|
||||||
|
window.addEventListener('online', updateNetwork);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('offline', updateNetwork);
|
||||||
|
window.removeEventListener('online', updateNetwork);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return isOnline;
|
||||||
|
}
|
Loading…
Reference in New Issue