diff --git a/web/src/components/overlay/ShareTimestampDialog.tsx b/web/src/components/overlay/ShareTimestampDialog.tsx index 003796489..c92a3aed3 100644 --- a/web/src/components/overlay/ShareTimestampDialog.tsx +++ b/web/src/components/overlay/ShareTimestampDialog.tsx @@ -238,6 +238,8 @@ function CustomTimestampSelector({ return 0; } + // the picker edits a timestamp in the configured UI timezone, + // but the stored value remains a unix timestamp return (timezoneOffset - localTimeOffset) * 60; }, [timezoneOffset, localTimeOffset]); @@ -262,6 +264,7 @@ function CustomTimestampSelector({ const setFromDisplayDate = useCallback( (date: Date) => { + // convert the edited display time back into the underlying Unix timestamp setTimestamp(date.getTime() / 1000 - offsetDeltaSeconds); }, [offsetDeltaSeconds, setTimestamp], diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index ee6638ba2..b1e829037 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -247,6 +247,11 @@ 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); @@ -262,16 +267,18 @@ export default function Events() { const reviewLink = parseRecordingReviewLink(camera, timestamp, timezone); if (!reviewLink) { - navigate(location.pathname + location.hash, { + navigate(location.pathname, { state: location.state, replace: true, }); return; } - const nextRecording = { + 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, }; @@ -280,7 +287,7 @@ export default function Events() { ...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)), }); - navigate(location.pathname + location.hash, { + navigate(location.pathname, { state: { ...location.state, recording: nextRecording, diff --git a/web/src/utils/recordingReviewUrl.ts b/web/src/utils/recordingReviewUrl.ts index e409c7db9..2083331bf 100644 --- a/web/src/utils/recordingReviewUrl.ts +++ b/web/src/utils/recordingReviewUrl.ts @@ -15,9 +15,12 @@ function formatRecordingReviewTimestamp( const date = new Date(Math.floor(timestamp) * 1000); if (timezone) { + // when the UI timezone is configured, keep the URL readable by storing + // local time plus a separate timezone query param return formatInTimeZone(date, timezone, "yyyy-MM-dd'T'HH:mm:ss"); } + // without a configured UI timezone, fall back to UTC timestamp return formatInTimeZone(date, "UTC", "yyyy-MM-dd'T'HH:mm:ss'Z'"); }