diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index ce80436b6..5d36ca1ba 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -29,6 +29,8 @@ import { } from "@/types/live"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { PlayerStats } from "@/components/player/PlayerStats"; +import { MdCircle } from "react-icons/md"; +import { useCameraActivity } from "@/hooks/use-camera-activity"; import { Skeleton } from "@/components/ui/skeleton"; import { isEqual } from "lodash"; @@ -877,6 +879,7 @@ export default function DraggableGridLayout({ [camera.name]: loading, })) } + showMotionDot={false} onClick={() => { !isEditMode && onSelectCamera(camera.name); }} @@ -908,7 +911,10 @@ export default function DraggableGridLayout({ minimal={true} /> )} - + {isEditMode && showCircles && } @@ -1075,6 +1081,24 @@ const BirdseyeLivePlayerGridItem = React.forwardRef< }, ); +// Separate component so it can call useCameraActivity as a hook (no hooks in loops). +// Direct WS subscription guarantees the dot reacts to motion changes in real-time +// without relying on an intermediate callback → parent-state chain. +function CameraMotionDot({ + camera, + autoLive, +}: { + camera: CameraConfig; + autoLive?: boolean; +}) { + const { activeMotion, offline } = useCameraActivity(camera); + if (autoLive === false || offline || !activeMotion) return null; + return ( +
+ +
+ ); +} type GridLiveContextMenuProps = { className?: string;