diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 20ed3859b6..e32d7c7012 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -571,6 +571,10 @@ "message": "Connecting", "description": "Displayed when the desktop client is currently connecting to the server." }, + "connect": { + "message": "Connect", + "description": "Shown to allow the user to manually attempt a reconnect." + }, "connectingHangOn": { "message": "Shouldn't be long...", "description": "Subtext description for when the client is connecting to the server." diff --git a/js/background.js b/js/background.js index ec3de96e79..f7144a094a 100644 --- a/js/background.js +++ b/js/background.js @@ -1446,6 +1446,7 @@ new textsecure.SyncRequest(textsecure.messaging, messageReceiver); let disconnectTimer = null; + let reconnectTimer = null; function onOffline() { window.log.info('offline'); @@ -1499,7 +1500,12 @@ let connectCount = 0; async function connect(firstRun) { - window.log.info('connect', firstRun); + window.log.info('connect', { firstRun, connectCount }); + + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } // Bootstrap our online/offline detection, only the first time we connect if (connectCount === 0 && navigator.onLine) { @@ -1799,6 +1805,11 @@ } } + Whisper.events.on('manualConnect', manualConnect); + function manualConnect() { + connect(); + } + function onConfiguration(ev) { ev.confirm(); @@ -2435,11 +2446,15 @@ return; } - if (error && error.name === 'HTTPError' && error.code === -1) { + if ( + error && + error.name === 'HTTPError' && + (error.code === -1 || error.code === 502) + ) { // Failed to connect to server if (navigator.onLine) { window.log.info('retrying in 1 minute'); - setTimeout(connect, 60000); + reconnectTimer = setTimeout(connect, 60000); Whisper.events.trigger('reconnectTimer'); } diff --git a/ts/components/NetworkStatus.stories.tsx b/ts/components/NetworkStatus.stories.tsx index 762cb7df43..516099dca8 100644 --- a/ts/components/NetworkStatus.stories.tsx +++ b/ts/components/NetworkStatus.stories.tsx @@ -19,6 +19,7 @@ const defaultProps = { isRegistrationDone: true, socketStatus: 0, relinkDevice: action('relink-device'), + manualReconnect: action('manual-reconnect'), withinConnectingGracePeriod: false, }; @@ -41,12 +42,6 @@ const permutations = [ socketStatus: 3, }, }, - { - title: 'Offline', - props: { - isOnline: false, - }, - }, { title: 'Unlinked (online)', props: { @@ -60,6 +55,12 @@ const permutations = [ isRegistrationDone: false, }, }, + { + title: 'Offline', + props: { + isOnline: false, + }, + }, ]; storiesOf('Components/NetworkStatus', module) diff --git a/ts/components/NetworkStatus.tsx b/ts/components/NetworkStatus.tsx index 93142bf666..56a154ed77 100644 --- a/ts/components/NetworkStatus.tsx +++ b/ts/components/NetworkStatus.tsx @@ -3,11 +3,14 @@ import React from 'react'; import { LocalizerType } from '../types/Util'; import { NetworkStateType } from '../state/ducks/network'; +const FIVE_SECONDS = 5 * 1000; + export interface PropsType extends NetworkStateType { hasNetworkDialog: boolean; i18n: LocalizerType; isRegistrationDone: boolean; relinkDevice: () => void; + manualReconnect: () => void; } type RenderDialogTypes = { @@ -39,17 +42,41 @@ export const NetworkStatus = ({ isRegistrationDone, socketStatus, relinkDevice, + manualReconnect, }: PropsType): JSX.Element | null => { if (!hasNetworkDialog) { return null; } - if (!isOnline) { - return renderDialog({ - subtext: i18n('checkNetworkConnection'), - title: i18n('offline'), - }); - } else if (!isRegistrationDone) { + const [isConnecting, setIsConnecting] = React.useState(false); + React.useEffect(() => { + let timeout: NodeJS.Timeout; + + if (isConnecting) { + timeout = setTimeout(() => { + setIsConnecting(false); + }, FIVE_SECONDS); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [isConnecting, setIsConnecting]); + + const reconnect = () => { + setIsConnecting(true); + manualReconnect(); + }; + + const manualReconnectButton = (): JSX.Element => ( +
+ +
+ ); + + if (!isRegistrationDone) { return renderDialog({ renderActionableButton: (): JSX.Element => (
@@ -59,10 +86,22 @@ export const NetworkStatus = ({ subtext: i18n('unlinkedWarning'), title: i18n('unlinked'), }); + } else if (isConnecting) { + return renderDialog({ + subtext: i18n('connectingHangOn'), + title: i18n('connecting'), + }); + } else if (!isOnline) { + return renderDialog({ + renderActionableButton: manualReconnectButton, + subtext: i18n('checkNetworkConnection'), + title: i18n('offline'), + }); } let subtext = ''; let title = ''; + let renderActionableButton; switch (socketStatus) { case WebSocket.CONNECTING: @@ -72,11 +111,13 @@ export const NetworkStatus = ({ case WebSocket.CLOSED: case WebSocket.CLOSING: default: + renderActionableButton = manualReconnectButton; title = i18n('disconnected'); subtext = i18n('checkNetworkConnection'); } return renderDialog({ + renderActionableButton, subtext, title, }); diff --git a/ts/state/ducks/user.ts b/ts/state/ducks/user.ts index 61b096aa59..ea15085acf 100644 --- a/ts/state/ducks/user.ts +++ b/ts/state/ducks/user.ts @@ -1,3 +1,6 @@ +import { trigger } from '../../shims/events'; + +import { NoopActionType } from './noop'; import { LocalizerType } from '../../types/Util'; // State @@ -34,6 +37,7 @@ export type UserActionType = UserChangedActionType; export const actions = { userChanged, + manualReconnect, }; function userChanged(attributes: { @@ -49,6 +53,15 @@ function userChanged(attributes: { }; } +function manualReconnect(): NoopActionType { + trigger('manualConnect'); + + return { + type: 'NOOP', + payload: null, + }; +} + // Reducer function getEmptyState(): UserStateType {