import { useEffect, useRef, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; import { Query, useQuery } from "@tanstack/react-query"; import type { SightingType } from "../types/types"; import { useSound } from "react-sounds"; import { useSoundContext } from "../context/SoundContext"; import { checkIsHotListHit, getNPEDCategory } from "../utils/utils"; import switchSound from "../assets/sounds/ui/switch.mp3"; import notification from "../assets/sounds/ui/notification.mp3"; import popup from "../assets/sounds/ui/popup_open.mp3"; import { useCachedSoundSrc } from "./usecachedSoundSrc"; async function fetchSighting(url: string | undefined, ref: number): Promise { const res = await fetch(`${url}${ref}`, { signal: AbortSignal.timeout(5000), }); if (!res.ok) throw new Error(String(res.status)); return res.json(); } export function useSightingFeed(url: string | undefined) { const { state, audioArmed } = useSoundContext(); const [sightings, setSightings] = useState([]); const [selectedRef, setSelectedRef] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); const [selectedSighting, setSelectedSighting] = useState(null); const { src: soundSrc } = useCachedSoundSrc(state?.sightingSound, switchSound); const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, notification); const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, popup); const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume }); const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume }); const { play: sightingSound } = useSound(soundSrc, { volume: state.sightingVolume }); const mostRecent = sightings[0] ?? null; const currentRef = useRef(-1); const lastValidTimestamp = useRef(Date.now()); function refetchInterval(query: Query) { if (!query) return; const data = query.state.data as SightingType | undefined; const now = Date.now(); if (data && data.ref !== -1) { lastValidTimestamp.current = now; return 100; } if (now - lastValidTimestamp.current > 60_000) { currentRef.current = -1; lastValidTimestamp.current = now; } return 400; } const query = useQuery({ queryKey: ["sighting-feed", url], enabled: !!url, queryFn: () => fetchSighting(url, currentRef.current), refetchInterval: (q) => refetchInterval(q), refetchIntervalInBackground: true, refetchOnWindowFocus: false, retry: false, staleTime: 0, }); const playHotlistsound = useDebouncedCallback(() => { hotlistsound(); }, 500); const playNPEDHitSound = useDebouncedCallback(() => { npedSound(); }, 500); const playSightingHitSound = useDebouncedCallback(() => { sightingSound(); }, 500); useEffect(() => { const data = query.data; if (!data || data.ref === -1) return; const isHotListHit = checkIsHotListHit(data); const cat = getNPEDCategory(data); const isNPEDHitA = cat === "A"; const isNPEDHitB = cat === "B"; const isNPEDHitC = cat === "C"; if ((isNPEDHitA && audioArmed) || (isNPEDHitB && audioArmed) || (isNPEDHitC && audioArmed)) { playNPEDHitSound(); } else if (isHotListHit && audioArmed) { playHotlistsound(); } else if (audioArmed) { playSightingHitSound(); } const now = Date.now(); currentRef.current = data.ref; lastValidTimestamp.current = now; setSightings((prev) => { if (prev[0]?.ref === data.ref) return prev; const dedupPrev = prev.filter((s) => s.ref !== data.ref); return [data, ...dedupPrev].slice(0, 7); }); setSelectedRef(data.ref); }, [query.data]); return { sightings, selectedRef, setSelectedRef, mostRecent, selectedSighting, sessionStarted, setSessionStarted, setSelectedSighting, data: query.data, isLoading: query.isLoading, isFetching: query.isFetching, isError: query.isError, error: query.error as Error | null, refetch: query.refetch, }; }