Switch to searchEffect implementation

This commit is contained in:
0x464e 2026-03-19 22:54:54 +02:00
parent 878e81e05b
commit 1c987581ff
No known key found for this signature in database
GPG Key ID: E6D221DF6CBFBFFA
2 changed files with 32 additions and 84 deletions

View File

@ -23,24 +23,19 @@ import {
} from "@/utils/dateUtil"; } from "@/utils/dateUtil";
import { import {
parseRecordingReviewLink, parseRecordingReviewLink,
RECORDING_REVIEW_START_PARAM, RECORDING_REVIEW_LINK_PARAM,
RECORDING_REVIEW_TIMEZONE_PARAM,
} from "@/utils/recordingReviewUrl"; } from "@/utils/recordingReviewUrl";
import EventView from "@/views/events/EventView"; import EventView from "@/views/events/EventView";
import MotionSearchView from "@/views/motion-search/MotionSearchView"; import MotionSearchView from "@/views/motion-search/MotionSearchView";
import { RecordingView } from "@/views/recording/RecordingView"; import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
export default function Events() { export default function Events() {
const { t } = useTranslation(["views/events"]); const { t } = useTranslation(["views/events"]);
const location = useLocation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
@ -75,7 +70,6 @@ export default function Events() {
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>( const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
undefined, undefined,
); );
const handledReviewLinkRef = useRef<string | null>(null);
const motionSearchCameras = useMemo(() => { const motionSearchCameras = useMemo(() => {
if (!config?.cameras) { if (!config?.cameras) {
@ -249,50 +243,20 @@ export default function Events() {
[recording, setRecording, setReviewFilter], [recording, setRecording, setReviewFilter],
); );
// shared recording links enter /review through query params, but the useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => {
// existing recording view is opened via router state (`recording`)
// this effect translates the URL entry point into the state shape the
// rest of the page already uses, then cleans the URL back to plain /review
useEffect(() => {
const timestamp = searchParams.get(RECORDING_REVIEW_START_PARAM);
const timezone = searchParams.get(RECORDING_REVIEW_TIMEZONE_PARAM);
if (!timestamp) {
handledReviewLinkRef.current = null;
return;
}
if (!config) { if (!config) {
return; return false;
} }
const camera = location.hash const reviewLink = parseRecordingReviewLink(reviewLinkValue);
? decodeURIComponent(location.hash.substring(1))
: null;
const reviewLinkKey = `${camera ?? ""}|${timestamp}|${timezone ?? ""}`;
if (handledReviewLinkRef.current === reviewLinkKey) {
return;
}
handledReviewLinkRef.current = reviewLinkKey;
const reviewLink = parseRecordingReviewLink(camera, timestamp, timezone);
if (!reviewLink) { if (!reviewLink) {
toast.error(t("recordings.invalidSharedLink"), { toast.error(t("recordings.invalidSharedLink"), {
position: "top-center", position: "top-center",
}); });
navigate(location.pathname, { return true;
state: location.state,
replace: true,
});
return;
} }
// reject unknown or unauthorized cameras before switching into
// recording view so bad links cleanly fall back to plain /review
const validCamera = const validCamera =
config.cameras[reviewLink.camera] && config.cameras[reviewLink.camera] &&
allowedCameras.includes(reviewLink.camera); allowedCameras.includes(reviewLink.camera);
@ -301,46 +265,27 @@ export default function Events() {
toast.error(t("recordings.invalidSharedCamera"), { toast.error(t("recordings.invalidSharedCamera"), {
position: "top-center", position: "top-center",
}); });
navigate(location.pathname, { return true;
state: location.state,
replace: true,
});
return;
} }
const nextRecording: RecordingStartingPoint = {
camera: reviewLink.camera,
startTime: reviewLink.timestamp,
// severity not actually applicable here, but the type requires it
// this pattern is also used LiveCameraView to enter recording view
severity: "alert" as const,
};
setReviewFilter({ setReviewFilter({
...reviewFilter, ...reviewFilter,
...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)), ...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
}); });
setRecording(
navigate(location.pathname, { {
state: { camera: reviewLink.camera,
...location.state, startTime: reviewLink.timestamp,
recording: nextRecording, // severity not actually applicable here, but the type requires it
// this pattern is also used LiveCameraView to enter recording view
severity: "alert",
timelineType: notificationTab,
}, },
replace: true, true,
);
return false;
}); });
}, [
location.hash,
location.pathname,
location.state,
config,
navigate,
allowedCameras,
getReviewDayBounds,
reviewFilter,
searchParams,
setReviewFilter,
t,
]);
// review paging // review paging

View File

@ -1,7 +1,6 @@
import { formatInTimeZone, fromZonedTime } from "date-fns-tz"; import { formatInTimeZone, fromZonedTime } from "date-fns-tz";
export const RECORDING_REVIEW_START_PARAM = "start_time"; export const RECORDING_REVIEW_LINK_PARAM = "timestamp";
export const RECORDING_REVIEW_TIMEZONE_PARAM = "timezone";
export type RecordingReviewLinkState = { export type RecordingReviewLinkState = {
camera: string; camera: string;
@ -25,10 +24,14 @@ function formatRecordingReviewTimestamp(
} }
export function parseRecordingReviewLink( export function parseRecordingReviewLink(
camera: string | null, value: string | null,
start: string | null,
timezone: string | null,
): RecordingReviewLinkState | undefined { ): RecordingReviewLinkState | undefined {
if (!value) {
return undefined;
}
const [camera, start, timezone] = value.split("|");
if (!camera || !start) { if (!camera || !start) {
return undefined; return undefined;
} }
@ -61,9 +64,9 @@ export function createRecordingReviewUrl(
const normalizedPathname = pathname.startsWith("/") const normalizedPathname = pathname.startsWith("/")
? pathname ? pathname
: `/${pathname}`; : `/${pathname}`;
const timezoneParam = timezone const reviewLink = timezone
? `&${RECORDING_REVIEW_TIMEZONE_PARAM}=${encodeURIComponent(timezone)}` ? `${state.camera}|${formattedTimestamp}|${timezone}`
: ""; : `${state.camera}|${formattedTimestamp}`;
return `${url.origin}${normalizedPathname}?${RECORDING_REVIEW_START_PARAM}=${formattedTimestamp}${timezoneParam}#${encodeURIComponent(state.camera)}`; return `${url.origin}${normalizedPathname}?${RECORDING_REVIEW_LINK_PARAM}=${reviewLink}`;
} }