+
(
handleThumbnailClick(index)}
>
@@ -513,7 +529,7 @@ export default function ObjectLifecycle({
- >
+
);
}
diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx
index b1b32ff5d..52c8091e0 100644
--- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx
+++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx
@@ -6,13 +6,6 @@ import {
SheetHeader,
SheetTitle,
} from "../../ui/sheet";
-import {
- Drawer,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
-} from "../../ui/drawer";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
@@ -20,12 +13,12 @@ import { getIconForLabel } from "@/utils/iconUtil";
import { useApiHost } from "@/api";
import { ReviewDetailPaneType, ReviewSegment } from "@/types/review";
import { Event } from "@/types/event";
-import { useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog";
import ObjectLifecycle from "./ObjectLifecycle";
import Chip from "@/components/indicators/Chip";
-import { FaDownload, FaImages } from "react-icons/fa";
+import { FaDownload, FaImages, FaShareAlt } from "react-icons/fa";
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
import { FaArrowsRotate } from "react-icons/fa6";
import {
@@ -34,6 +27,16 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useNavigate } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import { baseUrl } from "@/api/baseUrl";
+import { shareOrCopy } from "@/utils/browserUtil";
+import {
+ MobilePage,
+ MobilePageContent,
+ MobilePageDescription,
+ MobilePageHeader,
+ MobilePageTitle,
+} from "@/components/mobile/MobilePage";
type ReviewDetailDialogProps = {
review?: ReviewSegment;
@@ -70,6 +73,7 @@ export default function ReviewDetailDialog({
config?.ui.time_format == "24hour"
? "%b %-d %Y, %H:%M"
: "%b %-d %Y, %I:%M %p",
+ config?.ui.timezone,
);
// content
@@ -77,11 +81,19 @@ export default function ReviewDetailDialog({
const [selectedEvent, setSelectedEvent] = useState
();
const [pane, setPane] = useState("overview");
- const Overlay = isDesktop ? Sheet : Drawer;
- const Content = isDesktop ? SheetContent : DrawerContent;
- const Header = isDesktop ? SheetHeader : DrawerHeader;
- const Title = isDesktop ? SheetTitle : DrawerTitle;
- const Description = isDesktop ? SheetDescription : DrawerDescription;
+ // dialog and mobile page
+
+ const [isOpen, setIsOpen] = useState(review != undefined);
+
+ useEffect(() => {
+ setIsOpen(review != undefined);
+ }, [review]);
+
+ const Overlay = isDesktop ? Sheet : MobilePage;
+ const Content = isDesktop ? SheetContent : MobilePageContent;
+ const Header = isDesktop ? SheetHeader : MobilePageHeader;
+ const Title = isDesktop ? SheetTitle : MobilePageTitle;
+ const Description = isDesktop ? SheetDescription : MobilePageDescription;
if (!review) {
return;
@@ -90,7 +102,7 @@ export default function ReviewDetailDialog({
return (
<>
{
if (!open) {
setReview(undefined);
@@ -111,19 +123,43 @@ export default function ReviewDetailDialog({
-
- Review Item Details
- Review item details
-
+
{pane == "overview" && (
-
+
+ )}
+ {pane == "overview" && (
+
@@ -137,10 +173,10 @@ export default function ReviewDetailDialog({
{formattedDate}
-
-
+
+
Objects
-
+
{events?.map((event) => {
return (
{review.data.zones.length > 0 && (
-
+
Zones
{review.data.zones.map((zone) => {
@@ -199,11 +235,7 @@ export default function ReviewDetailDialog({
{pane == "details" && selectedEvent && (
-
+
)}
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx
index a67ec3b4a..f1692ccf4 100644
--- a/web/src/components/overlay/detail/SearchDetailDialog.tsx
+++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx
@@ -1,11 +1,4 @@
import { isDesktop, isIOS, isMobile } from "react-device-detect";
-import {
- Drawer,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
-} from "../../ui/drawer";
import { SearchResult } from "@/types/search";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
@@ -34,10 +27,23 @@ import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
-import { FaRegListAlt, FaVideo } from "react-icons/fa";
-import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
+import { FaImage, FaRegListAlt, FaVideo } from "react-icons/fa";
+import { FaRotate } from "react-icons/fa6";
+import ObjectLifecycle from "./ObjectLifecycle";
+import {
+ MobilePage,
+ MobilePageContent,
+ MobilePageDescription,
+ MobilePageHeader,
+ MobilePageTitle,
+} from "@/components/mobile/MobilePage";
-const SEARCH_TABS = ["details", "frigate+", "video"] as const;
+const SEARCH_TABS = [
+ "details",
+ "snapshot",
+ "video",
+ "object lifecycle",
+] as const;
type SearchTab = (typeof SEARCH_TABS)[number];
type SearchDetailDialogProps = {
@@ -59,6 +65,14 @@ export default function SearchDetailDialog({
const [page, setPage] = useState
("details");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
+ // dialog and mobile page
+
+ const [isOpen, setIsOpen] = useState(search != undefined);
+
+ useEffect(() => {
+ setIsOpen(search != undefined);
+ }, [search]);
+
const searchTabs = useMemo(() => {
if (!config || !search) {
return [];
@@ -66,8 +80,8 @@ export default function SearchDetailDialog({
const views = [...SEARCH_TABS];
- if (!config.plus.enabled || !search.has_snapshot) {
- const index = views.indexOf("frigate+");
+ if (!search.has_snapshot) {
+ const index = views.indexOf("snapshot");
views.splice(index, 1);
}
@@ -80,21 +94,31 @@ export default function SearchDetailDialog({
return views;
}, [config, search]);
+ useEffect(() => {
+ if (searchTabs.length == 0) {
+ return;
+ }
+
+ if (!searchTabs.includes(pageToggle)) {
+ setPage("details");
+ }
+ }, [pageToggle, searchTabs]);
+
if (!search) {
return;
}
// content
- const Overlay = isDesktop ? Dialog : Drawer;
- const Content = isDesktop ? DialogContent : DrawerContent;
- const Header = isDesktop ? DialogHeader : DrawerHeader;
- const Title = isDesktop ? DialogTitle : DrawerTitle;
- const Description = isDesktop ? DialogDescription : DrawerDescription;
+ const Overlay = isDesktop ? Dialog : MobilePage;
+ const Content = isDesktop ? DialogContent : MobilePageContent;
+ const Header = isDesktop ? DialogHeader : MobilePageHeader;
+ const Title = isDesktop ? DialogTitle : MobilePageTitle;
+ const Description = isDesktop ? DialogDescription : MobilePageDescription;
return (
{
if (!open) {
setSearch(undefined);
@@ -102,15 +126,16 @@ export default function SearchDetailDialog({
}}
>
-
+ setIsOpen(false)}>
Tracked Object Details
- Tracked object details
+ Tracked object details
{item == "details" && }
- {item == "frigate+" && }
+ {item == "snapshot" && }
{item == "video" && }
+ {item == "object lifecycle" && (
+
+ )}
{item}
))}
@@ -153,9 +181,14 @@ export default function SearchDetailDialog({
setSimilarity={setSimilarity}
/>
)}
- {page == "frigate+" && (
+ {page == "snapshot" && (
{}}
onEventUploaded={() => {
@@ -164,6 +197,14 @@ export default function SearchDetailDialog({
/>
)}
{page == "video" && }
+ {page == "object lifecycle" && (
+ {}}
+ />
+ )}
);
@@ -195,6 +236,7 @@ function ObjectDetailsTab({
config?.ui.time_format == "24hour"
? "%b %-d %Y, %H:%M"
: "%b %-d %Y, %I:%M %p",
+ config?.ui.timezone,
);
const score = useMemo(() => {
@@ -242,7 +284,7 @@ function ObjectDetailsTab({
}, [desc, search]);
return (
-
+
@@ -270,7 +312,7 @@ function ObjectDetailsTab({
{formattedDate}
-
+
![isDesktop]()
{
if (!config || !upload) {
return "";
@@ -79,60 +83,74 @@ export function FrigatePlusDialog({
const content = (
-
- Submit To Frigate+
-
- Objects in locations you want to avoid are not false positives.
- Submitting them as false positives will confuse the model.
-
-
-
- {upload?.id && (
-
- )}
-
+
+
+
+ Submit To Frigate+
+
+
+ Objects in locations you want to avoid are not false positives.
+ Submitting them as false positives will confuse the model.
+
+
+
+ {upload?.id && (
+
+ )}
+
-
- {state == "reviewing" && (
- <>
- {dialog && }
-
-
- >
- )}
- {state == "uploading" && }
-
+
+ {state == "reviewing" && (
+ <>
+ {dialog && }
+
+
+ >
+ )}
+ {state == "uploading" && }
+
+
);
diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx
index dff56ef88..fcb4bca4a 100644
--- a/web/src/components/player/PreviewPlayer.tsx
+++ b/web/src/components/player/PreviewPlayer.tsx
@@ -539,6 +539,7 @@ function PreviewFramesPlayer({
![]()
{previewFrames?.length === 0 && (
diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx
index d20ad1ec7..69a6ac6df 100644
--- a/web/src/components/player/PreviewThumbnailPlayer.tsx
+++ b/web/src/components/player/PreviewThumbnailPlayer.tsx
@@ -168,6 +168,7 @@ export default function PreviewThumbnailPlayer({
const formattedDate = useFormattedTimestamp(
review.start_time,
config?.ui.time_format == "24hour" ? "%b %-d, %H:%M" : "%b %-d, %I:%M %p",
+ config?.ui?.timezone,
);
return (
diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx
index 3b9f0dd7e..7600c3b29 100644
--- a/web/src/components/settings/ZoneEditPane.tsx
+++ b/web/src/components/settings/ZoneEditPane.tsx
@@ -373,7 +373,7 @@ export default function ZoneEditPane({
Name
@@ -395,7 +395,7 @@ export default function ZoneEditPane({
Inertia
@@ -417,7 +417,7 @@ export default function ZoneEditPane({
Loitering Time
diff --git a/web/src/hooks/use-api-filter.ts b/web/src/hooks/use-api-filter.ts
index 79269baee..185d0df9a 100644
--- a/web/src/hooks/use-api-filter.ts
+++ b/web/src/hooks/use-api-filter.ts
@@ -65,11 +65,11 @@ export function useApiFilterArgs<
const filter: { [key: string]: unknown } = {};
rawParams.forEach((value, key) => {
- if (isNaN(parseFloat(value))) {
+ if (value != "true" && value != "false" && isNaN(parseFloat(value))) {
filter[key] = value.includes(",") ? value.split(",") : [value];
} else {
if (value != undefined) {
- filter[key] = `${value}`;
+ filter[key] = JSON.parse(value);
}
}
});
diff --git a/web/src/hooks/use-date-utils.ts b/web/src/hooks/use-date-utils.ts
index c234cadd4..00f523920 100644
--- a/web/src/hooks/use-date-utils.ts
+++ b/web/src/hooks/use-date-utils.ts
@@ -2,12 +2,17 @@ import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useMemo } from "react";
-export function useFormattedTimestamp(timestamp: number, format: string) {
+export function useFormattedTimestamp(
+ timestamp: number,
+ format: string,
+ timezone?: string,
+) {
const formattedTimestamp = useMemo(() => {
return formatUnixTimestampToDateTime(timestamp, {
+ timezone,
strftime_fmt: format,
});
- }, [format, timestamp]);
+ }, [format, timestamp, timezone]);
return formattedTimestamp;
}
diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx
index 0db339c9b..e68476682 100644
--- a/web/src/hooks/use-overlay-state.tsx
+++ b/web/src/hooks/use-overlay-state.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
+import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { usePersistence } from "./use-persistence";
export function useOverlayState(
@@ -103,33 +103,29 @@ export function useHashState(): [
export function useSearchEffect(
key: string,
- callback: (value: string) => void,
+ callback: (value: string) => boolean,
) {
- const location = useLocation();
+ const [searchParams, setSearchParams] = useSearchParams();
const param = useMemo(() => {
- if (!location || !location.search || location.search.length == 0) {
+ const param = searchParams.get(key);
+
+ if (!param) {
return undefined;
}
- const params = location.search.substring(1).split("&");
-
- const foundParam = params
- .find((p) => p.includes("=") && p.split("=")[0] == key)
- ?.split("=");
-
- if (foundParam && foundParam.length === 2) {
- return [foundParam[0], decodeURIComponent(foundParam[1])];
- }
-
- return undefined;
- }, [location, key]);
+ return [key, decodeURIComponent(param)];
+ }, [searchParams, key]);
useEffect(() => {
if (!param) {
return;
}
- callback(param[1]);
- }, [param, callback]);
+ const remove = callback(param[1]);
+
+ if (remove) {
+ setSearchParams();
+ }
+ }, [param, callback, setSearchParams]);
}
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index 68e5c9adc..cbdd246f3 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -1,5 +1,5 @@
import ActivityIndicator from "@/components/indicators/activity-indicator";
-import useApiFilter from "@/hooks/use-api-filter";
+import { useApiFilterArgs } from "@/hooks/use-api-filter";
import { useCameraPreviews } from "@/hooks/use-camera-previews";
import { useTimezone } from "@/hooks/use-date-utils";
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
@@ -54,6 +54,8 @@ export default function Events() {
}
})
.catch(() => {});
+
+ return true;
});
const [startTime, setStartTime] = useState();
@@ -69,7 +71,7 @@ export default function Events() {
// review filter
const [reviewFilter, setReviewFilter, reviewSearchParams] =
- useApiFilter();
+ useApiFilterArgs();
useSearchEffect("group", (reviewGroup) => {
if (config && reviewGroup && reviewGroup != "default") {
@@ -83,7 +85,11 @@ export default function Events() {
cameras: group.cameras,
});
}
+
+ return true;
}
+
+ return false;
});
const onUpdateFilter = useCallback(
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx
index 750c75fde..f68bb5068 100644
--- a/web/src/pages/Explore.tsx
+++ b/web/src/pages/Explore.tsx
@@ -43,6 +43,7 @@ export default function Explore() {
setSearch(`similarity:${similarityId}`);
// @ts-expect-error we want to clear this
setSearchFilter({ ...searchFilter, similarity_search_id: undefined });
+ return false;
});
useEffect(() => {
diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx
index 451d52052..79555909c 100644
--- a/web/src/pages/Exports.tsx
+++ b/web/src/pages/Exports.tsx
@@ -13,6 +13,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner";
+import { useSearchEffect } from "@/hooks/use-overlay-state";
import { cn } from "@/lib/utils";
import { DeleteClipType, Export } from "@/types/export";
import axios from "axios";
@@ -46,6 +47,20 @@ function Exports() {
);
}, [exports, search]);
+ // Viewing
+
+ const [selected, setSelected] = useState();
+ const [selectedAspect, setSelectedAspect] = useState(0.0);
+
+ useSearchEffect("id", (id) => {
+ if (!exports) {
+ return false;
+ }
+
+ setSelected(exports.find((exp) => exp.id == id));
+ return true;
+ });
+
// Deleting
const [deleteClip, setDeleteClip] = useState();
@@ -91,11 +106,6 @@ function Exports() {
[mutate],
);
- // Viewing
-
- const [selected, setSelected] = useState();
- const [selectedAspect, setSelectedAspect] = useState(0.0);
-
return (
@@ -165,7 +175,7 @@ function Exports() {
{exports && (
setSearch(e.target.value)}
diff --git a/web/src/pages/Live.tsx b/web/src/pages/Live.tsx
index c088a5b04..9852852a7 100644
--- a/web/src/pages/Live.tsx
+++ b/web/src/pages/Live.tsx
@@ -29,7 +29,11 @@ function Live() {
if (group) {
setCameraGroup(cameraGroup);
}
+
+ return true;
}
+
+ return false;
});
// fullscreen
diff --git a/web/src/utils/browserUtil.ts b/web/src/utils/browserUtil.ts
new file mode 100644
index 000000000..b6a82fb54
--- /dev/null
+++ b/web/src/utils/browserUtil.ts
@@ -0,0 +1,16 @@
+import copy from "copy-to-clipboard";
+import { toast } from "sonner";
+
+export function shareOrCopy(url: string, title?: string) {
+ if (window.isSecureContext && "share" in navigator) {
+ navigator.share({
+ url: url,
+ title: title,
+ });
+ } else {
+ copy(url);
+ toast.success("Copied URL to clipboard.", {
+ position: "top-center",
+ });
+ }
+}
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index a6af2e331..25e6532d7 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -641,7 +641,7 @@ function DetectionReview({
>
{filter?.before == undefined && (
void;
+ onSelectSearch: (searchResult: SearchResult, index: number) => void;
};
export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
@@ -60,7 +60,7 @@ export default function ExploreView({ onSelectSearch }: ExploreViewProps) {
}
return (
-
+
{Object.entries(eventsByLabel).map(([label, filteredEvents]) => (
void;
+ onSelectSearch: (searchResult: SearchResult, index: number) => void;
};
function ThumbnailRow({
@@ -145,7 +145,7 @@ function ThumbnailRow({
type ExploreThumbnailImageProps = {
event: SearchResult;
- onSelectSearch: (searchResult: SearchResult) => void;
+ onSelectSearch: (searchResult: SearchResult, index: number) => void;
};
function ExploreThumbnailImage({
event,
@@ -176,7 +176,7 @@ function ExploreThumbnailImage({
loading={isSafari ? "eager" : "lazy"}
draggable={false}
src={`${apiHost}api/events/${event.id}/thumbnail.jpg`}
- onClick={() => onSelectSearch(event)}
+ onClick={() => onSelectSearch(event, 0)}
onLoad={() => {
onImgLoad();
}}
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx
index 6436867b7..3b6fb49e3 100644
--- a/web/src/views/search/SearchView.tsx
+++ b/web/src/views/search/SearchView.tsx
@@ -13,11 +13,15 @@ import {
import { cn } from "@/lib/utils";
import { FrigateConfig } from "@/types/frigateConfig";
import { SearchFilter, SearchResult } from "@/types/search";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { isMobileOnly } from "react-device-detect";
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
import useSWR from "swr";
import ExploreView from "../explore/ExploreView";
+import useKeyboardListener, {
+ KeyModifiers,
+} from "@/hooks/use-keyboard-listener";
+import scrollIntoView from "scroll-into-view-if-needed";
type SearchViewProps = {
search: string;
@@ -59,8 +63,12 @@ export default function SearchView({
// search interaction
- const onSelectSearch = useCallback((item: SearchResult) => {
+ const [selectedIndex, setSelectedIndex] = useState(null);
+ const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
+
+ const onSelectSearch = useCallback((item: SearchResult, index: number) => {
setSearchDetail(item);
+ setSelectedIndex(index);
}, []);
// confidence score - probably needs tweaking
@@ -87,6 +95,56 @@ export default function SearchView({
[searchResults, searchFilter],
);
+ // keyboard listener
+
+ const onKeyboardShortcut = useCallback(
+ (key: string | null, modifiers: KeyModifiers) => {
+ if (!modifiers.down || !uniqueResults) {
+ return;
+ }
+
+ switch (key) {
+ case "ArrowLeft":
+ setSelectedIndex((prevIndex) => {
+ const newIndex =
+ prevIndex === null
+ ? uniqueResults.length - 1
+ : (prevIndex - 1 + uniqueResults.length) % uniqueResults.length;
+ setSearchDetail(uniqueResults[newIndex]);
+ return newIndex;
+ });
+ break;
+ case "ArrowRight":
+ setSelectedIndex((prevIndex) => {
+ const newIndex =
+ prevIndex === null ? 0 : (prevIndex + 1) % uniqueResults.length;
+ setSearchDetail(uniqueResults[newIndex]);
+ return newIndex;
+ });
+ break;
+ }
+ },
+ [uniqueResults],
+ );
+
+ useKeyboardListener(["ArrowLeft", "ArrowRight"], onKeyboardShortcut);
+
+ // scroll into view
+
+ useEffect(() => {
+ if (
+ selectedIndex !== null &&
+ uniqueResults &&
+ itemRefs.current?.[selectedIndex]
+ ) {
+ scrollIntoView(itemRefs.current[selectedIndex], {
+ block: "center",
+ behavior: "smooth",
+ scrollMode: "if-needed",
+ });
+ }
+ }, [selectedIndex, uniqueResults]);
+
return (
@@ -156,12 +214,13 @@ export default function SearchView({
{uniqueResults && (
{uniqueResults &&
- uniqueResults.map((value) => {
- const selected = false;
+ uniqueResults.map((value, index) => {
+ const selected = selectedIndex === index;
return (
(itemRefs.current[index] = item)}
data-start={value.start_time}
className="review-item relative rounded-lg"
>
@@ -173,7 +232,7 @@ export default function SearchView({
setSimilaritySearch(value)}
- onClick={() => onSelectSearch(value)}
+ onClick={() => onSelectSearch(value, index)}
/>
{searchTerm && (
@@ -207,7 +266,7 @@ export default function SearchView({
)}
);
@@ -215,7 +274,7 @@ export default function SearchView({
)}
{!uniqueResults && !isLoading && (
-
+
)}
diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx
index 5e592b7a2..51d467fc3 100644
--- a/web/src/views/settings/NotificationsSettingsView.tsx
+++ b/web/src/views/settings/NotificationsSettingsView.tsx
@@ -254,7 +254,7 @@ export default function NotificationView({
Email