diff --git a/src/components/GlobalDataProvider.tsx b/src/components/GlobalDataProvider.tsx index 1097b9ea..f6fdbe36 100644 --- a/src/components/GlobalDataProvider.tsx +++ b/src/components/GlobalDataProvider.tsx @@ -1,31 +1,32 @@ +import { AO_CU_URL, log } from '@src/constants'; import { useEffectOnce } from '@src/hooks/useEffectOnce'; import { useGlobalState } from '@src/store'; import { cleanupDbCache } from '@src/store/db'; import { ReactElement, useEffect } from 'react'; -const GlobalDataProvider = ({ - children, -}: { - children: ReactElement; -}) => { - const twoMinutes = 120000; +// Time to wait in ms to check if the AO CU URL is congested +const CONGESTION_WINDOW = 5000; +const TWO_MINUTES = 120000; + +const GlobalDataProvider = ({ children }: { children: ReactElement }) => { const setBlockHeight = useGlobalState((state) => state.setBlockHeight); const setCurrentEpoch = useGlobalState((state) => state.setCurrentEpoch); const setTicker = useGlobalState((state) => state.setTicker); const arweave = useGlobalState((state) => state.arweave); const arioReadSDK = useGlobalState((state) => state.arIOReadSDK); + const setAoCongested = useGlobalState((state) => state.setAoCongested); useEffectOnce(() => { const update = async () => { - // perform this first as retrieving the current epic takes some time - const {Ticker} = await arioReadSDK.getInfo(); + // perform this first as retrieving the current epic takes some time + const { Ticker } = await arioReadSDK.getInfo(); setTicker(Ticker); const currentEpoch = await arioReadSDK.getCurrentEpoch(); setCurrentEpoch(currentEpoch); - if(currentEpoch?.epochIndex) { + if (currentEpoch?.epochIndex) { cleanupDbCache(currentEpoch.epochIndex); } }; @@ -34,15 +35,42 @@ const GlobalDataProvider = ({ }); useEffect(() => { + // Block Height Updater const updateBlockHeight = async () => { const blockHeight = await (await arweave.blocks.getCurrent()).height; setBlockHeight(blockHeight); }; updateBlockHeight(); - const interval = setInterval(updateBlockHeight, twoMinutes); + const interval = setInterval(updateBlockHeight, TWO_MINUTES); + + // AO congestion checker: Checks CU URL every 30 seconds and if it takes longer than 5 seconds will + // dispatch a warning to the user + + const checkAoCongestion = () => { + const startTime = Date.now(); + fetch(AO_CU_URL, { method: 'HEAD' }) + .then((res) => { + const endTime = Date.now(); + if (!res.ok) { + log.error('AO CU URL is down'); + setAoCongested(true); + } else if (endTime - startTime > CONGESTION_WINDOW) { + setAoCongested(true); + } + }) + .catch((error) => { + log.error('AO CU URL is down', error); + setAoCongested(true); + }); + }; + + checkAoCongestion(); + const congestionInterval = setInterval(checkAoCongestion, 30000); return () => { clearInterval(interval); + clearInterval(congestionInterval); + setAoCongested(false); }; }); diff --git a/src/components/NetworkStatusBanner.tsx b/src/components/NetworkStatusBanner.tsx new file mode 100644 index 00000000..0fd31feb --- /dev/null +++ b/src/components/NetworkStatusBanner.tsx @@ -0,0 +1,45 @@ +import { useGlobalState } from '@src/store'; +import { useEffect, useState } from 'react'; + +function NetworkStatusBanner() { + const [online, setOnline] = useState(navigator.onLine); + const aoCongested = useGlobalState((state) => state.aoCongested); + + const [statusMessage, setStatusMessage] = useState(''); + + useEffect(() => { + window.addEventListener('online', () => setOnline(true)); + window.addEventListener('offline', () => setOnline(false)); + + // Cleanup listeners + return () => { + window.removeEventListener('online', () => setOnline(true)); + window.removeEventListener('offline', () => setOnline(false)); + }; + }, []); + + useEffect(() => { + if (!online) { + setStatusMessage(`We can't connect to the Internet. Please check your connection + and try again.`); + } else if (aoCongested) { + setStatusMessage( + 'The AO network is experiencing congestion, load times may be longer than usual.', + ); + } else { + setStatusMessage(''); + } + }, [online, aoCongested]); + + return ( + <> + {statusMessage && ( +
+ {statusMessage} +
+ )} + + ); +} + +export default NetworkStatusBanner; diff --git a/src/layout/AppRouterLayout.tsx b/src/layout/AppRouterLayout.tsx index 30c357b3..a69cace5 100644 --- a/src/layout/AppRouterLayout.tsx +++ b/src/layout/AppRouterLayout.tsx @@ -1,3 +1,4 @@ +import NetworkStatusBanner from '@src/components/NetworkStatusBanner'; import { Toaster } from 'react-hot-toast'; import { Outlet } from 'react-router-dom'; import Sidebar from './Sidebar'; @@ -5,6 +6,7 @@ import Sidebar from './Sidebar'; function AppRouterLayout() { return (
+
diff --git a/src/layout/Sidebar.tsx b/src/layout/Sidebar.tsx index 8e8cc9ad..a79dbc39 100644 --- a/src/layout/Sidebar.tsx +++ b/src/layout/Sidebar.tsx @@ -54,10 +54,7 @@ const ROUTES_SECONDARY = [ const FORMATTED_CHANGELOG = changeLog .substring(changeLog.indexOf('## [Unreleased]') + 16) .trim() - .replace(/\[([\w.]+)\]/g, (match, text) => { - console.log(match, text); - return `v${text}`; - }); + .replace(/\[([\w.]+)\]/g, (match, text) => `v${text}`); const Sidebar = () => { const location = useLocation(); @@ -132,7 +129,7 @@ const Sidebar = () => { > {sidebarOpen && (