Introduce outage network status
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
1ca4ee555f
commit
1823f7eca9
11 changed files with 164 additions and 3 deletions
|
@ -6182,6 +6182,10 @@
|
||||||
"messageformat": "Update Downloaded",
|
"messageformat": "Update Downloaded",
|
||||||
"description": "The title of update dialog when update download is completed."
|
"description": "The title of update dialog when update download is completed."
|
||||||
},
|
},
|
||||||
|
"icu:DialogNetworkStatus__outage": {
|
||||||
|
"messageformat": "Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.",
|
||||||
|
"description": "The title of outage dialog during service outage."
|
||||||
|
},
|
||||||
"icu:InstallScreenUpdateDialog--unsupported-os__title": {
|
"icu:InstallScreenUpdateDialog--unsupported-os__title": {
|
||||||
"messageformat": "Update Required",
|
"messageformat": "Update Required",
|
||||||
"description": "The title of update dialog on install screen when user OS is unsupported"
|
"description": "The title of update dialog on install screen when user OS is unsupported"
|
||||||
|
|
|
@ -107,6 +107,11 @@
|
||||||
-webkit-mask: url('../images/icons/v3/error/error-triangle.svg') no-repeat
|
-webkit-mask: url('../images/icons/v3/error/error-triangle.svg') no-repeat
|
||||||
center;
|
center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
-webkit-mask: url('../images/icons/v3/error/error-circle.svg') no-repeat
|
||||||
|
center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-text {
|
&__action-text {
|
||||||
|
|
|
@ -20,6 +20,7 @@ const defaultProps = {
|
||||||
hasNetworkDialog: true,
|
hasNetworkDialog: true,
|
||||||
i18n,
|
i18n,
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
isOutage: false,
|
||||||
socketStatus: SocketStatus.CONNECTING,
|
socketStatus: SocketStatus.CONNECTING,
|
||||||
manualReconnect: action('manual-reconnect'),
|
manualReconnect: action('manual-reconnect'),
|
||||||
withinConnectingGracePeriod: false,
|
withinConnectingGracePeriod: false,
|
||||||
|
@ -54,6 +55,7 @@ KnobsPlayground.args = {
|
||||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||||
hasNetworkDialog: true,
|
hasNetworkDialog: true,
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
isOutage: false,
|
||||||
socketStatus: SocketStatus.CONNECTING,
|
socketStatus: SocketStatus.CONNECTING,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -105,6 +107,19 @@ export function OfflineWide(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OutageWide(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Wide}>
|
||||||
|
<DialogNetworkStatus
|
||||||
|
{...defaultProps}
|
||||||
|
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||||
|
isOnline={false}
|
||||||
|
isOutage
|
||||||
|
/>
|
||||||
|
</FakeLeftPaneContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ConnectingNarrow(): JSX.Element {
|
export function ConnectingNarrow(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
||||||
|
@ -152,3 +167,16 @@ export function OfflineNarrow(): JSX.Element {
|
||||||
</FakeLeftPaneContainer>
|
</FakeLeftPaneContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OutageNarrow(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
||||||
|
<DialogNetworkStatus
|
||||||
|
{...defaultProps}
|
||||||
|
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||||
|
isOnline={false}
|
||||||
|
isOutage
|
||||||
|
/>
|
||||||
|
</FakeLeftPaneContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
|
|
||||||
const FIVE_SECONDS = 5 * 1000;
|
const FIVE_SECONDS = 5 * 1000;
|
||||||
|
|
||||||
export type PropsType = Pick<NetworkStateType, 'isOnline' | 'socketStatus'> & {
|
export type PropsType = Pick<
|
||||||
|
NetworkStateType,
|
||||||
|
'isOnline' | 'isOutage' | 'socketStatus'
|
||||||
|
> & {
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
manualReconnect: () => void;
|
manualReconnect: () => void;
|
||||||
|
@ -23,6 +26,7 @@ export function DialogNetworkStatus({
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
i18n,
|
i18n,
|
||||||
isOnline,
|
isOnline,
|
||||||
|
isOutage,
|
||||||
socketStatus,
|
socketStatus,
|
||||||
manualReconnect,
|
manualReconnect,
|
||||||
}: PropsType): JSX.Element | null {
|
}: PropsType): JSX.Element | null {
|
||||||
|
@ -48,6 +52,17 @@ export function DialogNetworkStatus({
|
||||||
manualReconnect();
|
manualReconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isOutage) {
|
||||||
|
return (
|
||||||
|
<LeftPaneDialog
|
||||||
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
|
type="warning"
|
||||||
|
icon="error"
|
||||||
|
subtitle={i18n('icu:DialogNetworkStatus__outage')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isConnecting) {
|
if (isConnecting) {
|
||||||
const spinner = (
|
const spinner = (
|
||||||
<div className="LeftPaneDialog__spinner-container">
|
<div className="LeftPaneDialog__spinner-container">
|
||||||
|
|
|
@ -201,6 +201,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
socketStatus={SocketStatus.CLOSED}
|
socketStatus={SocketStatus.CLOSED}
|
||||||
isOnline={false}
|
isOnline={false}
|
||||||
|
isOutage={false}
|
||||||
manualReconnect={action('manualReconnect')}
|
manualReconnect={action('manualReconnect')}
|
||||||
{...overrideProps.dialogNetworkStatus}
|
{...overrideProps.dialogNetworkStatus}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -12,7 +12,7 @@ const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
type?: 'warning' | 'error';
|
type?: 'warning' | 'error';
|
||||||
icon?: 'update' | 'relink' | 'network' | 'warning' | ReactChild;
|
icon?: 'update' | 'relink' | 'network' | 'warning' | 'error' | ReactChild;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|
|
@ -8,10 +8,19 @@ import type {
|
||||||
import { getSocketStatus } from '../shims/socketStatus';
|
import { getSocketStatus } from '../shims/socketStatus';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
|
import { electronLookup } from '../util/dns';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
import { SocketStatus } from '../types/SocketStatus';
|
||||||
|
|
||||||
|
// DNS TTL
|
||||||
|
const OUTAGE_CHECK_INTERVAL = 60 * SECOND;
|
||||||
|
const OUTAGE_HEALTY_ADDR = '127.0.0.1';
|
||||||
|
const OUTAGE_NO_SERVICE_ADDR = '127.0.0.2';
|
||||||
|
|
||||||
type NetworkActions = {
|
type NetworkActions = {
|
||||||
checkNetworkStatus: (x: CheckNetworkStatusPayloadType) => NetworkActionType;
|
checkNetworkStatus: (x: CheckNetworkStatusPayloadType) => NetworkActionType;
|
||||||
closeConnectingGracePeriod: () => NetworkActionType;
|
closeConnectingGracePeriod: () => NetworkActionType;
|
||||||
|
setOutage: (isOutage: boolean) => NetworkActionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function initializeNetworkObserver(
|
export function initializeNetworkObserver(
|
||||||
|
@ -26,9 +35,62 @@ export function initializeNetworkObserver(
|
||||||
isOnline: navigator.onLine,
|
isOnline: navigator.onLine,
|
||||||
socketStatus,
|
socketStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (socketStatus === SocketStatus.OPEN) {
|
||||||
|
onOutageEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let outageTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
const checkOutage = async (): Promise<void> => {
|
||||||
|
electronLookup('uptime.signal.org', { all: false }, (error, address) => {
|
||||||
|
if (error) {
|
||||||
|
log.error('networkObserver: outage check failure', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address === OUTAGE_HEALTY_ADDR) {
|
||||||
|
log.info(
|
||||||
|
'networkObserver: got healthy response from uptime.signal.org'
|
||||||
|
);
|
||||||
|
onOutageEnd();
|
||||||
|
} else if (address === OUTAGE_NO_SERVICE_ADDR) {
|
||||||
|
log.warn('networkObserver: service is down');
|
||||||
|
networkActions.setOutage(true);
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
'networkObserver: unexpected DNS response for uptime.signal.org'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPotentialOutage = (): void => {
|
||||||
|
if (outageTimer != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn('networkObserver: initiating outage check');
|
||||||
|
|
||||||
|
outageTimer = setInterval(() => drop(checkOutage()), OUTAGE_CHECK_INTERVAL);
|
||||||
|
drop(checkOutage());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOutageEnd = (): void => {
|
||||||
|
if (outageTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn('networkObserver: clearing outage check');
|
||||||
|
clearInterval(outageTimer);
|
||||||
|
outageTimer = undefined;
|
||||||
|
|
||||||
|
networkActions.setOutage(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.Whisper.events.on('socketStatusChange', refresh);
|
window.Whisper.events.on('socketStatusChange', refresh);
|
||||||
|
window.Whisper.events.on('socketConnectError', onPotentialOutage);
|
||||||
|
|
||||||
window.addEventListener('online', refresh);
|
window.addEventListener('online', refresh);
|
||||||
window.addEventListener('offline', refresh);
|
window.addEventListener('offline', refresh);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnece
|
||||||
|
|
||||||
export type NetworkStateType = ReadonlyDeep<{
|
export type NetworkStateType = ReadonlyDeep<{
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
|
isOutage: boolean;
|
||||||
socketStatus: SocketStatus;
|
socketStatus: SocketStatus;
|
||||||
withinConnectingGracePeriod: boolean;
|
withinConnectingGracePeriod: boolean;
|
||||||
challengeStatus: 'required' | 'pending' | 'idle';
|
challengeStatus: 'required' | 'pending' | 'idle';
|
||||||
|
@ -21,6 +22,7 @@ const CHECK_NETWORK_STATUS = 'network/CHECK_NETWORK_STATUS';
|
||||||
const CLOSE_CONNECTING_GRACE_PERIOD = 'network/CLOSE_CONNECTING_GRACE_PERIOD';
|
const CLOSE_CONNECTING_GRACE_PERIOD = 'network/CLOSE_CONNECTING_GRACE_PERIOD';
|
||||||
const RELINK_DEVICE = 'network/RELINK_DEVICE';
|
const RELINK_DEVICE = 'network/RELINK_DEVICE';
|
||||||
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
|
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
|
||||||
|
const SET_OUTAGE = 'network/SET_OUTAGE';
|
||||||
|
|
||||||
export type CheckNetworkStatusPayloadType = ReadonlyDeep<{
|
export type CheckNetworkStatusPayloadType = ReadonlyDeep<{
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
|
@ -47,11 +49,19 @@ type SetChallengeStatusActionType = ReadonlyDeep<{
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type SetOutageActionType = ReadonlyDeep<{
|
||||||
|
type: 'network/SET_OUTAGE';
|
||||||
|
payload: {
|
||||||
|
isOutage: boolean;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
export type NetworkActionType = ReadonlyDeep<
|
export type NetworkActionType = ReadonlyDeep<
|
||||||
| CheckNetworkStatusAction
|
| CheckNetworkStatusAction
|
||||||
| CloseConnectingGracePeriodActionType
|
| CloseConnectingGracePeriodActionType
|
||||||
| RelinkDeviceActionType
|
| RelinkDeviceActionType
|
||||||
| SetChallengeStatusActionType
|
| SetChallengeStatusActionType
|
||||||
|
| SetOutageActionType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -88,11 +98,19 @@ function setChallengeStatus(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setOutage(isOutage: boolean): SetOutageActionType {
|
||||||
|
return {
|
||||||
|
type: SET_OUTAGE,
|
||||||
|
payload: { isOutage },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
checkNetworkStatus,
|
checkNetworkStatus,
|
||||||
closeConnectingGracePeriod,
|
closeConnectingGracePeriod,
|
||||||
relinkDevice,
|
relinkDevice,
|
||||||
setChallengeStatus,
|
setChallengeStatus,
|
||||||
|
setOutage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
@ -100,6 +118,7 @@ export const actions = {
|
||||||
export function getEmptyState(): NetworkStateType {
|
export function getEmptyState(): NetworkStateType {
|
||||||
return {
|
return {
|
||||||
isOnline: navigator.onLine,
|
isOnline: navigator.onLine,
|
||||||
|
isOutage: false,
|
||||||
socketStatus: SocketStatus.OPEN,
|
socketStatus: SocketStatus.OPEN,
|
||||||
withinConnectingGracePeriod: true,
|
withinConnectingGracePeriod: true,
|
||||||
challengeStatus: 'idle',
|
challengeStatus: 'idle',
|
||||||
|
@ -135,5 +154,16 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_OUTAGE) {
|
||||||
|
const { isOutage } = action.payload;
|
||||||
|
|
||||||
|
// This action is dispatched frequently when offline.
|
||||||
|
// We avoid allocating a new object if nothing has changed to
|
||||||
|
// avoid an unnecessary re-render.
|
||||||
|
return assignWithNoUnnecessaryAllocation(state, {
|
||||||
|
isOutage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,11 +14,17 @@ export const hasNetworkDialog = createSelector(
|
||||||
getNetwork,
|
getNetwork,
|
||||||
isDone,
|
isDone,
|
||||||
(
|
(
|
||||||
{ isOnline, socketStatus, withinConnectingGracePeriod }: NetworkStateType,
|
{
|
||||||
|
isOnline,
|
||||||
|
isOutage,
|
||||||
|
socketStatus,
|
||||||
|
withinConnectingGracePeriod,
|
||||||
|
}: NetworkStateType,
|
||||||
isRegistrationDone: boolean
|
isRegistrationDone: boolean
|
||||||
): boolean =>
|
): boolean =>
|
||||||
isRegistrationDone &&
|
isRegistrationDone &&
|
||||||
(!isOnline ||
|
(!isOnline ||
|
||||||
|
isOutage ||
|
||||||
(socketStatus === SocketStatus.CONNECTING &&
|
(socketStatus === SocketStatus.CONNECTING &&
|
||||||
!withinConnectingGracePeriod) ||
|
!withinConnectingGracePeriod) ||
|
||||||
socketStatus === SocketStatus.CLOSED ||
|
socketStatus === SocketStatus.CLOSED ||
|
||||||
|
|
|
@ -219,6 +219,10 @@ export class SocketManager extends EventListener {
|
||||||
// No reconnect attempt should be made
|
// No reconnect attempt should be made
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (code === -1) {
|
||||||
|
this.emit('connectError');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reconnect();
|
void reconnect();
|
||||||
|
@ -706,6 +710,7 @@ export class SocketManager extends EventListener {
|
||||||
): this;
|
): this;
|
||||||
public override on(type: 'statusChange', callback: () => void): this;
|
public override on(type: 'statusChange', callback: () => void): this;
|
||||||
public override on(type: 'deviceConflict', callback: () => void): this;
|
public override on(type: 'deviceConflict', callback: () => void): this;
|
||||||
|
public override on(type: 'connectError', callback: () => void): this;
|
||||||
|
|
||||||
public override on(
|
public override on(
|
||||||
type: string | symbol,
|
type: string | symbol,
|
||||||
|
@ -718,6 +723,7 @@ export class SocketManager extends EventListener {
|
||||||
public override emit(type: 'authError', error: HTTPError): boolean;
|
public override emit(type: 'authError', error: HTTPError): boolean;
|
||||||
public override emit(type: 'statusChange'): boolean;
|
public override emit(type: 'statusChange'): boolean;
|
||||||
public override emit(type: 'deviceConflict'): boolean;
|
public override emit(type: 'deviceConflict'): boolean;
|
||||||
|
public override emit(type: 'connectError'): boolean;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||||
|
|
|
@ -1325,6 +1325,10 @@ export function initialize({
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socketManager.on('connectError', () => {
|
||||||
|
window.Whisper.events.trigger('socketConnectError');
|
||||||
|
});
|
||||||
|
|
||||||
socketManager.on('deviceConflict', () => {
|
socketManager.on('deviceConflict', () => {
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue