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",
|
||||
"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": {
|
||||
"messageformat": "Update Required",
|
||||
"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
|
||||
center;
|
||||
}
|
||||
|
||||
&--error {
|
||||
-webkit-mask: url('../images/icons/v3/error/error-circle.svg') no-repeat
|
||||
center;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-text {
|
||||
|
|
|
@ -20,6 +20,7 @@ const defaultProps = {
|
|||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
isOutage: false,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
|
@ -54,6 +55,7 @@ KnobsPlayground.args = {
|
|||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
hasNetworkDialog: true,
|
||||
isOnline: true,
|
||||
isOutage: false,
|
||||
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 {
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
||||
|
@ -152,3 +167,16 @@ export function OfflineNarrow(): JSX.Element {
|
|||
</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;
|
||||
|
||||
export type PropsType = Pick<NetworkStateType, 'isOnline' | 'socketStatus'> & {
|
||||
export type PropsType = Pick<
|
||||
NetworkStateType,
|
||||
'isOnline' | 'isOutage' | 'socketStatus'
|
||||
> & {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
manualReconnect: () => void;
|
||||
|
@ -23,6 +26,7 @@ export function DialogNetworkStatus({
|
|||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
isOnline,
|
||||
isOutage,
|
||||
socketStatus,
|
||||
manualReconnect,
|
||||
}: PropsType): JSX.Element | null {
|
||||
|
@ -48,6 +52,17 @@ export function DialogNetworkStatus({
|
|||
manualReconnect();
|
||||
};
|
||||
|
||||
if (isOutage) {
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
icon="error"
|
||||
subtitle={i18n('icu:DialogNetworkStatus__outage')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
const spinner = (
|
||||
<div className="LeftPaneDialog__spinner-container">
|
||||
|
|
|
@ -201,6 +201,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
i18n={i18n}
|
||||
socketStatus={SocketStatus.CLOSED}
|
||||
isOnline={false}
|
||||
isOutage={false}
|
||||
manualReconnect={action('manualReconnect')}
|
||||
{...overrideProps.dialogNetworkStatus}
|
||||
{...props}
|
||||
|
|
|
@ -12,7 +12,7 @@ const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
|
|||
|
||||
export type PropsType = {
|
||||
type?: 'warning' | 'error';
|
||||
icon?: 'update' | 'relink' | 'network' | 'warning' | ReactChild;
|
||||
icon?: 'update' | 'relink' | 'network' | 'warning' | 'error' | ReactChild;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
children?: ReactNode;
|
||||
|
|
|
@ -8,10 +8,19 @@ import type {
|
|||
import { getSocketStatus } from '../shims/socketStatus';
|
||||
import * as log from '../logging/log';
|
||||
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 = {
|
||||
checkNetworkStatus: (x: CheckNetworkStatusPayloadType) => NetworkActionType;
|
||||
closeConnectingGracePeriod: () => NetworkActionType;
|
||||
setOutage: (isOutage: boolean) => NetworkActionType;
|
||||
};
|
||||
|
||||
export function initializeNetworkObserver(
|
||||
|
@ -26,9 +35,62 @@ export function initializeNetworkObserver(
|
|||
isOnline: navigator.onLine,
|
||||
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('socketConnectError', onPotentialOutage);
|
||||
|
||||
window.addEventListener('online', refresh);
|
||||
window.addEventListener('offline', refresh);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnece
|
|||
|
||||
export type NetworkStateType = ReadonlyDeep<{
|
||||
isOnline: boolean;
|
||||
isOutage: boolean;
|
||||
socketStatus: SocketStatus;
|
||||
withinConnectingGracePeriod: boolean;
|
||||
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 RELINK_DEVICE = 'network/RELINK_DEVICE';
|
||||
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
|
||||
const SET_OUTAGE = 'network/SET_OUTAGE';
|
||||
|
||||
export type CheckNetworkStatusPayloadType = ReadonlyDeep<{
|
||||
isOnline: boolean;
|
||||
|
@ -47,11 +49,19 @@ type SetChallengeStatusActionType = ReadonlyDeep<{
|
|||
};
|
||||
}>;
|
||||
|
||||
type SetOutageActionType = ReadonlyDeep<{
|
||||
type: 'network/SET_OUTAGE';
|
||||
payload: {
|
||||
isOutage: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type NetworkActionType = ReadonlyDeep<
|
||||
| CheckNetworkStatusAction
|
||||
| CloseConnectingGracePeriodActionType
|
||||
| RelinkDeviceActionType
|
||||
| SetChallengeStatusActionType
|
||||
| SetOutageActionType
|
||||
>;
|
||||
|
||||
// Action Creators
|
||||
|
@ -88,11 +98,19 @@ function setChallengeStatus(
|
|||
};
|
||||
}
|
||||
|
||||
function setOutage(isOutage: boolean): SetOutageActionType {
|
||||
return {
|
||||
type: SET_OUTAGE,
|
||||
payload: { isOutage },
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
checkNetworkStatus,
|
||||
closeConnectingGracePeriod,
|
||||
relinkDevice,
|
||||
setChallengeStatus,
|
||||
setOutage,
|
||||
};
|
||||
|
||||
// Reducer
|
||||
|
@ -100,6 +118,7 @@ export const actions = {
|
|||
export function getEmptyState(): NetworkStateType {
|
||||
return {
|
||||
isOnline: navigator.onLine,
|
||||
isOutage: false,
|
||||
socketStatus: SocketStatus.OPEN,
|
||||
withinConnectingGracePeriod: true,
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -14,11 +14,17 @@ export const hasNetworkDialog = createSelector(
|
|||
getNetwork,
|
||||
isDone,
|
||||
(
|
||||
{ isOnline, socketStatus, withinConnectingGracePeriod }: NetworkStateType,
|
||||
{
|
||||
isOnline,
|
||||
isOutage,
|
||||
socketStatus,
|
||||
withinConnectingGracePeriod,
|
||||
}: NetworkStateType,
|
||||
isRegistrationDone: boolean
|
||||
): boolean =>
|
||||
isRegistrationDone &&
|
||||
(!isOnline ||
|
||||
isOutage ||
|
||||
(socketStatus === SocketStatus.CONNECTING &&
|
||||
!withinConnectingGracePeriod) ||
|
||||
socketStatus === SocketStatus.CLOSED ||
|
||||
|
|
|
@ -219,6 +219,10 @@ export class SocketManager extends EventListener {
|
|||
// No reconnect attempt should be made
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === -1) {
|
||||
this.emit('connectError');
|
||||
}
|
||||
}
|
||||
|
||||
void reconnect();
|
||||
|
@ -706,6 +710,7 @@ export class SocketManager extends EventListener {
|
|||
): this;
|
||||
public override on(type: 'statusChange', callback: () => void): this;
|
||||
public override on(type: 'deviceConflict', callback: () => void): this;
|
||||
public override on(type: 'connectError', callback: () => void): this;
|
||||
|
||||
public override on(
|
||||
type: string | symbol,
|
||||
|
@ -718,6 +723,7 @@ export class SocketManager extends EventListener {
|
|||
public override emit(type: 'authError', error: HTTPError): boolean;
|
||||
public override emit(type: 'statusChange'): boolean;
|
||||
public override emit(type: 'deviceConflict'): boolean;
|
||||
public override emit(type: 'connectError'): boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||
|
|
|
@ -1325,6 +1325,10 @@ export function initialize({
|
|||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
});
|
||||
|
||||
socketManager.on('connectError', () => {
|
||||
window.Whisper.events.trigger('socketConnectError');
|
||||
});
|
||||
|
||||
socketManager.on('deviceConflict', () => {
|
||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue