Rework scrolling to fix nested scrolling

This commit is contained in:
Nicolas Mowen 2024-02-21 11:30:59 -07:00
parent 6b0701c73c
commit 16d81de5e1
8 changed files with 93 additions and 37 deletions

View File

@ -28,11 +28,11 @@ function App() {
<BrowserRouter> <BrowserRouter>
<Wrapper> <Wrapper>
<Header onToggleNavbar={toggleNavbar} /> <Header onToggleNavbar={toggleNavbar} />
<div className="grid grid-cols-[auto,1fr] flex-grow-1 overflow-auto"> <div className="w-full h-full pt-2 overflow-hidden">
<Sidebar sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} /> <Sidebar sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} />
<div <div
id="pageRoot" id="pageRoot"
className="overflow-x-hidden px-4 py-2 w-screen md:w-full" className="absolute left-0 md:left-16 top-16 md:top-0 right-0 bottom-0 overflow-hidden"
> >
<Routes> <Routes>
<Route path="/" element={<Live />} /> <Route path="/" element={<Live />} />

View File

@ -5,7 +5,7 @@ type TWrapperProps = {
}; };
const Wrapper = ({ children }: TWrapperProps) => { const Wrapper = ({ children }: TWrapperProps) => {
return <main className="flex flex-col h-screen">{children}</main>; return <main className="w-screen h-screen overflow-hidden">{children}</main>;
}; };
export default Wrapper; export default Wrapper;

View File

