import storage from '@/services/local-storage.service' import { LIVE_ACTIVITIES_SLIDE_INTERVAL_MS } from '@/lib/live-activities' import { toNote } from '@/lib/link' import { cn } from '@/lib/utils' import { useSmartNoteNavigation } from '@/PageManager' import { useLiveActivitiesOptional } from '@/providers/useLiveActivities' import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider' import { ExternalLink } from 'lucide-react' import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' type TPlacement = 'sidebar' | 'mobile' const SWIPE_MIN_PX = 44 const SWIPE_DOMINANCE_RATIO = 1.2 const SWIPE_NOTE_OPEN_SUPPRESS_MS = 400 export default function LiveActivitiesStrip({ placement }: { placement: TPlacement }) { const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigation() const userPrefs = useUserPreferencesOptional() const showLiveActivitiesBanner = userPrefs?.showLiveActivitiesBanner ?? storage.getShowLiveActivitiesBanner() const live = useLiveActivitiesOptional() const items = live?.items ?? [] const [reduceMotion, setReduceMotion] = useState(false) useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)') const apply = () => setReduceMotion(mq.matches) apply() mq.addEventListener('change', apply) return () => mq.removeEventListener('change', apply) }, []) const [slide, setSlide] = useState(0) useEffect(() => { setSlide(0) }, [items]) useEffect(() => { if (items.length <= 1 || reduceMotion) return const id = window.setInterval(() => { setSlide((s) => (s + 1) % items.length) }, LIVE_ACTIVITIES_SLIDE_INTERVAL_MS) return () => window.clearInterval(id) }, [items.length, reduceMotion]) useLayoutEffect(() => { if (items.length === 0) return setSlide((s) => Math.min(s, items.length - 1)) }, [items.length]) const swipeHintId = useId() const swipeGrabRef = useRef<{ x: number; y: number; pointerId: number } | null>(null) const swipeNavAtRef = useRef(0) const releasePointerIfCaptured = useCallback((el: HTMLDivElement, pointerId: number) => { if (typeof el.hasPointerCapture === 'function' && el.hasPointerCapture(pointerId)) { el.releasePointerCapture(pointerId) } }, []) const onSwipePointerDown = useCallback( (e: React.PointerEvent) => { if (placement !== 'mobile' || items.length <= 1) return swipeGrabRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId } e.currentTarget.setPointerCapture(e.pointerId) }, [placement, items.length] ) const onSwipePointerUp = useCallback( (e: React.PointerEvent) => { if (placement !== 'mobile' || items.length <= 1) return const grab = swipeGrabRef.current swipeGrabRef.current = null releasePointerIfCaptured(e.currentTarget, e.pointerId) if (!grab || grab.pointerId !== e.pointerId) return const dx = e.clientX - grab.x const dy = e.clientY - grab.y const ax = Math.abs(dx) const ay = Math.abs(dy) if (ax < SWIPE_MIN_PX || ax < ay * SWIPE_DOMINANCE_RATIO) return swipeNavAtRef.current = Date.now() if (dx < 0) { setSlide((s) => (s + 1) % items.length) } else { setSlide((s) => (s - 1 + items.length) % items.length) } }, [placement, items.length, releasePointerIfCaptured] ) const onSwipePointerCancel = useCallback( (e: React.PointerEvent) => { swipeGrabRef.current = null releasePointerIfCaptured(e.currentTarget, e.pointerId) }, [releasePointerIfCaptured] ) // `items` can shrink without a new array identity; `slide` may then be out of range until effects run. const displayIndex = items.length === 0 ? 0 : Math.min(slide, items.length - 1) const itemAtSlide = items[displayIndex] const openLiveNote = useCallback(() => { if (Date.now() - swipeNavAtRef.current < SWIPE_NOTE_OPEN_SUPPRESS_MS) return const ev = itemAtSlide?.event if (!ev) return // Same bech32 as {@link getNoteBech32Id} + pass event so {@link navigationEventStore} / cache match the URL. navigateToNote(toNote(ev), ev) }, [navigateToNote, itemAtSlide]) if (!showLiveActivitiesBanner || items.length === 0) { return null } const current = items[displayIndex] if (!current) { return null } const mobileSwipe = placement === 'mobile' && items.length > 1 return (
{mobileSwipe ? ( {t('liveActivities.swipeToBrowse')} ) : null}
{t('liveActivities.heading')}
e.stopPropagation()} >
{items.length > 1 ? ( placement === 'mobile' ? (
{items.map((item, i) => ( ))}
) : (
{items.map((item, i) => ( ))}
) ) : null}
) }