import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useAtomValue, useSetAtom } from 'jotai'; import { MatrixClient, SyncState } from 'matrix-js-sdk'; import { Box, Button, config, Icon, IconButton, Icons, IconSrc, Line, Text } from 'folds'; import { ContainerColor } from '../../styles/ContainerColor.css'; import { useClientConfig } from '../../hooks/useClientConfig'; import { useSyncState } from '../../hooks/useSyncState'; import { useServerStatus } from '../../hooks/useServerStatus'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { serverStatusAtom } from '../../state/serverStatus'; import { pickBanner, resolveStatusPage, StatusBanner } from '../../utils/kumaStatus'; const DISMISS_KEY = 'lotus-status-banner-dismissed'; const DISMISS_MAX = 50; const readDismissed = (): string[] => { try { const v = JSON.parse(localStorage.getItem(DISMISS_KEY) ?? '[]'); return Array.isArray(v) ? v.filter((k): k is string => typeof k === 'string') : []; } catch { return []; } }; const bannerIcon = (b: StatusBanner): IconSrc => { if (b.kind === 'maintenance') return Icons.Setting; if (b.tone === 'Primary') return Icons.Info; return Icons.Warning; }; /** * [Gitea #124] One status strip, in the same slot and style as the sync and * clock banners. Presentational: the caller picks the banner. */ export function StatusStrip({ banner, onDismiss, }: { banner: StatusBanner; onDismiss?: () => void; }) { const [open, setOpen] = useState(false); const mobile = useScreenSizeContext() === ScreenSize.Mobile; useEffect(() => setOpen(false), [banner.key]); const critical = banner.tone === 'Critical'; return ( {banner.text} {banner.detail && ( )} {banner.dismissable && onDismiss && ( )} {banner.detail && open && ( {banner.detail} )} ); } /** Runs the Kuma poller for the logged-in homeserver and publishes it. */ export function ServerStatusFeature({ mx }: { mx: MatrixClient }) { const { statusPages } = useClientConfig(); const page = useMemo( () => resolveStatusPage(statusPages, mx.getDomain() ?? undefined), [statusPages, mx], ); const [lostAt, setLostAt] = useState(null); useSyncState( mx, useCallback((state: SyncState | null) => { const lost = state === SyncState.Reconnecting || state === SyncState.Error; setLostAt((prev) => { if (!lost) return null; return prev ?? Date.now(); }); }, []), ); const syncLost = lostAt !== null; const status = useServerStatus(page, syncLost); const setAtom = useSetAtom(serverStatusAtom); useEffect(() => setAtom({ status, syncLost, lostAt }), [setAtom, status, syncLost, lostAt]); return null; } /** The logged-in app's status strip. */ export function ServerStatusBanner() { const { status, syncLost } = useAtomValue(serverStatusAtom); const [dismissed, setDismissed] = useState(readDismissed); const banner = pickBanner(status, { syncLost, where: 'client', dismissed: new Set(dismissed), }); if (!banner) return null; const dismiss = () => { const next = [...dismissed.filter((k) => k !== banner.key), banner.key].slice(-DISMISS_MAX); try { localStorage.setItem(DISMISS_KEY, JSON.stringify(next)); } catch { // storage unavailable — dismissed for this session only } setDismissed(next); }; return ; } /** The sign-in screen's strip, for the homeserver being signed in to. */ export function LoginStatusBanner({ serverName }: { serverName: string }) { const { statusPages } = useClientConfig(); const page = useMemo(() => resolveStatusPage(statusPages, serverName), [statusPages, serverName]); const status = useServerStatus(page); const banner = pickBanner(status, { syncLost: false, where: 'login' }); return banner ? : null; } /** Calls-down note for the call Join controls (null when calls are fine). */ export function useCallsDown(): boolean { const { status } = useAtomValue(serverStatusAtom); return status?.calls === 'down' && status.homeserver !== 'down'; }