mirror of
https://github.com/alexhopeoconnor/frigate.git
synced 2026-10-22 11:17:50 +10:00
Feature: Share Timestamped URL for Camera Footage History (#22537)
* Initial copy timestamp url implementation * revise url format * Implement share timestamp dialog * Use translations * Add comments * Add validations to shared link * Switch to searchEffect implementation * Add missing accessibility related dialog description * Change URL format to unix timestamps * Remove unnecessary useEffect * Remove duplicated dialog title * Fixes/improvements based off PR review comments * Add missing cancel button & separators to dialog * Make share description clearer * Bugfix: guard against showing toasts twice Because this effect ends up running multiple times * Clamp future timestamps to now * Revert "Bugfix: guard against showing toasts twice" This reverts commit 99fa5e1deebdc3a91a4b9015f5a46a36545bf349. * Use normal separator Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> * Fixes based off PR review comments * Bugfix: Share dialog was not receiving the player timestamp after removing key that triggered remounts * Defer `setRecording` and return true from hook for cleanup * Remove timeout defer hack in favor of refactored hook * Attempt to replay video muted on NotAllowedError * Use separate persistent mute and temporary forced mute states * Align cancel button with other dialogs * Prevent wrapping on dialog title * Remove extra "back" button on mobile drawer * Fix back navigation when coming from direct shared timestamp links * Use new timeformat hook * Simplify dialog radio buttons * Apply suggestions from code review Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,10 @@ import {
|
||||
getBeginningOfDayTimestamp,
|
||||
getEndOfDayTimestamp,
|
||||
} from "@/utils/dateUtil";
|
||||
import {
|
||||
parseRecordingReviewLink,
|
||||
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";
|
||||
@@ -28,6 +32,7 @@ import { useFrigateReviews } from "@/api/ws";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Events() {
|
||||
@@ -128,6 +133,14 @@ export default function Events() {
|
||||
const [notificationTab, setNotificationTab] =
|
||||
useState<TimelineType>("timeline");
|
||||
|
||||
const getReviewDayBounds = useCallback((date: Date) => {
|
||||
const now = Date.now() / 1000;
|
||||
return {
|
||||
after: getBeginningOfDayTimestamp(date),
|
||||
before: Math.min(getEndOfDayTimestamp(date), now),
|
||||
};
|
||||
}, []);
|
||||
|
||||
useSearchEffect("tab", (tab: string) => {
|
||||
if (tab === "timeline" || tab === "events" || tab === "detail") {
|
||||
setNotificationTab(tab as TimelineType);
|
||||
@@ -143,10 +156,7 @@ export default function Events() {
|
||||
const startTime = resp.data.start_time - REVIEW_PADDING;
|
||||
const date = new Date(startTime * 1000);
|
||||
|
||||
setReviewFilter({
|
||||
after: getBeginningOfDayTimestamp(date),
|
||||
before: getEndOfDayTimestamp(date),
|
||||
});
|
||||
setReviewFilter(getReviewDayBounds(date));
|
||||
setRecording(
|
||||
{
|
||||
camera: resp.data.camera,
|
||||
@@ -234,6 +244,51 @@ export default function Events() {
|
||||
[recording, setRecording, setReviewFilter],
|
||||
);
|
||||
|
||||
useSearchEffect(RECORDING_REVIEW_LINK_PARAM, (reviewLinkValue: string) => {
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reviewLink = parseRecordingReviewLink(reviewLinkValue);
|
||||
|
||||
if (!reviewLink) {
|
||||
toast.error(t("recordings.invalidSharedLink"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const validCamera =
|
||||
config.cameras[reviewLink.camera] &&
|
||||
allowedCameras.includes(reviewLink.camera);
|
||||
|
||||
if (!validCamera) {
|
||||
toast.error(t("recordings.invalidSharedCamera"), {
|
||||
position: "top-center",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
setReviewFilter({
|
||||
...reviewFilter,
|
||||
...getReviewDayBounds(new Date(reviewLink.timestamp * 1000)),
|
||||
});
|
||||
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,
|
||||
navigationSource: "shared-link",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// review paging
|
||||
|
||||
const [beforeTs, setBeforeTs] = useState(Math.ceil(Date.now() / 1000));
|
||||
|
||||
Reference in New Issue
Block a user