From 1c987581ffc1d3013b6e61da1cb00418df88d183 Mon Sep 17 00:00:00 2001 From: 0x464e <36742501+0x464e@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:54:54 +0200 Subject: [PATCH] Switch to searchEffect implementation --- web/src/pages/Events.tsx | 95 ++++++----------------------- web/src/utils/recordingReviewUrl.ts | 21 ++++--- 2 files changed, 32 insertions(+), 84 deletions(-) diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index 7d4ef42cc..e2c2a1db1 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -23,24 +23,19 @@ import { } from "@/utils/dateUtil"; import { parseRecordingReviewLink, - RECORDING_REVIEW_START_PARAM, - RECORDING_REVIEW_TIMEZONE_PARAM, + RECORDING_REVIEW_LINK_PARAM, } from "@/utils/recordingReviewUrl"; import EventView from "@/views/events/EventView"; import MotionSearchView from "@/views/motion-search/MotionSearchView"; import { RecordingView } from "@/views/recording/RecordingView"; 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 { useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import useSWR from "swr"; export default function Events() { const { t } = useTranslation(["views/events"]); - const location = useLocation(); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const { data: config } = useSWR("config", { revalidateOnFocus: false, @@ -75,7 +70,6 @@ export default function Events() { const [motionSearchDay, setMotionSearchDay] = useState( undefined, ); - const handledReviewLinkRef = useRef(null); const motionSearchCameras = useMemo(() => { if (!config?.cameras) { @@ -249,50 +243,20 @@ export default function Events() { [recording, setRecording, setReviewFilter], ); - // shared recording links enter /review through query params, but the - // 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; - } - + useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => { if (!config) { - return; + return false; } - const camera = location.hash - ? 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); + const reviewLink = parseRecordingReviewLink(reviewLinkValue); if (!reviewLink) { toast.error(t("recordings.invalidSharedLink"), { position: "top-center", }); - navigate(location.pathname, { - state: location.state, - replace: true, - }); - return; + return true; } - // reject unknown or unauthorized cameras before switching into - // recording view so bad links cleanly fall back to plain /review const validCamera = config.cameras[reviewLink.camera] && allowedCameras.includes(reviewLink.camera); @@ -301,46 +265,27 @@ export default function Events() { toast.error(t("recordings.invalidSharedCamera"), { position: "top-center", }); - navigate(location.pathname, { - state: location.state, - replace: true, - }); - return; + return true; } - 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({ ...reviewFilter, ...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)), }); - - navigate(location.pathname, { - state: { - ...location.state, - recording: nextRecording, + setRecording( + { + 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", + timelineType: notificationTab, }, - replace: true, - }); - }, [ - location.hash, - location.pathname, - location.state, - config, - navigate, - allowedCameras, - getReviewDayBounds, - reviewFilter, - searchParams, - setReviewFilter, - t, - ]); + true, + ); + + return false; + }); // review paging diff --git a/web/src/utils/recordingReviewUrl.ts b/web/src/utils/recordingReviewUrl.ts index 2083331bf..4cfc79014 100644 --- a/web/src/utils/recordingReviewUrl.ts +++ b/web/src/utils/recordingReviewUrl.ts @@ -1,7 +1,6 @@ import { formatInTimeZone, fromZonedTime } from "date-fns-tz"; -export const RECORDING_REVIEW_START_PARAM = "start_time"; -export const RECORDING_REVIEW_TIMEZONE_PARAM = "timezone"; +export const RECORDING_REVIEW_LINK_PARAM = "timestamp"; export type RecordingReviewLinkState = { camera: string; @@ -25,10 +24,14 @@ function formatRecordingReviewTimestamp( } export function parseRecordingReviewLink( - camera: string | null, - start: string | null, - timezone: string | null, + value: string | null, ): RecordingReviewLinkState | undefined { + if (!value) { + return undefined; + } + + const [camera, start, timezone] = value.split("|"); + if (!camera || !start) { return undefined; } @@ -61,9 +64,9 @@ export function createRecordingReviewUrl( const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`; - const timezoneParam = timezone - ? `&${RECORDING_REVIEW_TIMEZONE_PARAM}=${encodeURIComponent(timezone)}` - : ""; + const reviewLink = 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}`; }