2025-10-28 13:53:11 +00:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
2025-10-27 14:00:28 +00:00
|
|
|
import { useDebouncedCallback } from "use-debounce";
|
2025-10-15 11:00:52 +01:00
|
|
|
import { Query, useQuery } from "@tanstack/react-query";
|
2025-09-16 14:20:38 +01:00
|
|
|
import type { SightingType } from "../types/types";
|
2025-10-27 14:00:28 +00:00
|
|
|
import { useSound } from "react-sounds";
|
2025-09-30 14:51:37 +01:00
|
|
|
import { useSoundContext } from "../context/SoundContext";
|
2025-10-28 13:53:11 +00:00
|
|
|
import { checkIsHotListHit, getNPEDCategory } from "../utils/utils";
|
2025-09-30 15:32:00 +01:00
|
|
|
import switchSound from "../assets/sounds/ui/switch.mp3";
|
2025-10-27 14:00:28 +00:00
|
|
|
import notification from "../assets/sounds/ui/notification.mp3";
|
|
|
|
|
import popup from "../assets/sounds/ui/popup_open.mp3";
|
2025-10-28 13:53:11 +00:00
|
|
|
import { useCachedSoundSrc } from "./usecachedSoundSrc";
|
2025-08-20 08:27:05 +01:00
|
|
|
|
2025-10-15 11:00:52 +01:00
|
|
|
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
|
2025-10-06 14:21:56 +01:00
|
|
|
const res = await fetch(`${url}${ref}`, {
|
|
|
|
|
signal: AbortSignal.timeout(5000),
|
|
|
|
|
});
|
2025-08-22 10:38:28 +01:00
|
|
|
if (!res.ok) throw new Error(String(res.status));
|
2025-09-17 11:39:26 +01:00
|
|
|
return res.json();
|
2025-08-22 10:38:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-25 10:38:49 +01:00
|
|
|
export function useSightingFeed(url: string | undefined) {
|
2025-10-28 13:53:11 +00:00
|
|
|
const { state, audioArmed } = useSoundContext();
|
2025-09-16 14:20:38 +01:00
|
|
|
const [sightings, setSightings] = useState<SightingType[]>([]);
|
2025-08-20 08:27:05 +01:00
|
|
|
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
2025-09-25 10:38:49 +01:00
|
|
|
const [sessionStarted, setSessionStarted] = useState(false);
|
2025-10-15 11:00:52 +01:00
|
|
|
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
|
|
|
|
|
|
2025-10-28 13:53:11 +00:00
|
|
|
const { src: soundSrc } = useCachedSoundSrc(state?.sightingSound, switchSound);
|
|
|
|
|
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, notification);
|
|
|
|
|
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, popup);
|
2025-08-20 08:27:05 +01:00
|
|
|
|
2025-10-27 14:00:28 +00:00
|
|
|
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<number>(-1);
|
|
|
|
|
const lastValidTimestamp = useRef<number>(Date.now());
|
|
|
|
|
|
2025-10-15 11:00:52 +01:00
|
|
|
function refetchInterval(query: Query<SightingType, Error, SightingType, (string | undefined)[]>) {
|
|
|
|
|
if (!query) return;
|
|
|
|
|
const data = query.state.data as SightingType | undefined;
|
|
|
|
|
const now = Date.now();
|
2025-09-23 13:03:54 +01:00
|
|
|
|
2025-10-15 11:00:52 +01:00
|
|
|
if (data && data.ref !== -1) {
|
|
|
|
|
lastValidTimestamp.current = now;
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (now - lastValidTimestamp.current > 60_000) {
|
|
|
|
|
currentRef.current = -1;
|
|
|
|
|
lastValidTimestamp.current = now;
|
|
|
|
|
}
|
|
|
|
|
return 400;
|
|
|
|
|
}
|
2025-08-20 08:27:05 +01:00
|
|
|
|
2025-09-17 11:39:26 +01:00
|
|
|
const query = useQuery({
|
|
|
|
|
queryKey: ["sighting-feed", url],
|
|
|
|
|
enabled: !!url,
|
|
|
|
|
queryFn: () => fetchSighting(url, currentRef.current),
|
2025-10-15 11:00:52 +01:00
|
|
|
refetchInterval: (q) => refetchInterval(q),
|
2025-09-17 11:39:26 +01:00
|
|
|
refetchIntervalInBackground: true,
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
retry: false,
|
|
|
|
|
staleTime: 0,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-27 14:00:28 +00:00
|
|
|
const playHotlistsound = useDebouncedCallback(() => {
|
|
|
|
|
hotlistsound();
|
|
|
|
|
}, 500);
|
2025-10-17 16:12:02 +01:00
|
|
|
|
2025-10-27 14:00:28 +00:00
|
|
|
const playNPEDHitSound = useDebouncedCallback(() => {
|
|
|
|
|
npedSound();
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
const playSightingHitSound = useDebouncedCallback(() => {
|
|
|
|
|
sightingSound();
|
|
|
|
|
}, 500);
|
2025-10-15 11:00:52 +01:00
|
|
|
|
2025-09-17 11:39:26 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const data = query.data;
|
2025-10-27 14:00:28 +00:00
|
|
|
|
2025-10-15 11:00:52 +01:00
|
|
|
if (!data || data.ref === -1) return;
|
2025-10-27 14:00:28 +00:00
|
|
|
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();
|
|
|
|
|
}
|
2025-09-17 11:39:26 +01:00
|
|
|
|
|
|
|
|
const now = Date.now();
|
2025-08-22 10:38:28 +01:00
|
|
|
|
2025-09-17 11:39:26 +01:00
|
|
|
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]);
|
|
|
|
|
|
2025-08-20 08:27:05 +01:00
|
|
|
return {
|
2025-08-22 10:38:28 +01:00
|
|
|
sightings,
|
2025-08-20 08:27:05 +01:00
|
|
|
selectedRef,
|
|
|
|
|
setSelectedRef,
|
|
|
|
|
mostRecent,
|
2025-09-12 08:21:52 +01:00
|
|
|
selectedSighting,
|
2025-09-25 10:38:49 +01:00
|
|
|
sessionStarted,
|
|
|
|
|
setSessionStarted,
|
2025-09-17 11:39:26 +01:00
|
|
|
setSelectedSighting,
|
|
|
|
|
data: query.data,
|
|
|
|
|
isLoading: query.isLoading,
|
|
|
|
|
isFetching: query.isFetching,
|
|
|
|
|
isError: query.isError,
|
|
|
|
|
error: query.error as Error | null,
|
|
|
|
|
refetch: query.refetch,
|
2025-08-20 08:27:05 +01:00
|
|
|
};
|
|
|
|
|
}
|