diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index 576344be9..121083b9c 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -11,6 +11,7 @@ import EventSegment from "./EventSegment"; import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import ReviewTimeline from "./ReviewTimeline"; +import scrollIntoView from "scroll-into-view-if-needed"; export type EventReviewTimelineProps = { segmentDuration: number; @@ -60,6 +61,7 @@ export function EventReviewTimeline({ const [isDragging, setIsDragging] = useState(false); const [exportStartPosition, setExportStartPosition] = useState(0); const [exportEndPosition, setExportEndPosition] = useState(0); + const [visibleThumbnails, setVisibleThumbnails] = useState([]); const internalTimelineRef = useRef(null); const handlebarRef = useRef(null); @@ -68,6 +70,9 @@ export function EventReviewTimeline({ const exportStartTimeRef = useRef(null); const exportEndRef = useRef(null); const exportEndTimeRef = useRef(null); + const selectedTimelineRef = timelineRef || internalTimelineRef; + + const observer = useRef(null); const timelineDuration = useMemo( () => timelineStart - timelineEnd, @@ -77,7 +82,7 @@ export function EventReviewTimeline({ const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( segmentDuration, timelineDuration, - timelineRef || internalTimelineRef, + selectedTimelineRef, ); const timelineStartAligned = useMemo( @@ -103,7 +108,7 @@ export function EventReviewTimeline({ handleMouseMove: handlebarMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: handlebarRef, segmentDuration, showDraggableElement: showHandlebar, @@ -122,7 +127,7 @@ export function EventReviewTimeline({ handleMouseMove: exportStartMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: exportStartRef, segmentDuration, showDraggableElement: showExportHandles, @@ -143,7 +148,7 @@ export function EventReviewTimeline({ handleMouseMove: exportEndMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: exportEndRef, segmentDuration, showDraggableElement: showExportHandles, @@ -216,9 +221,67 @@ export function EventReviewTimeline({ } }, [isDragging, onHandlebarDraggingChange]); + useEffect(() => { + if (contentRef.current && segments) { + observer.current = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const segmentStartId = + entry.target.getAttribute("data-segment-start"); + if (segmentStartId) { + setVisibleThumbnails((prevState) => [ + ...prevState, + parseInt(segmentStartId), + ]); + } + } else { + const segmentStartId = + entry.target.getAttribute("data-segment-start"); + if (segmentStartId) { + setVisibleThumbnails((prevState) => + prevState.filter( + (timestamp) => timestamp !== parseInt(segmentStartId), + ), + ); + } + } + }); + }, + { threshold: 0.5 }, + ); + + const reviewItems = contentRef.current.querySelectorAll(".review-item"); + reviewItems.forEach((item) => { + observer.current?.observe(item); + }); + } + + return () => { + observer.current?.disconnect(); + }; + }, [contentRef, segments]); + + useEffect(() => { + if ( + selectedTimelineRef.current && + segments && + visibleThumbnails?.length > 0 && + !showMinimap + ) { + const element = selectedTimelineRef.current?.querySelector( + `[data-segment-id="${Math.max(...visibleThumbnails)}"]`, + ); + scrollIntoView(element as HTMLDivElement, { + scrollMode: "if-needed", + behavior: "smooth", + }); + } + }, [visibleThumbnails, selectedTimelineRef, segments, showMinimap]); + return ( handleTouchStart(event, segmentClick)} diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index ed2216722..d650b6ee0 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -508,7 +508,7 @@ function DetectionReview({ data-segment-start={ alignStartDateToTimeline(value.start_time) - segmentDuration } - className={`outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`} + className={`review-item outline outline-offset-1 rounded-lg shadow-none transition-all my-1 md:my-0 ${selected ? `outline-4 shadow-[0_0_6px_1px] outline-severity_${value.severity} shadow-severity_${value.severity}` : "outline-0 duration-500"}`} >