You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
5.2 KiB
143 lines
5.2 KiB
import { Button } from '@/components/ui/button' |
|
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants' |
|
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' |
|
import storage from '@/services/local-storage.service' |
|
import { WifiOff, X } from 'lucide-react' |
|
import { useEffect, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
|
|
const SLOW_DISMISSED_KEY = 'slowConnectionHintDismissed' |
|
|
|
function detectConnectionStatus(): { poor: boolean; offline: boolean } { |
|
const offline = !navigator.onLine |
|
const conn = (navigator as any).connection |
|
if (!conn) return { poor: offline, offline } |
|
if (conn.saveData === true) return { poor: true, offline } |
|
if (conn.type === 'none') return { poor: true, offline: true } |
|
const eff: string | undefined = conn.effectiveType |
|
return { poor: offline || eff === 'slow-2g' || eff === '2g', offline } |
|
} |
|
|
|
export default function SlowConnectionHint() { |
|
const { t } = useTranslation() |
|
const contentPolicy = useContentPolicyOptional() |
|
const autoplay = contentPolicy?.autoplay ?? storage.getAutoplay() |
|
const mediaAutoLoadPolicy = |
|
contentPolicy?.mediaAutoLoadPolicy ?? storage.getMediaAutoLoadPolicy() |
|
const setAutoplay = contentPolicy?.setAutoplay ?? ((v: boolean) => storage.setAutoplay(v)) |
|
const setMediaAutoLoadPolicy = |
|
contentPolicy?.setMediaAutoLoadPolicy ?? ((p) => storage.setMediaAutoLoadPolicy(p)) |
|
const [status, setStatus] = useState(detectConnectionStatus) |
|
const [slowDismissed, setSlowDismissed] = useState( |
|
() => sessionStorage.getItem(SLOW_DISMISSED_KEY) === 'true' |
|
) |
|
|
|
useEffect(() => { |
|
const refresh = () => setStatus(detectConnectionStatus()) |
|
window.addEventListener('online', refresh) |
|
window.addEventListener('offline', refresh) |
|
const conn = (navigator as any).connection |
|
conn?.addEventListener('change', refresh) |
|
return () => { |
|
window.removeEventListener('online', refresh) |
|
window.removeEventListener('offline', refresh) |
|
conn?.removeEventListener('change', refresh) |
|
} |
|
}, []) |
|
|
|
// Reset slow-connection dismissal when coming back online so the hint can |
|
// re-appear on the next slow-connection episode. |
|
useEffect(() => { |
|
if (!status.offline && !status.poor) { |
|
sessionStorage.removeItem(SLOW_DISMISSED_KEY) |
|
setSlowDismissed(false) |
|
} |
|
}, [status.offline, status.poor]) |
|
|
|
if (status.offline) { |
|
return ( |
|
<div |
|
role="status" |
|
aria-live="polite" |
|
className="shrink-0 border-b border-border bg-muted/60 px-4 py-2.5" |
|
> |
|
<div className="flex min-w-0 items-center gap-2.5 text-muted-foreground"> |
|
<WifiOff className="size-4 shrink-0" aria-hidden /> |
|
<div className="min-w-0 flex-1 text-sm"> |
|
<span className="font-medium text-foreground">{t('Offline mode')}</span> |
|
{' — '} |
|
{t('Only local relays and cached content are available.')} |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
const hasExpensiveSettings = |
|
autoplay || mediaAutoLoadPolicy === MEDIA_AUTO_LOAD_POLICY.ALWAYS |
|
|
|
if (!status.poor || !hasExpensiveSettings || slowDismissed) return null |
|
|
|
const handleSaveData = () => { |
|
if (autoplay) setAutoplay(false) |
|
if (mediaAutoLoadPolicy !== MEDIA_AUTO_LOAD_POLICY.NEVER) { |
|
setMediaAutoLoadPolicy(MEDIA_AUTO_LOAD_POLICY.NEVER) |
|
} |
|
dismissSlow() |
|
} |
|
|
|
const dismissSlow = () => { |
|
setSlowDismissed(true) |
|
sessionStorage.setItem(SLOW_DISMISSED_KEY, 'true') |
|
} |
|
|
|
const changesDescription = [ |
|
autoplay ? t('video autoplay off') : '', |
|
mediaAutoLoadPolicy !== MEDIA_AUTO_LOAD_POLICY.NEVER ? t('media loading off') : '' |
|
] |
|
.filter(Boolean) |
|
.join(', ') |
|
|
|
return ( |
|
<div |
|
role="alert" |
|
className="shrink-0 border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-800 dark:bg-amber-900/20" |
|
> |
|
<div className="flex items-center justify-between gap-4"> |
|
<div className="flex min-w-0 flex-1 items-center gap-3"> |
|
<WifiOff className="size-5 shrink-0 text-amber-600 dark:text-amber-400" aria-hidden /> |
|
<div className="min-w-0 flex-1"> |
|
<p className="text-sm font-medium text-amber-800 dark:text-amber-200"> |
|
{t('Slow connection detected')} |
|
</p> |
|
<p className="text-xs text-amber-600 dark:text-amber-300"> |
|
{changesDescription |
|
? t('Turn on low-bandwidth mode? This will set: {{changes}}.', { |
|
changes: changesDescription |
|
}) |
|
: t('Turn on low-bandwidth mode to reduce data usage.')} |
|
</p> |
|
</div> |
|
</div> |
|
<div className="flex shrink-0 items-center gap-2"> |
|
<Button |
|
size="sm" |
|
onClick={handleSaveData} |
|
className="bg-amber-600 text-white hover:bg-amber-700 dark:bg-amber-700 dark:hover:bg-amber-600" |
|
> |
|
{t('Save data')} |
|
</Button> |
|
<Button |
|
variant="ghost" |
|
size="icon" |
|
onClick={dismissSlow} |
|
aria-label={t('Dismiss')} |
|
className="size-8 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-200" |
|
> |
|
<X className="size-4" /> |
|
</Button> |
|
</div> |
|
</div> |
|
</div> |
|
) |
|
}
|
|
|