diff --git a/frigate/api/review.py b/frigate/api/review.py index 9f84fb9a1..5807f90ef 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -26,7 +26,7 @@ def review(): cameras = request.args.get("cameras", "all") labels = request.args.get("labels", "all") reviewed = request.args.get("reviewed", type=int, default=0) - limit = request.args.get("limit", 100) + limit = request.args.get("limit", type=int, default=None) severity = request.args.get("severity", None) before = request.args.get("before", type=float, default=datetime.now().timestamp()) diff --git a/web/src/components/dynamic/NewReviewData.tsx b/web/src/components/dynamic/NewReviewData.tsx index b1b094e08..8a4752470 100644 --- a/web/src/components/dynamic/NewReviewData.tsx +++ b/web/src/components/dynamic/NewReviewData.tsx @@ -8,19 +8,22 @@ type NewReviewDataProps = { className: string; contentRef: MutableRefObject; severity: ReviewSeverity; + hasUpdate: boolean; + setHasUpdate: (update: boolean) => void; pullLatestData: () => void; }; export default function NewReviewData({ className, contentRef, severity, + hasUpdate, + setHasUpdate, pullLatestData, }: NewReviewDataProps) { const { payload: review } = useFrigateReviews(); const startCheckTs = useMemo(() => Date.now() / 1000, []); const [reviewTs, setReviewTs] = useState(startCheckTs); - const [hasUpdate, setHasUpdate] = useState(false); useEffect(() => { if (!review) { @@ -36,7 +39,7 @@ export default function NewReviewData({ if (reviewTs > startCheckTs) { setHasUpdate(true); } - }, [startCheckTs, reviewTs]); + }, [startCheckTs, reviewTs, setHasUpdate]); return (
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index ab45850a7..3bb199fea 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -19,9 +19,6 @@ import axios from "axios"; import { useCallback, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; import useSWR from "swr"; -import useSWRInfinite from "swr/infinite"; - -const API_LIMIT = 100; export default function Events() { const { data: config } = useSWR("config"); @@ -41,7 +38,6 @@ export default function Events() { useApiFilter(); const onUpdateFilter = useCallback((newFilter: ReviewFilter) => { - setSize(1); setReviewFilter(newFilter); // we don't want this updating // eslint-disable-next-line react-hooks/exhaustive-deps @@ -69,53 +65,25 @@ export default function Events() { return axios.get(path, { params }).then((res) => res.data); }, []); - const getKey = useCallback( - (index: number, prevData: ReviewSegment[]) => { - if (index > 0) { - const lastDate = prevData[prevData.length - 1].start_time; - reviewSearchParams; - const pagedParams = { - cameras: reviewSearchParams["cameras"], - labels: reviewSearchParams["labels"], - reviewed: reviewSearchParams["showReviewed"], - before: lastDate, - after: reviewSearchParams["after"] || last24Hours.after, - limit: API_LIMIT, - }; - return ["review", pagedParams]; - } + const getKey = useCallback(() => { + const params = { + cameras: reviewSearchParams["cameras"], + labels: reviewSearchParams["labels"], + reviewed: 1, + before: reviewSearchParams["before"] || last24Hours.before, + after: reviewSearchParams["after"] || last24Hours.after, + }; + return ["review", params]; + }, [reviewSearchParams, last24Hours]); - const params = { - cameras: reviewSearchParams["cameras"], - labels: reviewSearchParams["labels"], - reviewed: reviewSearchParams["showReviewed"], - limit: API_LIMIT, - before: reviewSearchParams["before"] || last24Hours.before, - after: reviewSearchParams["after"] || last24Hours.after, - }; - return ["review", params]; + const { data: reviews, mutate: updateSegments } = useSWR( + getKey, + reviewSegmentFetcher, + { + revalidateOnFocus: false, }, - [reviewSearchParams, last24Hours], ); - const { - data: reviewPages, - mutate: updateSegments, - size, - setSize, - isValidating, - } = useSWRInfinite(getKey, reviewSegmentFetcher, { - revalidateOnFocus: false, - persistSize: true, - }); - - const isDone = useMemo( - () => (reviewPages?.at(-1)?.length ?? 0) < API_LIMIT, - [reviewPages], - ); - - const onLoadNextPage = useCallback(() => setSize(size + 1), [size, setSize]); - // review summary const { data: reviewSummary, mutate: updateSummary } = useSWR([ @@ -136,24 +104,20 @@ export default function Events() { // preview videos const previewTimes = useMemo(() => { - if ( - !reviewPages || - reviewPages.length == 0 || - reviewPages.at(-1)?.length == 0 - ) { + if (!reviews || reviews.length == 0) { return undefined; } const startDate = new Date(); startDate.setMinutes(0, 0, 0); - const endDate = new Date(reviewPages.at(-1)?.at(-1)?.end_time || 0); + const endDate = new Date(reviews.at(-1)?.end_time || 0); endDate.setHours(0, 0, 0, 0); return { start: startDate.getTime() / 1000, end: endDate.getTime() / 1000, }; - }, [reviewPages]); + }, [reviews]); const { data: allPreviews } = useSWR( previewTimes ? `preview/all/start/${previewTimes.start}/end/${previewTimes.end}` @@ -169,28 +133,21 @@ export default function Events() { if (resp.status == 200) { updateSegments( - (data: ReviewSegment[][] | undefined) => { + (data: ReviewSegment[] | undefined) => { if (!data) { return data; } - const newData: ReviewSegment[][] = []; + const reviewIndex = data.findIndex((item) => item.id == review.id); + if (reviewIndex == -1) { + return data; + } - data.forEach((page) => { - const reviewIndex = page.findIndex( - (item) => item.id == review.id, - ); - - if (reviewIndex == -1) { - newData.push([...page]); - } else { - newData.push([ - ...page.slice(0, reviewIndex), - { ...page[reviewIndex], has_been_reviewed: true }, - ...page.slice(reviewIndex + 1), - ]); - } - }); + const newData = [ + ...data.slice(0, reviewIndex), + { ...data[reviewIndex], has_been_reviewed: true }, + ...data.slice(reviewIndex + 1), + ]; return newData; }, @@ -252,7 +209,7 @@ export default function Events() { return undefined; } - if (!reviewPages) { + if (!reviews) { return undefined; } @@ -262,8 +219,6 @@ export default function Events() { const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras); - const allReviews = reviewPages.flat(); - if (selectedReviewId.startsWith("motion")) { const motionData = selectedReviewId.split(","); // format is motion,camera,start_time @@ -272,15 +227,13 @@ export default function Events() { severity: "significant_motion" as ReviewSeverity, start_time: parseFloat(motionData[2]), allCameras: allCameras, - cameraSegments: allReviews.filter((seg) => + cameraSegments: reviews.filter((seg) => allCameras.includes(seg.camera), ), }; } - const selectedReview = allReviews.find( - (item) => item.id == selectedReviewId, - ); + const selectedReview = reviews.find((item) => item.id == selectedReviewId); if (!selectedReview) { return undefined; @@ -296,14 +249,12 @@ export default function Events() { severity: selectedReview.severity, start_time: selectedReview.start_time, allCameras: allCameras, - cameraSegments: allReviews.filter((seg) => - allCameras.includes(seg.camera), - ), + cameraSegments: reviews.filter((seg) => allCameras.includes(seg.camera)), }; // previews will not update after item is selected // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedReviewId, reviewPages]); + }, [selectedReviewId, reviews]); if (!timezone) { return ; @@ -335,16 +286,13 @@ export default function Events() { } else { return ( void; - loadNextPage: () => void; markItemAsReviewed: (review: ReviewSegment) => void; onOpenReview: (reviewId: string) => void; pullLatestData: () => void; updateFilter: (filter: ReviewFilter) => void; }; export default function EventView({ - reviewPages, + reviews, reviewSummary, relevantPreviews, timeRange, - reachedEnd, - isValidating, filter, severity, setSeverity, - loadNextPage, markItemAsReviewed, onOpenReview, pullLatestData, @@ -115,22 +109,20 @@ export default function EventView({ const detections: ReviewSegment[] = []; const motion: ReviewSegment[] = []; - reviewPages?.forEach((page) => { - page.forEach((segment) => { - all.push(segment); + reviews?.forEach((segment) => { + all.push(segment); - switch (segment.severity) { - case "alert": - alerts.push(segment); - break; - case "detection": - detections.push(segment); - break; - default: - motion.push(segment); - break; - } - }); + switch (segment.severity) { + case "alert": + alerts.push(segment); + break; + case "detection": + detections.push(segment); + break; + default: + motion.push(segment); + break; + } }); return { @@ -139,7 +131,7 @@ export default function EventView({ detection: detections, significant_motion: motion, }; - }, [reviewPages]); + }, [reviews]); const currentItems = useMemo(() => { const current = reviewItems[severity]; @@ -151,8 +143,6 @@ export default function EventView({ return current; }, [reviewItems, severity]); - const pagingObserver = useRef(null); - // review interaction const [selectedReviews, setSelectedReviews] = useState([]); @@ -267,14 +257,11 @@ export default function EventView({ currentItems={currentItems} reviewItems={reviewItems} relevantPreviews={relevantPreviews} - pagingObserver={pagingObserver} selectedReviews={selectedReviews} + itemsToReview={reviewCounts[severity]} severity={severity} filter={filter} - isValidating={isValidating} - reachedEnd={reachedEnd} timeRange={timeRange} - loadNextPage={loadNextPage} markItemAsReviewed={markItemAsReviewed} onSelectReview={onSelectReview} pullLatestData={pullLatestData} @@ -306,14 +293,10 @@ type DetectionReviewProps = { }; itemsToReview?: number; relevantPreviews?: Preview[]; - pagingObserver: MutableRefObject; selectedReviews: string[]; severity: ReviewSeverity; filter?: ReviewFilter; - isValidating: boolean; - reachedEnd: boolean; timeRange: { before: number; after: number }; - loadNextPage: () => void; markItemAsReviewed: (review: ReviewSegment) => void; onSelectReview: (id: string, ctrl: boolean) => void; pullLatestData: () => void; @@ -324,14 +307,10 @@ function DetectionReview({ reviewItems, itemsToReview, relevantPreviews, - pagingObserver, selectedReviews, severity, filter, - isValidating, - reachedEnd, timeRange, - loadNextPage, markItemAsReviewed, onSelectReview, pullLatestData, @@ -344,23 +323,7 @@ function DetectionReview({ // review interaction - const lastReviewRef = useCallback( - (node: HTMLElement | null) => { - if (isValidating) return; - if (pagingObserver.current) pagingObserver.current.disconnect(); - try { - pagingObserver.current = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !reachedEnd) { - loadNextPage(); - } - }); - if (node) pagingObserver.current.observe(node); - } catch (e) { - // no op - } - }, - [isValidating, pagingObserver, reachedEnd, loadNextPage], - ); + const [hasUpdate, setHasUpdate] = useState(false); const markAllReviewed = useCallback(async () => { if (!currentItems) { @@ -370,6 +333,7 @@ function DetectionReview({ await axios.post(`reviews/viewed`, { ids: currentItems?.map((seg) => seg.id), }); + setHasUpdate(false); pullLatestData(); }, [currentItems, pullLatestData]); @@ -463,11 +427,13 @@ function DetectionReview({ className="absolute w-full z-30" contentRef={contentRef} severity={severity} + hasUpdate={hasUpdate} + setHasUpdate={setHasUpdate} pullLatestData={pullLatestData} /> )} - {(itemsToReview == 0 || (currentItems == null && !isValidating)) && ( + {itemsToReview == 0 && (
There are no {severity.replace(/_/g, " ")} items to review @@ -478,15 +444,18 @@ function DetectionReview({ className="w-full m-2 grid sm:grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 gap-2 md:gap-4" ref={contentRef} > - {currentItems ? ( - currentItems.map((value, segIdx) => { - const lastRow = segIdx == currentItems.length - 1; + {currentItems && + currentItems.map((value) => { + if (value.has_been_reviewed && filter?.showReviewed != 1) { + return; + } + const selected = selectedReviews.includes(value.id); return (
); - }) - ) : itemsToReview != 0 ? ( -
- ) : null} - {currentItems && ( + })} + {(itemsToReview ?? 0) > 0 && (
- {reachedEnd ? ( - - ) : ( - - )} +
)}