From 0ac7aaabe329f8fbdb5653f7892cdfc8f50c8f4b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:49:04 -0500 Subject: [PATCH] Timeline minimap and scrolling changes (#10589) * add function to get visible timeline duration * Don't show minimap when minimap bounds exceed timeline area * when minimap is hidden, only scroll timeline when needed * observe only when not showing minimap * no need to duplicate observer * fix out of order param * timeline utils hook props --------- Co-authored-by: Nicolas Mowen --- .../timeline/EventReviewTimeline.tsx | 48 ++++++++++++++++--- web/src/components/timeline/EventSegment.tsx | 6 ++- .../timeline/MotionReviewTimeline.tsx | 8 +++- web/src/components/timeline/MotionSegment.tsx | 5 +- .../components/timeline/SummaryTimeline.tsx | 16 ++++--- web/src/hooks/use-draggable-element.ts | 9 +++- web/src/hooks/use-timeline-utils.ts | 32 ++++++++++++- web/src/views/events/EventView.tsx | 35 ++++++++++++-- 8 files changed, 133 insertions(+), 26 deletions(-) diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index f8fb75817..1e165d799 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; @@ -29,6 +30,7 @@ export type EventReviewTimelineProps = { setExportStartTime?: React.Dispatch>; setExportEndTime?: React.Dispatch>; events: ReviewSegment[]; + visibleTimestamps?: number[]; severityType: ReviewSeverity; timelineRef?: RefObject; contentRef: RefObject; @@ -52,6 +54,7 @@ export function EventReviewTimeline({ setExportStartTime, setExportEndTime, events, + visibleTimestamps, severityType, timelineRef, contentRef, @@ -68,14 +71,20 @@ export function EventReviewTimeline({ const exportStartTimeRef = useRef(null); const exportEndRef = useRef(null); const exportEndTimeRef = useRef(null); + const selectedTimelineRef = timelineRef || internalTimelineRef; const timelineDuration = useMemo( () => timelineStart - timelineEnd, [timelineEnd, timelineStart], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = - useTimelineUtils(segmentDuration); + const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( + { + segmentDuration, + timelineDuration, + timelineRef: selectedTimelineRef, + }, + ); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart), @@ -100,7 +109,7 @@ export function EventReviewTimeline({ handleMouseMove: handlebarMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: handlebarRef, segmentDuration, showDraggableElement: showHandlebar, @@ -119,7 +128,7 @@ export function EventReviewTimeline({ handleMouseMove: exportStartMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: exportStartRef, segmentDuration, showDraggableElement: showExportHandles, @@ -140,7 +149,7 @@ export function EventReviewTimeline({ handleMouseMove: exportEndMouseMove, } = useDraggableElement({ contentRef, - timelineRef: timelineRef || internalTimelineRef, + timelineRef: selectedTimelineRef, draggableElementRef: exportEndRef, segmentDuration, showDraggableElement: showExportHandles, @@ -213,9 +222,36 @@ export function EventReviewTimeline({ } }, [isDragging, onHandlebarDraggingChange]); + useEffect(() => { + if ( + selectedTimelineRef.current && + segments && + visibleTimestamps && + visibleTimestamps?.length > 0 && + !showMinimap + ) { + const alignedVisibleTimestamps = visibleTimestamps.map( + alignStartDateToTimeline, + ); + const element = selectedTimelineRef.current?.querySelector( + `[data-segment-id="${Math.max(...alignedVisibleTimestamps)}"]`, + ); + scrollIntoView(element as HTMLDivElement, { + scrollMode: "if-needed", + behavior: "smooth", + }); + } + }, [ + selectedTimelineRef, + segments, + showMinimap, + alignStartDateToTimeline, + visibleTimestamps, + ]); + return ( getSeverity(segmentTime, displaySeverityType), @@ -199,6 +200,7 @@ export function EventSegment({ return (
handleTouchStart(event, segmentClick)} diff --git a/web/src/components/timeline/MotionReviewTimeline.tsx b/web/src/components/timeline/MotionReviewTimeline.tsx index a3115bb0a..9255df53e 100644 --- a/web/src/components/timeline/MotionReviewTimeline.tsx +++ b/web/src/components/timeline/MotionReviewTimeline.tsx @@ -76,8 +76,12 @@ export function MotionReviewTimeline({ [timelineEnd, timelineStart, segmentDuration], ); - const { alignStartDateToTimeline, alignEndDateToTimeline } = - useTimelineUtils(segmentDuration); + const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( + { + segmentDuration, + timelineDuration, + }, + ); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, diff --git a/web/src/components/timeline/MotionSegment.tsx b/web/src/components/timeline/MotionSegment.tsx index 88a9a5663..21d404859 100644 --- a/web/src/components/timeline/MotionSegment.tsx +++ b/web/src/components/timeline/MotionSegment.tsx @@ -42,8 +42,9 @@ export function MotionSegment({ const { getMotionSegmentValue, interpolateMotionAudioData } = useMotionSegmentUtils(segmentDuration, motion_events); - const { alignStartDateToTimeline, alignEndDateToTimeline } = - useTimelineUtils(segmentDuration); + const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( + { segmentDuration }, + ); const { handleTouchStart } = useTapUtils(); diff --git a/web/src/components/timeline/SummaryTimeline.tsx b/web/src/components/timeline/SummaryTimeline.tsx index 8b89b88a8..29fe11680 100644 --- a/web/src/components/timeline/SummaryTimeline.tsx +++ b/web/src/components/timeline/SummaryTimeline.tsx @@ -39,18 +39,22 @@ export function SummaryTimeline({ const observer = useRef(null); - const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration); + const reviewTimelineDuration = useMemo( + () => timelineStart - timelineEnd + 4 * segmentDuration, + [timelineEnd, timelineStart, segmentDuration], + ); + + const { alignStartDateToTimeline } = useTimelineUtils({ + segmentDuration, + timelineDuration: reviewTimelineDuration, + timelineRef: reviewTimelineRef, + }); const timelineStartAligned = useMemo( () => alignStartDateToTimeline(timelineStart) + 2 * segmentDuration, [timelineStart, alignStartDateToTimeline, segmentDuration], ); - const reviewTimelineDuration = useMemo( - () => timelineStart - timelineEnd + 4 * segmentDuration, - [timelineEnd, timelineStart, segmentDuration], - ); - // Generate segments for the timeline const generateSegments = useCallback(() => { const segmentCount = reviewTimelineDuration / segmentDuration; diff --git a/web/src/hooks/use-draggable-element.ts b/web/src/hooks/use-draggable-element.ts index fd20ebf85..1a70658f9 100644 --- a/web/src/hooks/use-draggable-element.ts +++ b/web/src/hooks/use-draggable-element.ts @@ -40,8 +40,13 @@ function useDraggableElement({ }: DraggableElementProps) { const [clientYPosition, setClientYPosition] = useState(null); const [initialClickAdjustment, setInitialClickAdjustment] = useState(0); - const { alignStartDateToTimeline, getCumulativeScrollTop } = - useTimelineUtils(segmentDuration); + const { alignStartDateToTimeline, getCumulativeScrollTop } = useTimelineUtils( + { + segmentDuration: segmentDuration, + timelineDuration: timelineDuration, + timelineRef, + }, + ); const draggingAtTopEdge = useMemo(() => { if (clientYPosition && timelineRef.current) { diff --git a/web/src/hooks/use-timeline-utils.ts b/web/src/hooks/use-timeline-utils.ts index 7b211b528..c52213161 100644 --- a/web/src/hooks/use-timeline-utils.ts +++ b/web/src/hooks/use-timeline-utils.ts @@ -1,6 +1,16 @@ import { useCallback } from "react"; -export const useTimelineUtils = (segmentDuration: number) => { +export type TimelineUtilsProps = { + segmentDuration: number; + timelineDuration?: number; + timelineRef?: React.RefObject; +}; + +export function useTimelineUtils({ + segmentDuration, + timelineDuration, + timelineRef, +}: TimelineUtilsProps) { const alignEndDateToTimeline = useCallback( (time: number): number => { const remainder = time % segmentDuration; @@ -28,9 +38,27 @@ export const useTimelineUtils = (segmentDuration: number) => { return scrollTop; }, []); + const getVisibleTimelineDuration = useCallback(() => { + if (timelineRef?.current && timelineDuration) { + const { + scrollHeight: timelineHeight, + clientHeight: visibleTimelineHeight, + } = timelineRef.current; + + const segmentHeight = + timelineHeight / (timelineDuration / segmentDuration); + + const visibleTime = + (visibleTimelineHeight / segmentHeight) * segmentDuration; + + return visibleTime; + } + }, [segmentDuration, timelineDuration, timelineRef]); + return { alignEndDateToTimeline, alignStartDateToTimeline, getCumulativeScrollTop, + getVisibleTimelineDuration, }; -}; +} diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index d1a372af9..5c555f402 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -379,7 +379,17 @@ function DetectionReview({ // timeline interaction - const { alignStartDateToTimeline } = useTimelineUtils(segmentDuration); + const timelineDuration = useMemo( + () => timeRange.before - timeRange.after, + [timeRange], + ); + + const { alignStartDateToTimeline, getVisibleTimelineDuration } = + useTimelineUtils({ + segmentDuration, + timelineDuration, + timelineRef: reviewTimelineRef, + }); const scrollLock = useScrollLockout(contentRef); @@ -448,10 +458,26 @@ function DetectionReview({ return false; } - return contentRef.current.scrollHeight > contentRef.current.clientHeight; + // don't show minimap if the view is not scrollable + if (contentRef.current.scrollHeight < contentRef.current.clientHeight) { + return false; + } + + const visibleTime = getVisibleTimelineDuration(); + const minimapTime = minimapBounds.end - minimapBounds.start; + if (visibleTime && minimapTime >= visibleTime * 0.75) { + return false; + } + + return true; // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contentRef.current?.scrollHeight, severity]); + }, [contentRef.current?.scrollHeight, minimapBounds]); + + const visibleTimestamps = useMemo( + () => minimap.map((str) => parseFloat(str)), + [minimap], + ); return ( <> @@ -499,7 +525,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"}`} >