Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 149 additions & 11 deletions apps/desktop/src/components/settings/general/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { commands as notificationCommands } from "@hypr/plugin-notification";
import { Badge } from "@hypr/ui/components/ui/badge";
import { Button } from "@hypr/ui/components/ui/button";
import { Input } from "@hypr/ui/components/ui/input";
import { Switch } from "@hypr/ui/components/ui/switch";
import { cn } from "@hypr/utils";

Expand All @@ -29,6 +30,10 @@ export function NotificationSettingsView() {
"notification_detect",
"respect_dnd",
"ignored_platforms",
"event_notify_before_minutes",
"event_notification_timeout_secs",
"mic_detection_delay_secs",
"mic_notification_timeout_secs",
] as const);

useEffect(() => {
Expand Down Expand Up @@ -101,12 +106,44 @@ export function NotificationSettingsView() {
settings.STORE_ID,
);

const handleSetEventNotifyBeforeMinutes = settings.UI.useSetValueCallback(
"event_notify_before_minutes",
(value: number) => value,
[],
settings.STORE_ID,
);

const handleSetEventNotificationTimeoutSecs = settings.UI.useSetValueCallback(
"event_notification_timeout_secs",
(value: number) => value,
[],
settings.STORE_ID,
);

const handleSetMicDetectionDelaySecs = settings.UI.useSetValueCallback(
"mic_detection_delay_secs",
(value: number) => value,
[],
settings.STORE_ID,
);

const handleSetMicNotificationTimeoutSecs = settings.UI.useSetValueCallback(
"mic_notification_timeout_secs",
(value: number) => value,
[],
settings.STORE_ID,
);

const form = useForm({
defaultValues: {
notification_event: configs.notification_event,
notification_detect: configs.notification_detect,
respect_dnd: configs.respect_dnd,
ignored_platforms: configs.ignored_platforms.map(bundleIdToName),
event_notify_before_minutes: configs.event_notify_before_minutes,
event_notification_timeout_secs: configs.event_notification_timeout_secs,
mic_detection_delay_secs: configs.mic_detection_delay_secs,
mic_notification_timeout_secs: configs.mic_notification_timeout_secs,
},
listeners: {
onChange: async ({ formApi }) => {
Expand All @@ -120,6 +157,12 @@ export function NotificationSettingsView() {
handleSetIgnoredPlatforms(
JSON.stringify(value.ignored_platforms.map(nameToBundleId)),
);
handleSetEventNotifyBeforeMinutes(value.event_notify_before_minutes);
handleSetEventNotificationTimeoutSecs(
value.event_notification_timeout_secs,
);
handleSetMicDetectionDelaySecs(value.mic_detection_delay_secs);
handleSetMicNotificationTimeoutSecs(value.mic_notification_timeout_secs);
},
});

Expand Down Expand Up @@ -220,17 +263,68 @@ export function NotificationSettingsView() {
<div className="flex flex-col gap-6">
<form.Field name="notification_event">
{(field) => (
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">Event notifications</h3>
<p className="text-xs text-neutral-600">
Get notified 5 minutes before calendar events start
</p>
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">
Event notifications
</h3>
<p className="text-xs text-neutral-600">
Get notified before calendar events start
</p>
</div>
<Switch
checked={field.state.value}
onCheckedChange={field.handleChange}
/>
</div>
<Switch
checked={field.state.value}
onCheckedChange={field.handleChange}
/>

{field.state.value && (
<div
className={cn([
"ml-6 border-l-2 border-muted pl-6 flex flex-col gap-3",
])}
>
<form.Field name="event_notify_before_minutes">
{(subField) => (
<div className="flex items-center justify-between gap-4">
<span className="text-xs text-neutral-600">
Minutes before event
</span>
<Input
type="number"
min={1}
max={60}
className="w-20 h-7 text-xs"
value={subField.state.value}
onChange={(e) =>
subField.handleChange(Number(e.target.value))
}
Comment on lines +300 to +302
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input validation issue: When user clears the input field, Number(e.target.value) returns 0 for empty string, which violates the min={1} constraint and bypasses HTML5 validation since the onChange fires immediately. This will set event_notify_before_minutes to 0, causing notifications to only trigger for events starting at the exact current moment (0ms window in event-notification/index.ts line 76).

value={subField.state.value}
onChange={(e) => {
  const val = Number(e.target.value);
  if (!isNaN(val) && val >= 1) {
    subField.handleChange(val);
  }
}}
Suggested change
onChange={(e) =>
subField.handleChange(Number(e.target.value))
}
onChange={(e) => {
const val = Number(e.target.value);
if (!isNaN(val) && val >= 1) {
subField.handleChange(val);
}
}}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

/>
</div>
)}
</form.Field>
<form.Field name="event_notification_timeout_secs">
{(subField) => (
<div className="flex items-center justify-between gap-4">
<span className="text-xs text-neutral-600">
Auto-dismiss after (seconds)
</span>
<Input
type="number"
min={0}
max={300}
className="w-20 h-7 text-xs"
value={subField.state.value}
onChange={(e) =>
subField.handleChange(Number(e.target.value))
}
/>
</div>
)}
</form.Field>
</div>
)}
</div>
)}
</form.Field>
Expand All @@ -255,7 +349,51 @@ export function NotificationSettingsView() {
</div>

{field.state.value && (
<div className={cn(["ml-6 border-l-2 border-muted pl-6 pt-2"])}>
<div
className={cn([
"ml-6 border-l-2 border-muted pl-6 pt-2 flex flex-col gap-4",
])}
>
<div className="flex flex-col gap-3">
<form.Field name="mic_detection_delay_secs">
{(subField) => (
<div className="flex items-center justify-between gap-4">
<span className="text-xs text-neutral-600">
Detection delay (seconds)
</span>
<Input
type="number"
min={0}
max={600}
className="w-20 h-7 text-xs"
value={subField.state.value}
onChange={(e) =>
subField.handleChange(Number(e.target.value))
}
/>
</div>
)}
</form.Field>
<form.Field name="mic_notification_timeout_secs">
{(subField) => (
<div className="flex items-center justify-between gap-4">
<span className="text-xs text-neutral-600">
Auto-dismiss after (seconds)
</span>
<Input
type="number"
min={0}
max={300}
className="w-20 h-7 text-xs"
value={subField.state.value}
onChange={(e) =>
subField.handleChange(Number(e.target.value))
}
/>
</div>
)}
</form.Field>
</div>
<div className="mb-3 flex flex-col gap-1">
<h4 className="text-sm font-medium">
Exclude apps from detection
Expand Down
32 changes: 0 additions & 32 deletions apps/desktop/src/components/settings/lab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import { arch, platform } from "@tauri-apps/plugin-os";
import { commands as openerCommands } from "@hypr/plugin-opener2";
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Button } from "@hypr/ui/components/ui/button";
import { Switch } from "@hypr/ui/components/ui/switch";
import { cn } from "@hypr/utils";

import { useConfigValue } from "../../../config/use-config";
import * as settings from "../../../store/tinybase/store/settings";

export function SettingsLab() {
const handleOpenControlWindow = async () => {
await windowsCommands.windowShow({ type: "control" });
Expand All @@ -30,39 +26,11 @@ export function SettingsLab() {
</Button>
</div>

<MeetingReminderToggle />

<DownloadButtons />
</div>
);
}

function MeetingReminderToggle() {
const value = useConfigValue("notification_in_meeting_reminder");
const setValue = settings.UI.useSetValueCallback(
"notification_in_meeting_reminder",
(value: boolean) => value,
[],
settings.STORE_ID,
);

return (
<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-sm font-medium mb-1">In-Meeting Reminder</h3>
<p className="text-xs text-neutral-600">
Get nudged when a meeting app is using your mic without Hyprnote
recording.
</p>
</div>
<Switch
checked={value}
onCheckedChange={(checked) => setValue(checked)}
/>
</div>
);
}

function DownloadButtons() {
const platformName = platform();
const archQuery = useQuery({
Expand Down
29 changes: 25 additions & 4 deletions apps/desktop/src/config/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export type ConfigKey =
| "current_llm_model"
| "timezone"
| "week_start"
| "notification_in_meeting_reminder";
| "event_notify_before_minutes"
| "event_notification_timeout_secs"
| "mic_detection_delay_secs"
| "mic_notification_timeout_secs";

type ConfigValueType<K extends ConfigKey> =
(typeof CONFIG_REGISTRY)[K]["default"];
Expand Down Expand Up @@ -153,8 +156,26 @@ export const CONFIG_REGISTRY = {
default: undefined as "sunday" | "monday" | undefined,
},

notification_in_meeting_reminder: {
key: "notification_in_meeting_reminder",
default: true,
event_notify_before_minutes: {
key: "event_notify_before_minutes",
default: 5,
},

event_notification_timeout_secs: {
key: "event_notification_timeout_secs",
default: 30,
},

mic_detection_delay_secs: {
key: "mic_detection_delay_secs",
default: 0,
sideEffect: async (value: number, _) => {
await detectCommands.setMicDetectionDelay(value);
},
},

mic_notification_timeout_secs: {
key: "mic_notification_timeout_secs",
default: 8,
},
} satisfies Record<ConfigKey, ConfigDefinition>;
Loading
Loading