diff --git a/frigate/util/media.py b/frigate/util/media.py index c7de85c9f..52d6b2c22 100644 --- a/frigate/util/media.py +++ b/frigate/util/media.py @@ -151,7 +151,9 @@ def sync_recordings( max_inserts = 1000 for batch in chunked(recordings_to_delete, max_inserts): - RecordingsToDelete.insert_many(batch).execute() + RecordingsToDelete.insert_many( + [{"id": r["id"]} for r in batch] + ).execute() try: deleted = ( diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx index 34389c5cf..b2ef9d2f1 100644 --- a/web/src/hooks/use-overlay-state.tsx +++ b/web/src/hooks/use-overlay-state.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useMemo } from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { usePersistence } from "./use-persistence"; import { useUserPersistence } from "./use-user-persistence"; @@ -12,20 +12,28 @@ export function useOverlayState( const location = useLocation(); const navigate = useNavigate(); - const currentLocationState = useMemo(() => location.state, [location]); + const locationRef = useRef(location); + locationRef.current = location; const setOverlayStateValue = useCallback( (value: S, replace: boolean = false) => { - const newLocationState = { ...currentLocationState }; + const loc = locationRef.current; + const currentValue = loc.state?.[key] as S | undefined; + + if (Object.is(currentValue, value)) { + return; + } + + const newLocationState = { ...loc.state }; newLocationState[key] = value; - navigate(location.pathname + (preserveSearch ? location.search : ""), { + navigate(loc.pathname + (preserveSearch ? loc.search : ""), { state: newLocationState, replace, }); }, - // we know that these deps are correct + // locationRef is stable so we don't need it in deps // eslint-disable-next-line react-hooks/exhaustive-deps - [key, currentLocationState, navigate], + [key, navigate, preserveSearch], ); const overlayStateValue = useMemo( @@ -47,7 +55,9 @@ export function usePersistedOverlayState( ] { const location = useLocation(); const navigate = useNavigate(); - const currentLocationState = useMemo(() => location.state, [location]); + + const locationRef = useRef(location); + locationRef.current = location; // currently selected value @@ -63,14 +73,21 @@ export function usePersistedOverlayState( const setOverlayStateValue = useCallback( (value: S | undefined, replace: boolean = false) => { + const loc = locationRef.current; + const currentValue = loc.state?.[key] as S | undefined; + + if (Object.is(currentValue, value)) { + return; + } + setPersistedValue(value); - const newLocationState = { ...currentLocationState }; + const newLocationState = { ...loc.state }; newLocationState[key] = value; - navigate(location.pathname, { state: newLocationState, replace }); + navigate(loc.pathname, { state: newLocationState, replace }); }, - // we know that these deps are correct + // locationRef is stable so we don't need it in deps // eslint-disable-next-line react-hooks/exhaustive-deps - [key, currentLocationState, navigate], + [key, navigate, setPersistedValue], ); return [ @@ -98,7 +115,9 @@ export function useUserPersistedOverlayState( const { auth } = useContext(AuthContext); const location = useLocation(); const navigate = useNavigate(); - const currentLocationState = useMemo(() => location.state, [location]); + + const locationRef = useRef(location); + locationRef.current = location; // currently selected value from URL state const overlayStateValue = useMemo( @@ -112,14 +131,21 @@ export function useUserPersistedOverlayState( const setOverlayStateValue = useCallback( (value: S | undefined, replace: boolean = false) => { + const loc = locationRef.current; + const currentValue = loc.state?.[key] as S | undefined; + + if (Object.is(currentValue, value)) { + return; + } + setPersistedValue(value); - const newLocationState = { ...currentLocationState }; + const newLocationState = { ...loc.state }; newLocationState[key] = value; - navigate(location.pathname, { state: newLocationState, replace }); + navigate(loc.pathname, { state: newLocationState, replace }); }, - // we know that these deps are correct + // locationRef is stable so we don't need it in deps // eslint-disable-next-line react-hooks/exhaustive-deps - [key, currentLocationState, navigate, setPersistedValue], + [key, navigate, setPersistedValue], ); // Don't return a value until auth has finished loading @@ -142,17 +168,21 @@ export function useHashState(): [ const location = useLocation(); const navigate = useNavigate(); + const locationRef = useRef(location); + locationRef.current = location; + const setHash = useCallback( (value: S | undefined) => { + const loc = locationRef.current; if (!value) { - navigate(location.pathname); + navigate(loc.pathname); } else { - navigate(`${location.pathname}#${value}`, { state: location.state }); + navigate(`${loc.pathname}#${value}`, { state: loc.state }); } }, - // we know that these deps are correct + // locationRef is stable so we don't need it in deps // eslint-disable-next-line react-hooks/exhaustive-deps - [location, navigate], + [navigate], ); const hash = useMemo( diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 1b680967f..342865527 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -632,9 +632,10 @@ export default function DraggableGridLayout({ toggleStats={() => toggleStats(camera.name)} volumeState={volumeStates[camera.name]} setVolumeState={(value) => - setVolumeStates({ + setVolumeStates((prev) => ({ + ...prev, [camera.name]: value, - }) + })) } muteAll={muteAll} unmuteAll={unmuteAll} diff --git a/web/src/views/motion-search/MotionSearchView.tsx b/web/src/views/motion-search/MotionSearchView.tsx index 6789dad89..122e05985 100644 --- a/web/src/views/motion-search/MotionSearchView.tsx +++ b/web/src/views/motion-search/MotionSearchView.tsx @@ -131,12 +131,10 @@ export default function MotionSearchView({ ); // Camera previews – defer until dialog is closed - const allPreviews = useCameraPreviews( - isSearchDialogOpen ? { after: 0, before: 0 } : timeRange, - { - camera: selectedCamera ?? undefined, - }, - ); + const allPreviews = useCameraPreviews(timeRange, { + camera: selectedCamera ?? undefined, + fetchPreviews: !isSearchDialogOpen, + }); // ROI state const [polygonPoints, setPolygonPoints] = useState([]);