@ -1,5 +1,11 @@
import VideoPlayer from "./VideoPlayer"; import VideoPlayer from "./VideoPlayer";
import React, { useCallback, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import Player from "video.js/dist/types/player"; import Player from "video.js/dist/types/player";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
@ -37,14 +43,34 @@ export default function PreviewThumbnailPlayer({
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>(); const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout | null>();
const [hover, setHover] = useState(false); const [playback, setPlayback] = useState(false);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const playingBack = useMemo( const playingBack = useMemo(
() => relevantPreview && (hover || autoPlayback), () => relevantPreview && playback,
[hover, autoPlayback, relevantPreview] [playback, autoPlayback, relevantPreview]
); );
useEffect(() => {
if (!autoPlayback) {
setPlayback(false);
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
return;
}
const timeout = setTimeout(() => {
setPlayback(true);
setHoverTimeout(null);
}, 500);
return () => {
clearTimeout(timeout);
};
}, [autoPlayback]);
const onPlayback = useCallback( const onPlayback = useCallback(
(isHovered: Boolean) => { (isHovered: Boolean) => {
if (!relevantPreview) { if (!relevantPreview) {
@ -54,7 +80,7 @@ export default function PreviewThumbnailPlayer({
if (isHovered) { if (isHovered) {
setHoverTimeout( setHoverTimeout(
setTimeout(() => { setTimeout(() => {
setHover(true); setPlayback(true);
setHoverTimeout(null); setHoverTimeout(null);
}, 500) }, 500)
); );
@ -63,7 +89,7 @@ export default function PreviewThumbnailPlayer({
clearTimeout(hoverTimeout); clearTimeout(hoverTimeout);
} }
setHover(false); setPlayback(false);
setProgress(0); setProgress(0);
if (playerRef.current) { if (playerRef.current) {
@ -203,7 +229,7 @@ function PreviewContent({
(player.currentTime() || 0) - playerStartTime; (player.currentTime() || 0) - playerStartTime;
// end with a bit of padding // end with a bit of padding
const playerDuration = (review.end_time - review.start_time) + 8; const playerDuration = review.end_time - review.start_time + 8;
const playerPercent = (playerProgress / playerDuration) * 100; const playerPercent = (playerProgress / playerDuration) * 100;
if ( if (

View File

@ -47,7 +47,7 @@ export function EventReviewTimeline({
const currentTimeRef = useRef<HTMLDivElement>(null); const currentTimeRef = useRef<HTMLDivElement>(null);
const observer = useRef<ResizeObserver | null>(null); const observer = useRef<ResizeObserver | null>(null);
const timelineDuration = useMemo( const timelineDuration = useMemo(
() => timelineEnd - timelineStart, () => timelineStart - timelineEnd,
[timelineEnd, timelineStart] [timelineEnd, timelineStart]
); );

View File

@ -6,9 +6,12 @@ export const useSegmentUtils = (
events: ReviewSegment[], events: ReviewSegment[],
severityType: string severityType: string
) => { ) => {
const getSegmentStart = useCallback((time: number): number => { const getSegmentStart = useCallback(
return Math.floor(time / (segmentDuration)) * (segmentDuration); (time: number): number => {
}, [segmentDuration]); return Math.floor(time / segmentDuration) * segmentDuration;
},
[segmentDuration]
);
const getSegmentEnd = useCallback( const getSegmentEnd = useCallback(
(time: number | undefined): number => { (time: number | undefined): number => {
@ -67,7 +70,8 @@ export const useSegmentUtils = (
[events, getSegmentStart, getSegmentEnd, mapSeverityToNumber] [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber]
); );
const getReviewed = useCallback((time: number): boolean => { const getReviewed = useCallback(
(time: number): boolean => {
return events.some((event) => { return events.some((event) => {
const segmentStart = getSegmentStart(event.start_time); const segmentStart = getSegmentStart(event.start_time);
const segmentEnd = getSegmentEnd(event.end_time); const segmentEnd = getSegmentEnd(event.end_time);
@ -75,7 +79,9 @@ export const useSegmentUtils = (
time >= segmentStart && time < segmentEnd && event.has_been_reviewed time >= segmentStart && time < segmentEnd && event.has_been_reviewed
); );
}); });
}, [events, getSegmentStart, getSegmentEnd]); },
[events, getSegmentStart, getSegmentEnd]
);
const shouldShowRoundedCorners = useCallback( const shouldShowRoundedCorners = useCallback(
(segmentTime: number): { roundTop: boolean; roundBottom: boolean } => { (segmentTime: number): { roundTop: boolean; roundBottom: boolean } => {
@ -150,5 +156,12 @@ export const useSegmentUtils = (
[events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType]
); );
return { getSegmentStart, getSegmentEnd, getSeverity, displaySeverityType, getReviewed, shouldShowRoundedCorners }; return {
getSegmentStart,
getSegmentEnd,
getSeverity,
displaySeverityType,
getReviewed,
shouldShowRoundedCorners,
};
}; };

View File

@ -79,7 +79,7 @@ function Live() {
}, []); }, []);
return ( return (
<> <div className="w-full h-full overflow-scroll">
{events && events.length > 0 && ( {events && events.length > 0 && (
<ScrollArea> <ScrollArea>
<TooltipProvider> <TooltipProvider>
@ -115,7 +115,7 @@ function Live() {
); );
})} })}
</div> </div>
</> </div>
); );
} }

View File

@ -244,6 +244,8 @@ export default function DesktopEventView() {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
console.log("end of the timeline is " + after + " vs " + (Math.floor(Date.now() / 1000) + 2 * 60 * 60))
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<div className="absolute flex justify-between left-0 top-0 right-0"> <div className="absolute flex justify-between left-0 top-0 right-0">
@ -329,23 +331,40 @@ export default function DesktopEventView() {
})} })}
</div> </div>
<div className="absolute top-12 right-0 bottom-0"> <div className="absolute top-12 right-0 bottom-0">
<EventReviewTimeline {after != 0 && (<EventReviewTimeline
segmentDuration={60} segmentDuration={60}
timestampSpread={15} timestampSpread={15}
timelineStart={Math.floor(Date.now() / 1000)} timelineStart={Math.floor(Date.now() / 1000)} // start of the timeline - all times are numeric, not Date objects
timelineDuration={24 * 60 * 60} timelineEnd={after} // end of timeline - timestamp
showMinimap showMinimap
minimapStartTime={minimapBounds.start} minimapStartTime={minimapBounds.start}
minimapEndTime={minimapBounds.end} minimapEndTime={minimapBounds.end}
events={reviewItems.all} events={reviewItems.all}
severityType={severity} severityType={severity}
contentRef={contentRef} contentRef={contentRef}
/> />)}
</div> </div>
</div> </div>
); );
} }
/**
* <EventReviewTimeline
segmentDuration={60} // seconds per segment
timestampSpread={15} // minutes between each major timestamp
timelineStart={Math.floor(Date.now() / 1000)} // start of the timeline - all times are numeric, not Date objects
timelineEnd={Math.floor(Date.now() / 1000) + 2 * 60 * 60} // end of timeline - timestamp
showHandlebar // show / hide the handlebar
handlebarTime={Math.floor(Date.now() / 1000) - 27 * 60} // set the time of the handlebar
showMinimap // show / hide the minimap
minimapStartTime={Math.floor(Date.now() / 1000) - 35 * 60} // start time of the minimap - the earlier time (eg 1:00pm)
minimapEndTime={Math.floor(Date.now() / 1000) - 21 * 60} // end of the minimap - the later time (eg 3:00pm)
events={mockEvents} // events, including new has_been_reviewed and severity properties
severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
/>
*/
function ReviewCalendarButton() { function ReviewCalendarButton() {
const disabledDates = useMemo(() => { const disabledDates = useMemo(() => {
const tomorrow = new Date(); const tomorrow = new Date();

View File

@ -139,7 +139,6 @@ export default function MobileEventView() {
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
const start = (entry.target as HTMLElement).dataset.start; const start = (entry.target as HTMLElement).dataset.start;
console.log(`${start} has been updated as intersect ${entry.isIntersecting}`)
if (!start) { if (!start) {
return; return;
@ -153,7 +152,8 @@ export default function MobileEventView() {
setMinimap([...visibleTimestamps]); setMinimap([...visibleTimestamps]);
}); });
}, { threshold: 0.5 } },
{ threshold: 0.5 }
); );
return () => { return () => {
@ -186,8 +186,6 @@ export default function MobileEventView() {
data.start = parseFloat(list[0]); data.start = parseFloat(list[0]);
} }
console.log("the new times are " + JSON.stringify(data))
return data; return data;
}, [minimap]); }, [minimap]);
@ -237,7 +235,7 @@ export default function MobileEventView() {
} }
return ( return (
<div className="w-full h-full"> <>
<ToggleGroup <ToggleGroup
type="single" type="single"
defaultValue="alert" defaultValue="alert"
@ -278,7 +276,7 @@ export default function MobileEventView() {
<div <div
ref={contentRef} ref={contentRef}
className="grid grid-cols-1 sm:grid-cols-2 mt-2 gap-2 overflow-y-auto no-scrollbar" className="w-full h-full grid grid-cols-1 sm:grid-cols-2 mt-2 gap-2 overflow-y-auto"
> >
{reviewItems[severity]?.map((value, segIdx) => { {reviewItems[severity]?.map((value, segIdx) => {
const lastRow = segIdx == reviewItems[severity].length - 1; const lastRow = segIdx == reviewItems[severity].length - 1;
@ -308,6 +306,6 @@ export default function MobileEventView() {
); );
})} })}
</div> </div>
</div> </>
); );
} }