307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React from 'react';
|
|
|
|
import ConnectionBanner from '@components/connection_banner/connection_banner';
|
|
import {toMilliseconds} from '@utils/datetime';
|
|
import {getIntlShape} from '@utils/general';
|
|
|
|
import {BannerManager} from './banner_manager';
|
|
|
|
import type {NetworkPerformanceState} from './network_performance_manager';
|
|
import type {FloatingBannerConfig} from '@components/floating_banner/types';
|
|
|
|
const RECONNECTION_BANNER_DURATION = toMilliseconds({seconds: 3});
|
|
const PERFORMANCE_BANNER_DURATION = toMilliseconds({seconds: 10});
|
|
const NETWORK_STATUS_BANNER_ID = 'network-status';
|
|
|
|
function getConnectionMessageText(
|
|
websocketState: WebsocketConnectedState,
|
|
isInternetReachable: boolean | null,
|
|
formatMessage: (descriptor: {id: string; defaultMessage: string}) => string,
|
|
): string {
|
|
const isConnected = websocketState === 'connected';
|
|
|
|
if (isConnected) {
|
|
return formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'});
|
|
}
|
|
|
|
if (websocketState === 'connecting') {
|
|
return formatMessage({id: 'connection_banner.connecting', defaultMessage: 'Connecting...'});
|
|
}
|
|
|
|
if (isInternetReachable) {
|
|
return formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'});
|
|
}
|
|
|
|
return formatMessage({id: 'connection_banner.not_connected', defaultMessage: 'Unable to connect to network'});
|
|
}
|
|
|
|
function isReconnection(
|
|
previousWebsocketState: WebsocketConnectedState | null,
|
|
isOnAppStart: boolean,
|
|
): boolean {
|
|
return previousWebsocketState !== 'connected' && !isOnAppStart;
|
|
}
|
|
|
|
function shouldShowDisconnectedBanner(
|
|
websocketState: WebsocketConnectedState | null,
|
|
isOnAppStart: boolean,
|
|
): boolean {
|
|
return websocketState === 'not_connected' && !isOnAppStart;
|
|
}
|
|
|
|
function shouldShowConnectingBanner(
|
|
websocketState: WebsocketConnectedState | null,
|
|
isOnAppStart: boolean,
|
|
): boolean {
|
|
return websocketState === 'connecting' && !isOnAppStart;
|
|
}
|
|
|
|
function shouldShowPerformanceBanner(
|
|
performanceState: NetworkPerformanceState | null,
|
|
suppressPerformanceBanner: boolean,
|
|
): boolean {
|
|
return performanceState === 'slow' && !suppressPerformanceBanner;
|
|
}
|
|
|
|
function shouldShowReconnectionBanner(
|
|
websocketState: WebsocketConnectedState | null,
|
|
previousWebsocketState: WebsocketConnectedState | null,
|
|
isOnAppStart: boolean,
|
|
): boolean {
|
|
return websocketState === 'connected' &&
|
|
isReconnection(previousWebsocketState, isOnAppStart);
|
|
}
|
|
|
|
class NetworkConnectivityManagerSingleton {
|
|
private currentServerUrl: string | null = null;
|
|
private websocketState: WebsocketConnectedState | null = null;
|
|
private previousWebsocketState: WebsocketConnectedState | null = null;
|
|
private isOnAppStart = true;
|
|
private netInfo: {isInternetReachable: boolean | null} | null = null;
|
|
private appState: string | null = null;
|
|
private readonly intl = getIntlShape();
|
|
private currentPerformanceState: NetworkPerformanceState | null = null;
|
|
private suppressSlowPerformanceBanner = false;
|
|
|
|
private getConnectionMessage(): string {
|
|
if (!this.websocketState || !this.netInfo) {
|
|
return this.intl.formatMessage({id: 'connection_banner.status_unknown', defaultMessage: 'Connection status unknown'});
|
|
}
|
|
|
|
return getConnectionMessageText(this.websocketState, this.netInfo.isInternetReachable, this.intl.formatMessage);
|
|
}
|
|
|
|
private getPerformanceMessage(): string {
|
|
return this.intl.formatMessage({id: 'connection_banner.limited_network_connection', defaultMessage: 'Limited network connection'});
|
|
}
|
|
|
|
private showConnectivity(isConnected: boolean, durationMs?: number) {
|
|
const message = this.getConnectionMessage();
|
|
const bannerConfig = this.createConnectivityBannerConfig(message, isConnected);
|
|
|
|
if (durationMs) {
|
|
BannerManager.showBannerWithAutoHide(bannerConfig, durationMs);
|
|
} else {
|
|
BannerManager.showBanner(bannerConfig);
|
|
}
|
|
}
|
|
|
|
private showPerformance(durationMs: number) {
|
|
const message = this.getPerformanceMessage();
|
|
const bannerConfig = this.createPerformanceBannerConfig(message);
|
|
BannerManager.showBannerWithAutoHide(bannerConfig, durationMs);
|
|
}
|
|
|
|
private createConnectivityBannerConfig(message: string, isConnected: boolean): FloatingBannerConfig {
|
|
return {
|
|
id: NETWORK_STATUS_BANNER_ID,
|
|
dismissible: true,
|
|
customComponent: React.createElement(ConnectionBanner, {
|
|
isConnected,
|
|
message,
|
|
dismissible: true,
|
|
onDismiss: () => {
|
|
// Placeholder to enable dismiss button - actual dismissal handled by BannerManager
|
|
},
|
|
}),
|
|
position: 'bottom',
|
|
};
|
|
}
|
|
|
|
private createPerformanceBannerConfig(message: string): FloatingBannerConfig {
|
|
return {
|
|
id: NETWORK_STATUS_BANNER_ID,
|
|
dismissible: true,
|
|
onDismiss: () => {
|
|
this.suppressSlowPerformanceBanner = true;
|
|
},
|
|
customComponent: React.createElement(ConnectionBanner, {
|
|
isConnected: false,
|
|
message,
|
|
dismissible: true,
|
|
onDismiss: () => {
|
|
// Placeholder to enable dismiss button - actual dismissal handled by BannerManager
|
|
},
|
|
}),
|
|
position: 'bottom',
|
|
};
|
|
}
|
|
|
|
init(serverUrl: string | null = null) {
|
|
this.currentServerUrl = serverUrl;
|
|
this.cleanup();
|
|
}
|
|
|
|
setServerConnectionStatus(connected: boolean, serverUrl: string | null = null) {
|
|
this.currentServerUrl = serverUrl;
|
|
|
|
if (!connected) {
|
|
BannerManager.hideBanner(NETWORK_STATUS_BANNER_ID);
|
|
this.websocketState = null;
|
|
this.previousWebsocketState = null;
|
|
}
|
|
}
|
|
|
|
updateState(
|
|
websocketState: WebsocketConnectedState,
|
|
netInfo: {isInternetReachable: boolean | null},
|
|
appState: string,
|
|
) {
|
|
this.previousWebsocketState = this.websocketState;
|
|
this.websocketState = websocketState;
|
|
this.netInfo = netInfo;
|
|
this.appState = appState;
|
|
|
|
this.updateBanner();
|
|
|
|
if (websocketState === 'connected' && this.isOnAppStart) {
|
|
this.isOnAppStart = false;
|
|
}
|
|
}
|
|
|
|
updatePerformanceState(
|
|
performanceState: NetworkPerformanceState,
|
|
) {
|
|
const wasSlowPerformance = this.currentPerformanceState === 'slow';
|
|
this.currentPerformanceState = performanceState;
|
|
|
|
// Performance state changes require special handling to avoid showing reconnection banners.
|
|
// We can't always call updateBanner() like updateState() does, because it would re-evaluate
|
|
// all banner conditions (including reconnection) when only performance has changed.
|
|
|
|
// Show slow performance banner (suppression check here prevents unnecessary updateBanner calls.
|
|
// suppressSlowPerformanceBanner also checks suppression for calls coming from updateState).
|
|
|
|
if (performanceState === 'slow' && !this.suppressSlowPerformanceBanner) {
|
|
this.updateBanner();
|
|
} else if (wasSlowPerformance && performanceState === 'normal') {
|
|
BannerManager.hideBanner(NETWORK_STATUS_BANNER_ID);
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.suppressSlowPerformanceBanner = false;
|
|
}
|
|
|
|
cleanup() {
|
|
BannerManager.cleanup();
|
|
this.previousWebsocketState = null;
|
|
this.isOnAppStart = true;
|
|
this.reset();
|
|
}
|
|
|
|
shutdown() {
|
|
this.cleanup();
|
|
this.currentServerUrl = null;
|
|
this.websocketState = null;
|
|
this.netInfo = null;
|
|
this.appState = null;
|
|
this.currentPerformanceState = null;
|
|
}
|
|
|
|
private updateBanner() {
|
|
if (!this.currentServerUrl || this.appState === 'background') {
|
|
BannerManager.hideBanner(NETWORK_STATUS_BANNER_ID);
|
|
return;
|
|
}
|
|
|
|
// Banner priority order (highest to lowest)
|
|
// 1. Disconnected - critical connection loss
|
|
if (this.handleDisconnectedState()) {
|
|
return;
|
|
}
|
|
|
|
// 2. Performance - slow network warning
|
|
if (this.handlePerformanceState()) {
|
|
return;
|
|
}
|
|
|
|
// 3. Connecting - attempting to reconnect
|
|
if (this.handleConnectingState()) {
|
|
return;
|
|
}
|
|
|
|
// 4. Reconnection - successful reconnection (auto-hide)
|
|
if (this.handleReconnectionState()) {
|
|
return;
|
|
}
|
|
|
|
BannerManager.hideBanner(NETWORK_STATUS_BANNER_ID);
|
|
}
|
|
|
|
private handleDisconnectedState(): boolean {
|
|
if (shouldShowDisconnectedBanner(this.websocketState, this.isOnAppStart)) {
|
|
this.showConnectivity(false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private handleConnectingState(): boolean {
|
|
if (shouldShowConnectingBanner(this.websocketState, this.isOnAppStart)) {
|
|
this.showConnectivity(false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private handlePerformanceState(): boolean {
|
|
if (shouldShowPerformanceBanner(this.currentPerformanceState, this.suppressSlowPerformanceBanner)) {
|
|
this.showPerformance(PERFORMANCE_BANNER_DURATION);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private handleReconnectionState(): boolean {
|
|
if (shouldShowReconnectionBanner(
|
|
this.websocketState,
|
|
this.previousWebsocketState,
|
|
this.isOnAppStart,
|
|
)) {
|
|
this.showConnectivity(true, RECONNECTION_BANNER_DURATION);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const NetworkConnectivityManager = new NetworkConnectivityManagerSingleton();
|
|
|
|
export default NetworkConnectivityManager;
|
|
|
|
export const testExports = {
|
|
NetworkConnectivityManager: NetworkConnectivityManagerSingleton,
|
|
getConnectionMessageText,
|
|
isReconnection,
|
|
shouldShowDisconnectedBanner,
|
|
shouldShowConnectingBanner,
|
|
shouldShowPerformanceBanner,
|
|
shouldShowReconnectionBanner,
|
|
RECONNECTION_BANNER_DURATION,
|
|
PERFORMANCE_BANNER_DURATION,
|
|
NETWORK_STATUS_BANNER_ID,
|
|
};
|