import { useEffect, useMemo, useRef, useState } from "react"; import { Query, useQuery } from "@tanstack/react-query"; import type { SightingType } from "../types/types"; import { useSoundOnChange } from "react-sounds"; import { useSoundContext } from "../context/SoundContext"; import { getSoundFileURL } from "../utils/utils"; import switchSound from "../assets/sounds/ui/switch.mp3"; 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 mostRecent = sightings[0] ?? null; const latestRef = mostRecent?.ref ?? null; const first = useRef(true); const lastSoundAt = useRef(0); const COOLDOWN_MS = 1500; const currentRef = useRef(-1); const lastValidTimestamp = useRef(Date.now()); const trigger = useMemo(() => { if (latestRef == null || !audioArmed) return null; if (first.current) { first.current = false; return Symbol("skip"); } const now = Date.now(); if (now - lastSoundAt.current < COOLDOWN_MS) return Symbol("skip"); lastSoundAt.current = now; return latestRef; }, [audioArmed, latestRef]); const soundSrc = useMemo(() => { if (state?.sightingSound?.includes(".mp3")) { const file = state.soundOptions?.find((item) => item.name === state.sightingSound); return file?.soundUrl; } return getSoundFileURL(state?.sightingSound) ?? switchSound; }, [state.sightingSound, state.soundOptions]); 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, }); //use latestref instead of trigger to revert back useSoundOnChange(soundSrc, trigger, { volume: state.sightingVolume, initial: false, }); useEffect(() => { const data = query.data; if (!data || data.ref === -1) return; 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, }; }