mattermost-mobile/app/components/connection_banner/use_connection_banner.ts
Daniel Espino García 1a01c532a1
Fix connection banner not disappearing (#9532)
* Fix connection banner not disappearing

* Add relevant test
2026-03-03 13:08:45 +01:00

195 lines
6.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useCallback, useEffect, useRef, useState} from 'react';
import useDidUpdate from '@hooks/did_update';
import type {NetworkPerformanceState} from '@managers/network_performance_manager';
import type {NetInfoState} from '@react-native-community/netinfo';
import type {IntlShape} from 'react-intl';
const CLOSE_TIMEOUT_DURATION_MS = 2000;
const clearTimeoutRef = (ref: React.MutableRefObject<NodeJS.Timeout | null | undefined>) => {
if (ref.current) {
clearTimeout(ref.current);
ref.current = null;
}
};
type UseConnectionBannerParams = {
websocketState: WebsocketConnectedState;
networkPerformanceState: NetworkPerformanceState;
netInfo: NetInfoState;
appState: string;
intl: IntlShape;
};
type UseConnectionBannerReturn = {
visible: boolean;
bannerText: string;
isShowingConnectedBanner: boolean;
};
export const useConnectionBanner = ({
websocketState,
networkPerformanceState,
netInfo,
appState,
intl,
}: UseConnectionBannerParams): UseConnectionBannerReturn => {
const closeTimeout = useRef<NodeJS.Timeout | null>();
const openTimeout = useRef<NodeJS.Timeout | null>();
const initialAppSession = useRef(true);
const previousWebsocketState = useRef<WebsocketConnectedState>(websocketState);
const hasShownSlowBanner = useRef(false);
const [visible, setVisible] = useState(false);
const [bannerText, setBannerText] = useState('');
const [isShowingConnectedBanner, setIsShowingConnectedBanner] = useState(false);
const closeCallback = useCallback(() => {
setVisible(false);
clearTimeoutRef(closeTimeout);
}, []);
const openCallback = useCallback(() => {
clearTimeoutRef(closeTimeout);
clearTimeoutRef(openTimeout);
setVisible(true);
}, []);
const handleDisconnectedState = useCallback((): boolean => {
if (websocketState === 'not_connected') {
previousWebsocketState.current = 'not_connected';
if (!initialAppSession.current) {
setBannerText(intl.formatMessage({id: 'connection_banner.not_connected', defaultMessage: 'Unable to connect to network'}));
openCallback();
return true;
}
}
return false;
}, [websocketState, openCallback, intl]);
const handleInternetUnreachableState = useCallback((): boolean => {
if (netInfo.isInternetReachable === false) {
setBannerText(intl.formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'}));
openCallback();
closeTimeout.current = setTimeout(closeCallback, CLOSE_TIMEOUT_DURATION_MS);
return true;
}
return false;
}, [netInfo.isInternetReachable, intl, openCallback, closeCallback]);
const handleSlowNetworkState = useCallback((): boolean => {
if (networkPerformanceState === 'slow' && !hasShownSlowBanner.current) {
hasShownSlowBanner.current = true;
setBannerText(intl.formatMessage({id: 'connection_banner.slow', defaultMessage: 'Limited network connection'}));
openCallback();
closeTimeout.current = setTimeout(() => {
closeCallback();
}, CLOSE_TIMEOUT_DURATION_MS);
return true;
}
return false;
}, [networkPerformanceState, intl, openCallback, closeCallback]);
const handleConnectedState = useCallback((): boolean => {
if (websocketState === 'connected' && previousWebsocketState.current !== 'connected') {
previousWebsocketState.current = 'connected';
if (!initialAppSession.current && !isShowingConnectedBanner) {
setIsShowingConnectedBanner(true);
setBannerText(intl.formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'}));
openCallback();
closeTimeout.current = setTimeout(() => {
closeCallback();
setIsShowingConnectedBanner(false);
}, CLOSE_TIMEOUT_DURATION_MS);
return true;
}
initialAppSession.current = false;
return true;
}
return false;
}, [websocketState, intl, openCallback, closeCallback, isShowingConnectedBanner]);
const handleConnectingState = useCallback((): boolean => {
if (websocketState === 'connecting') {
if (!initialAppSession.current) {
setBannerText(intl.formatMessage({id: 'connection_banner.connecting', defaultMessage: 'Connecting...'}));
openCallback();
return true;
}
previousWebsocketState.current = 'connecting';
}
return false;
}, [websocketState, intl, openCallback]);
useEffect(() => {
return () => {
clearTimeoutRef(closeTimeout);
clearTimeoutRef(openTimeout);
};
}, []);
useEffect(() => {
if (appState !== 'active') {
return;
}
if (visible && closeTimeout.current) {
return;
}
const priorities = () => {
if (handleInternetUnreachableState()) {
return;
}
if (handleDisconnectedState()) {
return;
}
if (handleSlowNetworkState()) {
return;
}
if (handleConnectedState()) {
return;
}
handleConnectingState();
};
priorities();
// We omit 'visible' from dependencies because we do not want
// to show again the banner the moment the banner is closed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
handleInternetUnreachableState,
handleDisconnectedState,
handleSlowNetworkState,
handleConnectedState,
handleConnectingState,
appState,
]);
useDidUpdate(() => {
if (appState !== 'active') {
setVisible(false);
setBannerText('');
clearTimeoutRef(openTimeout);
clearTimeoutRef(closeTimeout);
hasShownSlowBanner.current = false;
setIsShowingConnectedBanner(false);
}
}, [appState]);
return {
visible,
bannerText,
isShowingConnectedBanner,
};
};