Add Detail stream in History view (#20525)

* new type

* activity stream panel

* use context provider for activity stream

* new activity stream panel in history view

* overlay for object tracking details in history view

* use overlay in video player

* don't refetch timeline

* fix activity stream group from being highlighted prematurely

* use annotation offset

* fix scrolling and use custom hook for interaction

* memoize to prevent unnecessary renders

* i18n and timestamp formatting

* add annotation offset slider

* bg color

* add collapsible component

* refactor

* rename activity to detail

* fix merge conflicts

* i18n

* more i18n
This commit is contained in:
Josh Hawkins
2025-10-16 07:24:14 -06:00
committed by GitHub
parent 2e7a2fd780
commit b52044aecc
16 changed files with 1818 additions and 326 deletions
+5 -45
View File
@@ -1,10 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTimelineUtils } from "./use-timeline-utils";
import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useDateLocale } from "./use-date-locale";
import { useTranslation } from "react-i18next";
import useUserInteraction from "./use-user-interaction";
type DraggableElementProps = {
contentRef: React.RefObject<HTMLElement>;
@@ -71,9 +72,9 @@ function useDraggableElement({
// track user interaction and adjust scrolling behavior
const [userInteracting, setUserInteracting] = useState(false);
const interactionTimeout = useRef<NodeJS.Timeout>();
const isProgrammaticScroll = useRef(false);
const { userInteracting } = useUserInteraction({
elementRef: timelineRef,
});
const draggingAtTopEdge = useMemo(() => {
if (clientYPosition && timelineRef.current && scrollEdgeSize) {
@@ -507,47 +508,6 @@ function useDraggableElement({
}
}, [timelineRef, segmentsRef, segments]);
useEffect(() => {
const handleUserInteraction = () => {
if (!isProgrammaticScroll.current) {
setUserInteracting(true);
if (interactionTimeout.current) {
clearTimeout(interactionTimeout.current);
}
interactionTimeout.current = setTimeout(() => {
setUserInteracting(false);
}, 3000);
} else {
isProgrammaticScroll.current = false;
}
};
const timelineElement = timelineRef.current;
if (timelineElement) {
timelineElement.addEventListener("scroll", handleUserInteraction);
timelineElement.addEventListener("mousedown", handleUserInteraction);
timelineElement.addEventListener("mouseup", handleUserInteraction);
timelineElement.addEventListener("touchstart", handleUserInteraction);
timelineElement.addEventListener("touchmove", handleUserInteraction);
timelineElement.addEventListener("touchend", handleUserInteraction);
return () => {
timelineElement.removeEventListener("scroll", handleUserInteraction);
timelineElement.removeEventListener("mousedown", handleUserInteraction);
timelineElement.removeEventListener("mouseup", handleUserInteraction);
timelineElement.removeEventListener(
"touchstart",
handleUserInteraction,
);
timelineElement.removeEventListener("touchmove", handleUserInteraction);
timelineElement.removeEventListener("touchend", handleUserInteraction);
};
}
}, [timelineRef]);
return { handleMouseDown, handleMouseUp, handleMouseMove };
}
+57
View File
@@ -0,0 +1,57 @@
import { useCallback, useEffect, useRef, useState } from "react";
type UseUserInteractionProps = {
elementRef: React.RefObject<HTMLElement>;
};
function useUserInteraction({ elementRef }: UseUserInteractionProps) {
const [userInteracting, setUserInteracting] = useState(false);
const interactionTimeout = useRef<NodeJS.Timeout>();
const isProgrammaticScroll = useRef(false);
const setProgrammaticScroll = useCallback(() => {
isProgrammaticScroll.current = true;
}, []);
useEffect(() => {
const handleUserInteraction = () => {
if (!isProgrammaticScroll.current) {
setUserInteracting(true);
if (interactionTimeout.current) {
clearTimeout(interactionTimeout.current);
}
interactionTimeout.current = setTimeout(() => {
setUserInteracting(false);
}, 3000);
} else {
isProgrammaticScroll.current = false;
}
};
const element = elementRef.current;
if (element) {
element.addEventListener("scroll", handleUserInteraction);
element.addEventListener("mousedown", handleUserInteraction);
element.addEventListener("mouseup", handleUserInteraction);
element.addEventListener("touchstart", handleUserInteraction);
element.addEventListener("touchmove", handleUserInteraction);
element.addEventListener("touchend", handleUserInteraction);
return () => {
element.removeEventListener("scroll", handleUserInteraction);
element.removeEventListener("mousedown", handleUserInteraction);
element.removeEventListener("mouseup", handleUserInteraction);
element.removeEventListener("touchstart", handleUserInteraction);
element.removeEventListener("touchmove", handleUserInteraction);
element.removeEventListener("touchend", handleUserInteraction);
};
}
}, [elementRef]);
return { userInteracting, setProgrammaticScroll };
}
export default useUserInteraction;