import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { HitKind, QueuedHit, ReducedSightingType, SightingType } from "../../types/types"; import { BLANK_IMG } from "../../utils/utils"; import NumberPlate from "../PlateStack/NumberPlate"; import Card from "../UI/Card"; import CardHeader from "../UI/CardHeader"; import clsx from "clsx"; import { useSightingFeedContext } from "../../context/SightingFeedContext"; import SightingModal from "../SightingModal/SightingModal"; import { useAlertHitContext } from "../../context/AlertHitContext"; import HotListImg from "/Hotlist_Hit.svg"; import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg"; import popup from "../../assets/sounds/ui/popup_open.mp3"; import notification from "../../assets/sounds/ui/notification.mp3"; import { useSound } from "react-sounds"; import { useIntegrationsContext } from "../../context/IntegrationsContext"; import { useSoundContext } from "../../context/SoundContext"; import Loading from "../UI/Loading"; import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils"; import { useCachedSoundSrc } from "../../hooks/usecachedSoundSrc"; function useNow(tickMs = 1000) { const [, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), tickMs); return () => clearInterval(id); }, [tickMs]); return null; } type SightingHistoryProps = { baseUrl?: string; entries?: number; pollMs?: number; autoSelectLatest?: boolean; title: string; className?: string; }; export default function SightingHistoryWidget({ className, title }: SightingHistoryProps) { const [modalQueue, setModalQueue] = useState([]); useNow(1000); const { state } = useSoundContext(); const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification); const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup); const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume }); const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume }); const { sightings, setSelectedSighting, setSightingModalOpen, isSightingModalOpen, selectedSighting, mostRecent, isLoading, } = useSightingFeedContext(); const { dispatch, state: alertState } = useAlertHitContext(); const { state: integrationState, dispatch: integrationDispatch } = useIntegrationsContext(); const sessionStarted = integrationState.sessionStarted; const sessionPaused = integrationState.sessionPaused; const processedRefs = useRef>(new Set()); const hasAutoOpenedRef = useRef(false); const npedRef = useRef(false); const enqueue = useCallback((sighting: SightingType, kind: HitKind) => { const id = sighting.vrm ?? sighting.ref; if (processedRefs.current.has(id)) return; const inList = alertState?.alertList?.find((sighting) => sighting.vrm === id); if (inList) { return; } processedRefs.current.add(id); setModalQueue((q) => [...q, { id, sighting, kind }]); }, []); const reduceObject = (obj: SightingType): ReducedSightingType => { return { vrm: obj.vrm, metadata: obj?.metadata, }; }; useEffect(() => { if (sessionStarted) { if (!mostRecent) return; if (sessionPaused) return; const reducedMostRecent = reduceObject(mostRecent); integrationDispatch({ type: "ADD", payload: reducedMostRecent }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mostRecent, sessionStarted]); const onRowClick = useCallback( (sighting: SightingType) => { if (!sighting) return; setSightingModalOpen(true); setSelectedSighting(sighting); }, [setSelectedSighting, setSightingModalOpen] ); const rows = useMemo(() => sightings?.filter(Boolean) as SightingType[], [sightings]); useEffect(() => { if (!rows?.length) return; for (const sighting of rows) { const id = sighting.vrm; if (processedRefs.current.has(id)) continue; const isHotlistHit = checkIsHotListHit(sighting); const npedcategory = sighting?.metadata?.npedJSON?.["NPED CATEGORY"]; const isNPED = npedcategory === "A" || npedcategory === "B" || npedcategory === "C"; if (isNPED || isHotlistHit) { enqueue(sighting, isNPED ? "NPED" : "HOTLIST"); // enqueue ONLY } } }, [rows, enqueue]); useEffect(() => { rows?.forEach((obj) => { const cat = getNPEDCategory(obj); const isNPEDHitA = cat === "A"; const isNPEDHitB = cat === "B"; const isNPEDHitC = cat === "C"; if (isNPEDHitA || isNPEDHitB || isNPEDHitC) { dispatch({ type: "ADD", payload: obj, }); } }); }, [dispatch]); useEffect(() => { if (hasAutoOpenedRef.current || npedRef.current) return; const firstNPED = rows.find((r) => { const cat = getNPEDCategory(r); const isNPEDHitA = cat === "A"; const isNPEDHitB = cat === "B"; const isNPEDHitC = cat === "C"; return isNPEDHitA || isNPEDHitB || isNPEDHitC; }); const firstHot = rows?.find((r) => { const isHotListHit = checkIsHotListHit(r); return isHotListHit; }); if (firstNPED) { enqueue(firstNPED, "NPED"); npedRef.current = true; } if (firstHot) { enqueue(firstHot, "HOTLIST"); hasAutoOpenedRef.current = true; } }, [enqueue, hotlistsound, npedSound, rows, setSelectedSighting, setSightingModalOpen]); useEffect(() => { if (!isSightingModalOpen && modalQueue.length > 0) { const next = modalQueue[0]; if (next.kind === "NPED") npedSound(); else hotlistsound(); setSelectedSighting(next.sighting); setSightingModalOpen(true); } }, [isSightingModalOpen, npedSound, hotlistsound, setSelectedSighting, setSightingModalOpen, modalQueue]); const handleClose = () => { setSightingModalOpen(false); setModalQueue((q) => q.slice(1)); }; return ( <>
{isLoading && (
)} {/* Rows */}
{rows?.map((obj) => { const cat = getNPEDCategory(obj); const isNPEDHitA = cat === "A"; const isNPEDHitB = cat === "B"; const isNPEDHitC = cat === "C"; const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY"; const isHotListHit = checkIsHotListHit(obj); return (
onRowClick(obj)} >
colour patch
{isHotListHit && ( hotlistHit )} {isNPEDHitA && hotlistHit} {isNPEDHitB && hotlistHit} {isNPEDHitC && hotlistHit}
); })}
); }