From 52c617525574d25260e9d00aadfc263b03cf7e0b Mon Sep 17 00:00:00 2001 From: Dmytro Marchuk Date: Fri, 6 Nov 2026 16:45:37 +0200 Subject: [PATCH] Custom notifications suspend duration UI (#23286) * Add custom notification suspension dialog * Improve on the dialog, fix some bugs * Review changes * Make Dropdown look like Select * Review changes * Remove accidential changes * Fix file mode * Update web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix llm findings * fix another llm finding --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- web/public/locales/en/common.json | 1 + web/public/locales/en/views/settings.json | 7 + .../NotificationsSettingsExtras.tsx | 76 ++++--- web/src/components/menu/LiveContextMenu.tsx | 19 ++ .../overlay/ReviewActivityCalendar.tsx | 13 +- .../overlay/dialog/CustomSuspensionDialog.tsx | 208 ++++++++++++++++++ 6 files changed, 294 insertions(+), 30 deletions(-) create mode 100644 web/src/components/overlay/dialog/CustomSuspensionDialog.tsx diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index e5b3499a2d..85204bddec 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -21,6 +21,7 @@ "1hour": "1 hour", "12hours": "12 hours", "24hours": "24 hours", + "custom": "Custom...", "pm": "pm", "am": "am", "yr": "{{time}}yr", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index adb840d5ae..a789a87072 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1254,8 +1254,15 @@ "1hour": "Suspend for 1 hour", "12hours": "Suspend for 12 hours", "24hours": "Suspend for 24 hours", + "custom": "Suspend until...", "untilRestart": "Suspend until restart" }, + "customSuspension": { + "title": "Custom suspension time", + "description": "Suspend notifications for this camera until the selected time.", + "untilLabel": "Suspend until", + "invalidTime": "Pick a time in the future." + }, "cancelSuspension": "Cancel Suspension", "toast": { "success": { diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index 6f38611742..616d2310bd 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -23,7 +23,7 @@ import { useState, } from "react"; import { useForm } from "react-hook-form"; -import { LuCheck, LuExternalLink, LuX } from "react-icons/lu"; +import { LuCheck, LuChevronDown, LuExternalLink, LuX } from "react-icons/lu"; import { ConfigFieldMessage } from "../ConfigFieldMessage"; import { Link } from "react-router-dom"; import { toast } from "sonner"; @@ -35,12 +35,12 @@ import { useNotificationTest, } from "@/api/ws"; import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "@/components/ui/select"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { use24HourTime } from "@/hooks/use-date-utils"; import FilterSwitch from "@/components/filter/FilterSwitch"; @@ -50,6 +50,7 @@ import { useDocDomain } from "@/hooks/use-doc-domain"; import { isPWA } from "@/utils/isPWA"; import { isIOS } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import CustomSuspensionDialog from "@/components/overlay/dialog/CustomSuspensionDialog"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { cn } from "@/lib/utils"; import cloneDeep from "lodash/cloneDeep"; @@ -745,6 +746,8 @@ export function CameraNotificationSwitch({ const isSuspended = notificationSuspendUntil !== "0" || notificationState === "OFF"; + const [customDialogOpen, setCustomDialogOpen] = useState(false); + const handleSuspend = (duration: string) => { if (duration == "off") { sendNotification("OFF"); @@ -753,6 +756,10 @@ export function CameraNotificationSwitch({ } }; + const handleCustomSuspend = (totalMinutes: number) => { + sendNotificationSuspend(totalMinutes); + }; + const handleCancelSuspension = () => { sendNotification("ON"); sendNotificationSuspend(0); @@ -812,34 +819,41 @@ export function CameraNotificationSwitch({ {!isSuspended ? ( - + + + setCustomDialogOpen(true)}> + {t("notification.suspendTime.custom")} + + + ) : ( + + + { + if (!day) return; + const next = new Date(day); + const carry = + untilWallClock ?? toWallClock(new Date(), timezone); + next.setHours( + carry.getHours(), + carry.getMinutes(), + carry.getSeconds(), + 0, + ); + setUntil(fromWallClock(next, timezone)); + setCalendarOpen(false); + }} + /> + + + { + const [h, m] = e.target.value.split(":"); + const hh = Number.parseInt(h ?? "", 10); + const mm = Number.parseInt(m ?? "", 10); + if (Number.isNaN(hh) || Number.isNaN(mm)) return; + const next = new Date( + untilWallClock ?? toWallClock(new Date(), timezone), + ); + next.setHours(hh, mm, 0, 0); + setUntil(fromWallClock(next, timezone)); + }} + /> + + {!isFuture && ( +

+ {t("notification.customSuspension.invalidTime")} +

+ )} + + + + + + + + + ); +}