mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-02-25 18:55:25 -06:00
Rewrite websocket to use tracked state instead of context (#10091)
* Rewrite websocket to use tracked state instead of context * Cleanup * Use component for updating items * Fix scroll update * Don't save vite
This commit is contained in:
parent
f95ce913b1
commit
21defbea9d
48
web/package-lock.json
generated
48
web/package-lock.json
generated
@ -46,6 +46,7 @@
|
|||||||
"react-hook-form": "^7.48.2",
|
"react-hook-form": "^7.48.2",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
|
"react-tracked": "^1.7.11",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use-websocket": "^4.5.0",
|
"react-use-websocket": "^4.5.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
@ -6498,6 +6499,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-compare": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg=="
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@ -6729,6 +6735,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-tracked": {
|
||||||
|
"version": "1.7.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.11.tgz",
|
||||||
|
"integrity": "sha512-+XXv4dJH7NnLtSD/cPVL9omra4A3KRK91L33owevXZ81r7qF/a9DdCsVZa90jMGht/V1Ym9sasbmidsJykhULQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"proxy-compare": "2.4.0",
|
||||||
|
"use-context-selector": "1.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"scheduler": ">=0.19.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
@ -7918,6 +7947,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-context-selector": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"scheduler": ">=0.19.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sidecar": {
|
"node_modules/use-sidecar": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"react-hook-form": "^7.48.2",
|
"react-hook-form": "^7.48.2",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
|
"react-tracked": "^1.7.11",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use-websocket": "^4.5.0",
|
"react-use-websocket": "^4.5.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { baseUrl } from "./baseUrl";
|
import { baseUrl } from "./baseUrl";
|
||||||
import useSWR, { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
import { WsProvider } from "./ws";
|
import { WsProvider } from "./ws";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
|
||||||
|
|
||||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||||
|
|
||||||
@ -38,9 +37,7 @@ type WsWithConfigType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function WsWithConfig({ children }: WsWithConfigType) {
|
function WsWithConfig({ children }: WsWithConfigType) {
|
||||||
const { data } = useSWR<FrigateConfig>("config");
|
return <WsProvider>{children}</WsProvider>;
|
||||||
|
|
||||||
return data ? <WsProvider config={data}>{children}</WsProvider> : children;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApiHost() {
|
export function useApiHost() {
|
||||||
|
@ -1,149 +1,104 @@
|
|||||||
import { baseUrl } from "./baseUrl";
|
import { baseUrl } from "./baseUrl";
|
||||||
import {
|
import { useCallback, useEffect, useState } from "react";
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useReducer,
|
|
||||||
} from "react";
|
|
||||||
import { produce, Draft } from "immer";
|
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { FrigateEvent, FrigateReview, ToggleableSetting } from "@/types/ws";
|
import { FrigateEvent, FrigateReview, ToggleableSetting } from "@/types/ws";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { createContainer } from "react-tracked";
|
||||||
|
|
||||||
type ReducerState = {
|
type Update = {
|
||||||
[topic: string]: {
|
|
||||||
lastUpdate: number;
|
|
||||||
payload: any;
|
|
||||||
retain: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type ReducerAction = {
|
|
||||||
topic: string;
|
topic: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
retain: boolean;
|
retain: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: ReducerState = {
|
type WsState = {
|
||||||
_initial_state: {
|
[topic: string]: any;
|
||||||
lastUpdate: 0,
|
|
||||||
payload: "",
|
|
||||||
retain: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type WebSocketContextProps = {
|
type useValueReturn = [WsState, (update: Update) => void];
|
||||||
state: ReducerState;
|
|
||||||
readyState: ReadyState;
|
|
||||||
sendJsonMessage: (message: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WS = createContext<WebSocketContextProps>({
|
function useValue(): useValueReturn {
|
||||||
state: initialState,
|
// basic config
|
||||||
readyState: ReadyState.CLOSED,
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
sendJsonMessage: () => {},
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
|
||||||
|
|
||||||
export const useWebSocketContext = (): WebSocketContextProps => {
|
// main state
|
||||||
const context = useContext(WS);
|
const [wsState, setWsState] = useState<WsState>({});
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"useWebSocketContext must be used within a WebSocketProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
function reducer(state: ReducerState, action: ReducerAction): ReducerState {
|
useEffect(() => {
|
||||||
switch (action.topic) {
|
if (!config) {
|
||||||
default:
|
return;
|
||||||
return produce(state, (draftState: Draft<ReducerState>) => {
|
}
|
||||||
let parsedPayload = action.payload;
|
|
||||||
try {
|
|
||||||
parsedPayload = action.payload && JSON.parse(action.payload);
|
|
||||||
} catch (e) {}
|
|
||||||
draftState[action.topic] = {
|
|
||||||
lastUpdate: Date.now(),
|
|
||||||
payload: parsedPayload,
|
|
||||||
retain: action.retain,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type WsProviderType = {
|
const cameraStates: WsState = {};
|
||||||
config: FrigateConfig;
|
|
||||||
children: ReactNode;
|
|
||||||
wsUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function WsProvider({
|
Object.keys(config.cameras).forEach((camera) => {
|
||||||
config,
|
const { name, record, detect, snapshots, audio } = config.cameras[camera];
|
||||||
children,
|
cameraStates[`${name}/recordings/state`] = record.enabled ? "ON" : "OFF";
|
||||||
wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`,
|
cameraStates[`${name}/detect/state`] = detect.enabled ? "ON" : "OFF";
|
||||||
}: WsProviderType) {
|
cameraStates[`${name}/snapshots/state`] = snapshots.enabled
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/audio/state`] = audio.enabled ? "ON" : "OFF";
|
||||||
|
});
|
||||||
|
|
||||||
|
setWsState({ ...wsState, ...cameraStates });
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
// ws handler
|
||||||
const { sendJsonMessage, readyState } = useWebSocket(wsUrl, {
|
const { sendJsonMessage, readyState } = useWebSocket(wsUrl, {
|
||||||
onMessage: (event) => {
|
onMessage: (event) => {
|
||||||
dispatch(JSON.parse(event.data));
|
const data: Update = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setWsState({ ...wsState, [data.topic]: data.payload });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onOpen: () => dispatch({ topic: "", payload: "", retain: false }),
|
onOpen: () => {},
|
||||||
shouldReconnect: () => true,
|
shouldReconnect: () => true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const setState = useCallback(
|
||||||
Object.keys(config.cameras).forEach((camera) => {
|
(message: Update) => {
|
||||||
const { name, record, detect, snapshots, audio } = config.cameras[camera];
|
|
||||||
dispatch({
|
|
||||||
topic: `${name}/recordings/state`,
|
|
||||||
payload: record.enabled ? "ON" : "OFF",
|
|
||||||
retain: false,
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
topic: `${name}/detect/state`,
|
|
||||||
payload: detect.enabled ? "ON" : "OFF",
|
|
||||||
retain: false,
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
topic: `${name}/snapshots/state`,
|
|
||||||
payload: snapshots.enabled ? "ON" : "OFF",
|
|
||||||
retain: false,
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
topic: `${name}/audio/state`,
|
|
||||||
payload: audio.enabled ? "ON" : "OFF",
|
|
||||||
retain: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WS.Provider value={{ state, readyState, sendJsonMessage }}>
|
|
||||||
{children}
|
|
||||||
</WS.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWs(watchTopic: string, publishTopic: string) {
|
|
||||||
const { state, readyState, sendJsonMessage } = useWebSocketContext();
|
|
||||||
|
|
||||||
const value = state[watchTopic] || { payload: null };
|
|
||||||
|
|
||||||
const send = useCallback(
|
|
||||||
(payload: any, retain = false) => {
|
|
||||||
if (readyState === ReadyState.OPEN) {
|
if (readyState === ReadyState.OPEN) {
|
||||||
sendJsonMessage({
|
sendJsonMessage({
|
||||||
topic: publishTopic || watchTopic,
|
topic: message.topic,
|
||||||
payload,
|
payload: message.payload,
|
||||||
retain,
|
retain: message.retain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sendJsonMessage, readyState, watchTopic, publishTopic]
|
[readyState, sendJsonMessage]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [wsState, setState];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const {
|
||||||
|
Provider: WsProvider,
|
||||||
|
useTrackedState: useWsState,
|
||||||
|
useUpdate: useWsUpdate,
|
||||||
|
} = createContainer(useValue, { defaultState: {}, concurrentMode: true });
|
||||||
|
|
||||||
|
export function useWs(watchTopic: string, publishTopic: string) {
|
||||||
|
const state = useWsState();
|
||||||
|
const sendJsonMessage = useWsUpdate();
|
||||||
|
|
||||||
|
const value = { payload: state[watchTopic] || null };
|
||||||
|
|
||||||
|
const send = useCallback(
|
||||||
|
(payload: any, retain = false) => {
|
||||||
|
sendJsonMessage({
|
||||||
|
topic: publishTopic || watchTopic,
|
||||||
|
payload,
|
||||||
|
retain,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[sendJsonMessage, watchTopic, publishTopic]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { value, send };
|
return { value, send };
|
||||||
@ -219,21 +174,21 @@ export function useFrigateEvents(): { payload: FrigateEvent } {
|
|||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("events", "");
|
} = useWs("events", "");
|
||||||
return { payload };
|
return { payload: JSON.parse(payload) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFrigateReviews(): { payload: FrigateReview } {
|
export function useFrigateReviews(): { payload: FrigateReview } {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("reviews", "");
|
} = useWs("reviews", "");
|
||||||
return { payload };
|
return { payload: JSON.parse(payload) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFrigateStats(): { payload: FrigateStats } {
|
export function useFrigateStats(): { payload: FrigateStats } {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("stats", "");
|
} = useWs("stats", "");
|
||||||
return { payload };
|
return { payload: JSON.parse(payload) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMotionActivity(camera: string): { payload: string } {
|
export function useMotionActivity(camera: string): { payload: string } {
|
||||||
|
67
web/src/components/dynamic/NewReviewData.tsx
Normal file
67
web/src/components/dynamic/NewReviewData.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useFrigateReviews } from "@/api/ws";
|
||||||
|
import { ReviewSeverity } from "@/types/review";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { LuRefreshCcw } from "react-icons/lu";
|
||||||
|
import { MutableRefObject, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type NewReviewDataProps = {
|
||||||
|
className: string;
|
||||||
|
contentRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
severity: ReviewSeverity;
|
||||||
|
pullLatestData: () => void;
|
||||||
|
};
|
||||||
|
export default function NewReviewData({
|
||||||
|
className,
|
||||||
|
contentRef,
|
||||||
|
severity,
|
||||||
|
pullLatestData,
|
||||||
|
}: NewReviewDataProps) {
|
||||||
|
const { payload: review } = useFrigateReviews();
|
||||||
|
|
||||||
|
const [reviewId, setReviewId] = useState("");
|
||||||
|
const [hasUpdate, setHasUpdate] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!review) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (review.type == "end" && review.review.severity == severity) {
|
||||||
|
setReviewId(review.review.id);
|
||||||
|
}
|
||||||
|
}, [review]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reviewId != "") {
|
||||||
|
setHasUpdate(true);
|
||||||
|
}
|
||||||
|
}, [reviewId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="flex justify-center items-center mr-[100px]">
|
||||||
|
<Button
|
||||||
|
className={`${
|
||||||
|
hasUpdate
|
||||||
|
? "animate-in slide-in-from-top duration-500"
|
||||||
|
: "invisible"
|
||||||
|
} text-center mt-5 mx-auto bg-gray-400 text-white`}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setHasUpdate(false);
|
||||||
|
pullLatestData();
|
||||||
|
if (contentRef.current) {
|
||||||
|
contentRef.current.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuRefreshCcw className="w-4 h-4 mr-2" />
|
||||||
|
New Items To Review
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { useFrigateReviews } from "@/api/ws";
|
|
||||||
import useApiFilter from "@/hooks/use-api-filter";
|
import useApiFilter from "@/hooks/use-api-filter";
|
||||||
import useOverlayState from "@/hooks/use-overlay-state";
|
import useOverlayState from "@/hooks/use-overlay-state";
|
||||||
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
@ -6,7 +5,7 @@ import DesktopEventView from "@/views/events/DesktopEventView";
|
|||||||
import DesktopRecordingView from "@/views/events/DesktopRecordingView";
|
import DesktopRecordingView from "@/views/events/DesktopRecordingView";
|
||||||
import MobileEventView from "@/views/events/MobileEventView";
|
import MobileEventView from "@/views/events/MobileEventView";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
@ -102,7 +101,7 @@ export default function Events() {
|
|||||||
const reloadData = useCallback(() => {
|
const reloadData = useCallback(() => {
|
||||||
setSize(1);
|
setSize(1);
|
||||||
updateSegments();
|
updateSegments();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// preview videos
|
// preview videos
|
||||||
|
|
||||||
@ -201,24 +200,6 @@ export default function Events() {
|
|||||||
};
|
};
|
||||||
}, [selectedReviewId, reviewPages]);
|
}, [selectedReviewId, reviewPages]);
|
||||||
|
|
||||||
// review updates
|
|
||||||
|
|
||||||
const { payload: reviewUpdate } = useFrigateReviews();
|
|
||||||
const [hasUpdate, setHasUpdate] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!reviewUpdate || hasUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
reviewUpdate.type == "end" &&
|
|
||||||
reviewUpdate.review.severity == severity
|
|
||||||
) {
|
|
||||||
setHasUpdate(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, [reviewUpdate]);
|
|
||||||
|
|
||||||
if (selectedData) {
|
if (selectedData) {
|
||||||
return (
|
return (
|
||||||
<DesktopRecordingView
|
<DesktopRecordingView
|
||||||
@ -236,9 +217,7 @@ export default function Events() {
|
|||||||
reachedEnd={isDone}
|
reachedEnd={isDone}
|
||||||
isValidating={isValidating}
|
isValidating={isValidating}
|
||||||
severity={severity}
|
severity={severity}
|
||||||
hasUpdate={hasUpdate}
|
|
||||||
setSeverity={setSeverity}
|
setSeverity={setSeverity}
|
||||||
setHasUpdate={setHasUpdate}
|
|
||||||
loadNextPage={onLoadNextPage}
|
loadNextPage={onLoadNextPage}
|
||||||
markItemAsReviewed={markItemAsReviewed}
|
markItemAsReviewed={markItemAsReviewed}
|
||||||
pullLatestData={reloadData}
|
pullLatestData={reloadData}
|
||||||
@ -255,9 +234,7 @@ export default function Events() {
|
|||||||
isValidating={isValidating}
|
isValidating={isValidating}
|
||||||
filter={reviewFilter}
|
filter={reviewFilter}
|
||||||
severity={severity}
|
severity={severity}
|
||||||
hasUpdate={hasUpdate}
|
|
||||||
setSeverity={setSeverity}
|
setSeverity={setSeverity}
|
||||||
setHasUpdate={setHasUpdate}
|
|
||||||
loadNextPage={onLoadNextPage}
|
loadNextPage={onLoadNextPage}
|
||||||
markItemAsReviewed={markItemAsReviewed}
|
markItemAsReviewed={markItemAsReviewed}
|
||||||
onSelectReview={setSelectedReviewId}
|
onSelectReview={setSelectedReviewId}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
import NewReviewData from "@/components/dynamic/NewReviewData";
|
||||||
import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup";
|
import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup";
|
||||||
import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
|
import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
|
||||||
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { LuFolderCheck, LuRefreshCcw } from "react-icons/lu";
|
import { LuFolderCheck } from "react-icons/lu";
|
||||||
import { MdCircle } from "react-icons/md";
|
import { MdCircle } from "react-icons/md";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -19,9 +19,7 @@ type DesktopEventViewProps = {
|
|||||||
isValidating: boolean;
|
isValidating: boolean;
|
||||||
filter?: ReviewFilter;
|
filter?: ReviewFilter;
|
||||||
severity: ReviewSeverity;
|
severity: ReviewSeverity;
|
||||||
hasUpdate: boolean;
|
|
||||||
setSeverity: (severity: ReviewSeverity) => void;
|
setSeverity: (severity: ReviewSeverity) => void;
|
||||||
setHasUpdate: (hasUpdated: boolean) => void;
|
|
||||||
loadNextPage: () => void;
|
loadNextPage: () => void;
|
||||||
markItemAsReviewed: (reviewId: string) => void;
|
markItemAsReviewed: (reviewId: string) => void;
|
||||||
onSelectReview: (reviewId: string) => void;
|
onSelectReview: (reviewId: string) => void;
|
||||||
@ -36,9 +34,7 @@ export default function DesktopEventView({
|
|||||||
isValidating,
|
isValidating,
|
||||||
filter,
|
filter,
|
||||||
severity,
|
severity,
|
||||||
hasUpdate,
|
|
||||||
setSeverity,
|
setSeverity,
|
||||||
setHasUpdate,
|
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
markItemAsReviewed,
|
markItemAsReviewed,
|
||||||
onSelectReview,
|
onSelectReview,
|
||||||
@ -98,7 +94,7 @@ export default function DesktopEventView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return contentRef.current.scrollHeight > contentRef.current.clientHeight;
|
return contentRef.current.scrollHeight > contentRef.current.clientHeight;
|
||||||
}, [contentRef.current?.scrollHeight]);
|
}, [contentRef.current?.scrollHeight, severity]);
|
||||||
|
|
||||||
// review interaction
|
// review interaction
|
||||||
|
|
||||||
@ -235,33 +231,12 @@ export default function DesktopEventView({
|
|||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
className="flex flex-1 flex-wrap content-start gap-2 overflow-y-auto no-scrollbar"
|
className="flex flex-1 flex-wrap content-start gap-2 overflow-y-auto no-scrollbar"
|
||||||
>
|
>
|
||||||
{hasUpdate && (
|
<NewReviewData
|
||||||
<div className="absolute w-full z-30">
|
className="absolute w-full z-30"
|
||||||
<div className="flex justify-center items-center mr-[100px]">
|
contentRef={contentRef}
|
||||||
<Button
|
severity={severity}
|
||||||
className={`${
|
pullLatestData={pullLatestData}
|
||||||
hasUpdate
|
/>
|
||||||
? "animate-in slide-in-from-top duration-500"
|
|
||||||
: "invisible"
|
|
||||||
} text-center mt-5 mx-auto bg-gray-400 text-white`}
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setHasUpdate(false);
|
|
||||||
pullLatestData();
|
|
||||||
if (contentRef.current) {
|
|
||||||
contentRef.current.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuRefreshCcw className="w-4 h-4 mr-2" />
|
|
||||||
New Items To Review
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{reachedEnd && currentItems == null && (
|
{reachedEnd && currentItems == null && (
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center">
|
<div className="w-full h-full flex flex-col justify-center items-center">
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
|
import NewReviewData from "@/components/dynamic/NewReviewData";
|
||||||
import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
|
import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
|
||||||
import ActivityIndicator from "@/components/ui/activity-indicator";
|
import ActivityIndicator from "@/components/ui/activity-indicator";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
import { ReviewSegment, ReviewSeverity } from "@/types/review";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { LuRefreshCcw } from "react-icons/lu";
|
|
||||||
import { MdCircle } from "react-icons/md";
|
import { MdCircle } from "react-icons/md";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -15,9 +14,7 @@ type MobileEventViewProps = {
|
|||||||
reachedEnd: boolean;
|
reachedEnd: boolean;
|
||||||
isValidating: boolean;
|
isValidating: boolean;
|
||||||
severity: ReviewSeverity;
|
severity: ReviewSeverity;
|
||||||
hasUpdate: boolean;
|
|
||||||
setSeverity: (severity: ReviewSeverity) => void;
|
setSeverity: (severity: ReviewSeverity) => void;
|
||||||
setHasUpdate: (hasUpdated: boolean) => void;
|
|
||||||
loadNextPage: () => void;
|
loadNextPage: () => void;
|
||||||
markItemAsReviewed: (reviewId: string) => void;
|
markItemAsReviewed: (reviewId: string) => void;
|
||||||
pullLatestData: () => void;
|
pullLatestData: () => void;
|
||||||
@ -28,9 +25,7 @@ export default function MobileEventView({
|
|||||||
reachedEnd,
|
reachedEnd,
|
||||||
isValidating,
|
isValidating,
|
||||||
severity,
|
severity,
|
||||||
hasUpdate,
|
|
||||||
setSeverity,
|
setSeverity,
|
||||||
setHasUpdate,
|
|
||||||
loadNextPage,
|
loadNextPage,
|
||||||
markItemAsReviewed,
|
markItemAsReviewed,
|
||||||
pullLatestData,
|
pullLatestData,
|
||||||
@ -209,33 +204,12 @@ export default function MobileEventView({
|
|||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
|
|
||||||
{hasUpdate && (
|
<NewReviewData
|
||||||
<div className="absolute w-full z-30">
|
className="absolute w-full z-30"
|
||||||
<div className="flex justify-center items-center">
|
contentRef={contentRef}
|
||||||
<Button
|
severity={severity}
|
||||||
className={`${
|
pullLatestData={pullLatestData}
|
||||||
hasUpdate
|
/>
|
||||||
? "animate-in slide-in-from-top duration-500"
|
|
||||||
: "invisible"
|
|
||||||
} text-center mt-5 mx-auto bg-gray-400 text-white`}
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setHasUpdate(false);
|
|
||||||
pullLatestData();
|
|
||||||
if (contentRef.current) {
|
|
||||||
contentRef.current.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuRefreshCcw className="w-4 h-4 mr-2" />
|
|
||||||
New Items To Review
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
|
Loading…
Reference in New Issue
Block a user