import { useEffect, useMemo, 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, getSoundFileURL } 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 { useFileUpload } from "./useFileUpload"; 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, dispatch } = useSoundContext(); const [sightings, setSightings] = useState([]); const [selectedRef, setSelectedRef] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); const [selectedSighting, setSelectedSighting] = useState(null); const isUploaded = state?.sightingSound?.endsWith(".mp3") || state?.sightingSound?.endsWith(".wav"); const fileName = isUploaded ? state.sightingSound : switchSound; const { query: fileQuery } = useFileUpload({ queryKey: fileName ? [fileName] : undefined, }); const objUrlRef = useRef(null); const soundSrcHotlist = useMemo(() => { if (state?.hotlistSound?.includes(".mp3") || state.hotlistSound?.includes(".wav")) { const file = state.soundOptions?.find((item) => item.name === state.hotlistSound); return file?.soundUrl ?? notification; } return getSoundFileURL(state?.hotlistSound) ?? notification; }, [state.hotlistSound, state.soundOptions]); const soundSrcNped = useMemo(() => { if (state?.NPEDsound?.includes(".mp3") || state.NPEDsound?.includes(".wav")) { const file = state.soundOptions?.find((item) => item.name === state.NPEDsound); return file?.soundUrl ?? popup; } return getSoundFileURL(state.NPEDsound) ?? popup; }, [state.NPEDsound, state.soundOptions]); const soundSrc = useMemo(() => { if (isUploaded && fileQuery?.data instanceof Blob) { if (objUrlRef.current) URL.revokeObjectURL(objUrlRef.current); objUrlRef.current = URL.createObjectURL(fileQuery.data); console.log(fileQuery.data); return objUrlRef.current; } return getSoundFileURL(state?.sightingSound) ?? switchSound; }, [isUploaded, fileQuery?.data, state?.sightingSound]); useEffect(() => { return () => { if (objUrlRef.current) URL.revokeObjectURL(objUrlRef.current); }; }, []); 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, }; }