1import { useState } from "react";
2
3import {
4 hasDesktopNotifications,
5 hasNotificationSound,
6 NOTIFICATION_MODE_LABELS,
7 unlockNotificationAudio,
8} from "../../threadNotifications";
9import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
10import { SettingsRow } from "./settingsLayout";
11import { searchableSetting } from "./settingsSearch";
12import { useScopedSettings, useUpdateScopedSettings } from "./useScopedSettings";
13
14export function NotificationSettings() {
15 const mode = useScopedSettings((settings) => settings.notificationMode);
16 const updateSettings = useUpdateScopedSettings();
17 const [permissionMessage, setPermissionMessage] = useState<string | null>(null);
18 const [requesting, setRequesting] = useState(false);
19
20 return (
21 <SettingsRow
22 {...searchableSetting("thread-notifications")}
23 description={
24 permissionMessage ??
25 "System alerts when a thread finishes, fails, or needs input or approval. Applies to this device while The Knife is open."
26 }
27 control={
28 <Select
29 value={mode}
30 disabled={requesting}
31 onValueChange={async (value) => {
32 if (
33 value !== "off" &&
34 value !== "notifications" &&
35 value !== "sound" &&
36 value !== "notifications-and-sound"
37 )
38 return;
39 setPermissionMessage(null);
40 if (hasNotificationSound(value)) unlockNotificationAudio();
41 if (hasDesktopNotifications(value)) {
42 if (typeof Notification === "undefined" || !window.isSecureContext) {
43 setPermissionMessage(
44 "Notifications need a supported browser over HTTPS, or the desktop app. Sound only is still available.",
45 );
46 return;
47 }
48 setRequesting(true);
49 try {
50 const permission = await Notification.requestPermission();
51 if (permission !== "granted") {
52 setPermissionMessage(
53 "Allow notifications in your browser or system settings, then choose this option again. Sound only is still available.",
54 );
55 return;
56 }
57 } catch {
58 setPermissionMessage(
59 "Notifications are unavailable in this browser. Sound only is still available.",
60 );
61 return;
62 } finally {
63 setRequesting(false);
64 }
65 }
66 updateSettings({ notificationMode: value });
67 }}
68 >
69 <SelectTrigger size="sm" className="w-full sm:w-56" aria-label="Thread notifications">
70 <SelectValue>{NOTIFICATION_MODE_LABELS[mode]}</SelectValue>
71 </SelectTrigger>
72 <SelectPopup align="end" alignItemWithTrigger={false}>
73 {Object.entries(NOTIFICATION_MODE_LABELS).map(([value, label]) => (
74 <SelectItem key={value} hideIndicator value={value}>
75 {label}
76 </SelectItem>
77 ))}
78 </SelectPopup>
79 </Select>
80 }
81 />
82 );
83}