// SPDX-License-Identifier: AGPL-3.0-or-later import Accessibility from '@app/features/accessibility/state/Accessibility'; import {ConnectionIssuesLinks} from '@app/features/app/components/ConnectionIssuesLinks'; import {NativeDragRegion} from '@app/features/app/components/layout/NativeDragRegion'; import styles from '@app/features/app/components/layout/SplashScreen.module.css'; import {useSplashScreenGuard} from '@app/features/app/hooks/useSplashScreenGuard'; import Initialization from '@app/features/app/state/Initialization'; import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions'; import GatewayConnection from '@app/features/gateway/transport/GatewayConnection'; import {FluxerIcon} from '@app/features/ui/components/icons/FluxerIcon'; import {getReducedMotionProps} from '@app/features/ui/utils/ReducedMotionAnimation'; import StatusPage from '@app/features/user/state/StatusPage'; import {type SplashQuote, useSplashQuotes} from '@app/media/data/SplashQuotes'; import {ExternalUrls} from '@fluxer/constants/src/ExternalUrls'; import {Trans} from '@lingui/react/macro'; import {AnimatePresence, motion} from 'framer-motion'; import {observer} from 'mobx-react-lite'; import {useCallback, useEffect, useRef, useState} from 'react'; const PROBLEMS_DELAY = 10000; const STATUS_PAGE_DISPLAY_DELAY = 5000; const FORCE_DISMISS_TIMEOUT_MS = 3000; type SplashScreenMode = 'live' | 'outage'; export const SplashScreen = observer(() => { const shouldBypass = DeveloperOptions.bypassSplashScreen; const interrupted = GatewayConnection.isConnectionInterrupted; const isInitialized = Initialization.canNavigateToProtectedRoutes; const isReady = !interrupted && isInitialized; const [hasTimedOut, setHasTimedOut] = useState(false); useEffect(() => { if (!isReady) { setHasTimedOut(false); return; } const timer = setTimeout(() => setHasTimedOut(true), FORCE_DISMISS_TIMEOUT_MS); return () => clearTimeout(timer); }, [isReady]); if (shouldBypass || hasTimedOut) return null; return ( {!isReady && } ); }); export const OutageSplashScreen = observer(() => ( )); const SPLASH_MOTION = { initial: {opacity: 0}, animate: {opacity: 1}, exit: {opacity: 0}, transition: {duration: 0.35}, }; const CONNECTION_ISSUES_MOTION = { initial: {opacity: 0}, animate: {opacity: 1}, exit: {opacity: 0}, transition: {duration: 0.4}, }; const INCIDENT_CTA_MOTION = { initial: {opacity: 0}, animate: {opacity: 1}, exit: {opacity: 0}, transition: {duration: 0.2}, }; interface SplashScreenContentProps { mode: SplashScreenMode; } function pickRandomQuote(quotes: ReadonlyArray): SplashQuote { return quotes[Math.floor(Math.random() * quotes.length)]; } const SplashScreenContent = observer(({mode}: SplashScreenContentProps) => { const splashOverlayRef = useRef(null); useSplashScreenGuard(splashOverlayRef); const isOutageMode = mode === 'outage'; const splashMotion = getReducedMotionProps(SPLASH_MOTION, Accessibility.useReducedMotion); const connectionIssuesMotion = getReducedMotionProps(CONNECTION_ISSUES_MOTION, Accessibility.useReducedMotion); const incidentCtaMotion = getReducedMotionProps(INCIDENT_CTA_MOTION, Accessibility.useReducedMotion); const interrupted = isOutageMode ? true : GatewayConnection.isConnectionInterrupted; const isInitialized = isOutageMode ? false : Initialization.canNavigateToProtectedRoutes; const isReady = !interrupted && isInitialized; const splashQuotes = useSplashQuotes(); const incident = StatusPage.incident; const quoteRef = useRef(null); if (quoteRef.current == null) { quoteRef.current = pickRandomQuote(splashQuotes); } const quote = quoteRef.current; const [showStatusData, setShowStatusData] = useState(isOutageMode); const [showProblems, setShowProblems] = useState(isOutageMode); const problemsTimerRef = useRef(null); const clearProblemsTimer = useCallback(() => { if (problemsTimerRef.current != null) { clearTimeout(problemsTimerRef.current); problemsTimerRef.current = null; } }, []); useEffect(() => { if (isOutageMode) { setShowStatusData(true); return; } const timer = setTimeout(() => setShowStatusData(true), STATUS_PAGE_DISPLAY_DELAY); return () => clearTimeout(timer); }, [isOutageMode]); useEffect(() => { if (isOutageMode) { clearProblemsTimer(); setShowProblems(true); void StatusPage.checkIncidents(); return; } if (isReady) { clearProblemsTimer(); setShowProblems(false); return; } problemsTimerRef.current = setTimeout(() => { setShowProblems(true); void StatusPage.checkIncidents(); }, PROBLEMS_DELAY); return clearProblemsTimer; }, [clearProblemsTimer, isOutageMode, isReady]); const liveIncident = showStatusData ? incident : null; const frozenDisplayRef = useRef<{incident: typeof liveIncident; text: string} | null>(null); if (!isReady) { frozenDisplayRef.current = { incident: liveIncident, text: liveIncident ? liveIncident.name : quote.text, }; } const visibleIncident = frozenDisplayRef.current?.incident ?? liveIncident; const displayText = frozenDisplayRef.current?.text ?? quote.text; const incidentUrl = visibleIncident?.url ?? ExternalUrls.SERVICE_STATUS; if (isOutageMode) { return ( {visibleIncident != null ? ( {displayText} ) : ( Connection lost )} {visibleIncident != null && ( View on status page )} {showProblems && !isReady && visibleIncident == null && ( )} ); } return ( {visibleIncident != null ? ( {displayText} ) : ( <> {displayText} {quote.source} > )} {visibleIncident != null && ( View on status page )} {showProblems && !isReady && visibleIncident == null && ( )} ); });
Connection lost
{displayText}
{quote.source}