Skip to content

Commit 8b52962

Browse files
committed
fix notification ui and auto-start trigger
1 parent 03f7280 commit 8b52962

File tree

11 files changed

+138
-47
lines changed

11 files changed

+138
-47
lines changed

apps/desktop/src/components/event-listeners.tsx

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
import { getCurrentWebviewWindowLabel } from "@hypr/plugin-windows";
1010

1111
import * as main from "../store/tinybase/store/main";
12-
import { getOrCreateSessionForEventId } from "../store/tinybase/store/sessions";
12+
import {
13+
createSession,
14+
getOrCreateSessionForEventId,
15+
} from "../store/tinybase/store/sessions";
1316
import { useTabs } from "../store/zustand/tabs";
1417

1518
function useUpdaterEvents() {
@@ -43,13 +46,22 @@ function useUpdaterEvents() {
4346
function useNotificationEvents() {
4447
const store = main.UI.useStore(main.STORE_ID);
4548
const openNew = useTabs((state) => state.openNew);
46-
const pendingEventId = useRef<string | null>(null);
49+
const pendingAutoStart = useRef<{ eventId: string | null } | null>(null);
50+
const storeRef = useRef(store);
51+
const openNewRef = useRef(openNew);
52+
53+
useEffect(() => {
54+
storeRef.current = store;
55+
openNewRef.current = openNew;
56+
}, [store, openNew]);
4757

4858
useEffect(() => {
49-
if (pendingEventId.current && store) {
50-
const eventId = pendingEventId.current;
51-
pendingEventId.current = null;
52-
const sessionId = getOrCreateSessionForEventId(store, eventId);
59+
if (pendingAutoStart.current && store) {
60+
const { eventId } = pendingAutoStart.current;
61+
pendingAutoStart.current = null;
62+
const sessionId = eventId
63+
? getOrCreateSessionForEventId(store, eventId)
64+
: createSession(store);
5365
openNew({
5466
type: "sessions",
5567
id: sessionId,
@@ -64,37 +76,42 @@ function useNotificationEvents() {
6476
}
6577

6678
let unlisten: UnlistenFn | null = null;
79+
let cancelled = false;
6780

6881
void notificationEvents.notificationEvent
6982
.listen(({ payload }) => {
7083
if (
71-
(payload.type === "notification_confirm" ||
72-
payload.type === "notification_accept") &&
73-
payload.event_id
84+
payload.type === "notification_confirm" ||
85+
payload.type === "notification_accept"
7486
) {
75-
if (!store) {
76-
pendingEventId.current = payload.event_id;
87+
const currentStore = storeRef.current;
88+
if (!currentStore) {
89+
pendingAutoStart.current = { eventId: payload.event_id };
7790
return;
7891
}
79-
const sessionId = getOrCreateSessionForEventId(
80-
store,
81-
payload.event_id,
82-
);
83-
openNew({
92+
const sessionId = payload.event_id
93+
? getOrCreateSessionForEventId(currentStore, payload.event_id)
94+
: createSession(currentStore);
95+
openNewRef.current({
8496
type: "sessions",
8597
id: sessionId,
8698
state: { view: null, autoStart: true },
8799
});
88100
}
89101
})
90102
.then((f) => {
91-
unlisten = f;
103+
if (cancelled) {
104+
f();
105+
} else {
106+
unlisten = f;
107+
}
92108
});
93109

94110
return () => {
111+
cancelled = true;
95112
unlisten?.();
96113
};
97-
}, [store, openNew]);
114+
}, []);
98115
}
99116

100117
export function EventListeners() {

apps/desktop/src/components/task-manager.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
checkEventNotifications,
1717
EVENT_NOTIFICATION_INTERVAL,
1818
EVENT_NOTIFICATION_TASK_ID,
19+
type NotifiedEventsMap,
1920
} from "../services/event-notification";
2021
import * as main from "../store/tinybase/store/main";
2122
import * as settings from "../store/tinybase/store/settings";
@@ -27,7 +28,7 @@ export function TaskManager() {
2728
const queries = main.UI.useQueries(main.STORE_ID);
2829

2930
const settingsStore = settings.UI.useStore(settings.STORE_ID);
30-
const notifiedEventsRef = useRef<Set<string>>(new Set());
31+
const notifiedEventsRef = useRef<NotifiedEventsMap>(new Map());
3132

3233
useSetTask(CALENDAR_SYNC_TASK_ID, async () => {
3334
await syncCalendarEvents(

apps/desktop/src/services/apple-calendar/fetch/incoming.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import type {
99
IncomingParticipants,
1010
} from "./types";
1111

12+
export class CalendarFetchError extends Error {
13+
constructor(
14+
public readonly calendarTrackingId: string,
15+
public readonly cause: string,
16+
) {
17+
super(
18+
`Failed to fetch events for calendar ${calendarTrackingId}: ${cause}`,
19+
);
20+
this.name = "CalendarFetchError";
21+
}
22+
}
23+
1224
export async function fetchIncomingEvents(ctx: Ctx): Promise<{
1325
events: IncomingEvent[];
1426
participants: IncomingParticipants;
@@ -24,7 +36,7 @@ export async function fetchIncomingEvents(ctx: Ctx): Promise<{
2436
});
2537

2638
if (result.status === "error") {
27-
return [];
39+
throw new CalendarFetchError(trackingId, result.error);
2840
}
2941

3042
return result.data;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { fetchExistingEvents } from "./existing";
2-
export { fetchIncomingEvents } from "./incoming";
2+
export { CalendarFetchError, fetchIncomingEvents } from "./incoming";
33
export type { ExistingEvent, IncomingEvent } from "./types";

apps/desktop/src/services/apple-calendar/index.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import type { Queries } from "tinybase/with-schemas";
22

33
import type { Schemas, Store } from "../../store/tinybase/store/main";
44
import { createCtx } from "./ctx";
5-
import { fetchExistingEvents, fetchIncomingEvents } from "./fetch";
5+
import {
6+
CalendarFetchError,
7+
fetchExistingEvents,
8+
fetchIncomingEvents,
9+
} from "./fetch";
610
import {
711
executeForEventsSync,
812
executeForParticipantsSync,
@@ -28,8 +32,23 @@ async function run(store: Store, queries: Queries<Schemas>) {
2832
return null;
2933
}
3034

31-
const { events: incoming, participants: incomingParticipants } =
32-
await fetchIncomingEvents(ctx);
35+
let incoming;
36+
let incomingParticipants;
37+
38+
try {
39+
const result = await fetchIncomingEvents(ctx);
40+
incoming = result.events;
41+
incomingParticipants = result.participants;
42+
} catch (error) {
43+
if (error instanceof CalendarFetchError) {
44+
console.error(
45+
`[calendar-sync] Aborting sync due to fetch error: ${error.message}`,
46+
);
47+
return null;
48+
}
49+
throw error;
50+
}
51+
3352
const existing = fetchExistingEvents(ctx);
3453

3554
const eventsOut = syncEvents(ctx, { incoming, existing });

apps/desktop/src/services/event-notification/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,42 @@ export const EVENT_NOTIFICATION_TASK_ID = "eventNotification";
77
export const EVENT_NOTIFICATION_INTERVAL = 30 * 1000; // 30 sec
88

99
const NOTIFY_WINDOW_MS = 5 * 60 * 1000; // 5 minutes before
10+
const NOTIFIED_EVENTS_TTL_MS = 10 * 60 * 1000; // 10 minutes TTL for cleanup
11+
12+
export type NotifiedEventsMap = Map<string, number>;
1013

1114
export function checkEventNotifications(
1215
store: main.Store,
1316
settingsStore: settings.Store,
14-
notifiedEvents: Set<string>,
17+
notifiedEvents: NotifiedEventsMap,
1518
) {
1619
const notificationEnabled = settingsStore?.getValue("notification_event");
1720
if (!notificationEnabled || !store) {
1821
return;
1922
}
2023

21-
const now = new Date();
24+
const now = Date.now();
25+
26+
for (const [key, timestamp] of notifiedEvents) {
27+
if (now - timestamp > NOTIFIED_EVENTS_TTL_MS) {
28+
notifiedEvents.delete(key);
29+
}
30+
}
2231

2332
store.forEachRow("events", (eventId, _forEachCell) => {
2433
const event = store.getRow("events", eventId);
2534
if (!event?.started_at) return;
2635

2736
const startTime = new Date(String(event.started_at));
28-
const timeUntilStart = startTime.getTime() - now.getTime();
37+
const timeUntilStart = startTime.getTime() - now;
2938
const notificationKey = `event-${eventId}-${startTime.getTime()}`;
3039

3140
if (timeUntilStart > 0 && timeUntilStart <= NOTIFY_WINDOW_MS) {
3241
if (notifiedEvents.has(notificationKey)) {
3342
return;
3443
}
3544

36-
notifiedEvents.add(notificationKey);
45+
notifiedEvents.set(notificationKey, now);
3746

3847
const title = String(event.title || "Upcoming Event");
3948
const minutesUntil = Math.ceil(timeUntilStart / 60000);

apps/desktop/src/store/tinybase/store/sessions.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import * as main from "./main";
66

77
type Store = NonNullable<ReturnType<typeof main.UI.useStore>>;
88

9+
export function createSession(store: Store, title?: string): string {
10+
const sessionId = id();
11+
store.setRow("sessions", sessionId, {
12+
title: title ?? "",
13+
created_at: new Date().toISOString(),
14+
raw_md: "",
15+
enhanced_md: "",
16+
user_id: DEFAULT_USER_ID,
17+
});
18+
void analyticsCommands.event({
19+
event: "note_created",
20+
has_event_id: false,
21+
});
22+
return sessionId;
23+
}
24+
925
export function getOrCreateSessionForEventId(
1026
store: Store,
1127
eventId: string,

crates/notification-linux/examples/test_notification.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,19 @@ use gtk::prelude::*;
33
use notification_linux::*;
44

55
fn main() {
6-
// Initialize GTK
76
gtk::init().expect("Failed to initialize GTK");
87

98
let notification = Notification::builder()
109
.title("Test Notification")
1110
.message("This is a test notification from hyprnote")
12-
.url("https://example.com")
1311
.timeout(std::time::Duration::from_secs(5))
1412
.build();
1513

16-
// Queue the notification on the default main context
1714
show(&notification);
1815

19-
// Quit the GTK main loop after 10 seconds so the example exits
2016
glib::timeout_add_seconds_local_once(10, || {
2117
gtk::main_quit();
2218
});
2319

24-
// Drive the GTK / GLib event loop so the queued closure runs
2520
gtk::main();
2621
}

crates/notification-macos/examples/test_notification.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,23 @@ fn main() {
4747
std::thread::spawn(|| {
4848
std::thread::sleep(Duration::from_millis(200));
4949

50+
setup_notification_accept_handler(|id| {
51+
println!("accept: {}", id);
52+
});
5053
setup_notification_confirm_handler(|id| {
5154
println!("confirm: {}", id);
5255
});
5356
setup_notification_dismiss_handler(|id| {
5457
println!("dismiss: {}", id);
5558
});
59+
setup_notification_timeout_handler(|id| {
60+
println!("timeout: {}", id);
61+
});
5662

5763
let notification = Notification::builder()
5864
.key("test_notification")
5965
.title("Test Notification")
6066
.message("Hover/click should now react")
61-
.url("https://example.com")
6267
.timeout(Duration::from_secs(30))
6368
.build();
6469

crates/notification-macos/swift-lib/src/lib.swift

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class ClickableView: NSView {
134134
notification.key.withCString { keyPtr in
135135
rustOnNotificationConfirm(keyPtr)
136136
}
137-
notification.dismissWithUserAction()
137+
notification.dismiss()
138138
}
139139
}
140140

@@ -239,30 +239,30 @@ class ActionButton: NSButton {
239239
wantsLayer = true
240240
isBordered = false
241241
bezelStyle = .rounded
242-
controlSize = .regular
243-
font = NSFont.systemFont(ofSize: 14, weight: .semibold)
242+
controlSize = .small
243+
font = NSFont.systemFont(ofSize: 12, weight: .medium)
244244
focusRingType = .none
245245

246246
contentTintColor = NSColor(calibratedWhite: 0.1, alpha: 1.0)
247247
if #available(macOS 11.0, *) {
248248
bezelColor = NSColor(calibratedWhite: 0.9, alpha: 1.0)
249249
}
250250

251-
layer?.cornerRadius = 10
251+
layer?.cornerRadius = 8
252252
layer?.backgroundColor = NSColor(calibratedWhite: 0.95, alpha: 0.9).cgColor
253253
layer?.borderColor = NSColor(calibratedWhite: 0.7, alpha: 0.5).cgColor
254254
layer?.borderWidth = 0.5
255255

256256
layer?.shadowColor = NSColor(calibratedWhite: 0.0, alpha: 0.5).cgColor
257-
layer?.shadowOpacity = 0.3
258-
layer?.shadowRadius = 3
257+
layer?.shadowOpacity = 0.2
258+
layer?.shadowRadius = 2
259259
layer?.shadowOffset = CGSize(width: 0, height: 1)
260260
}
261261

262262
override var intrinsicContentSize: NSSize {
263263
var s = super.intrinsicContentSize
264-
s.width += 22
265-
s.height = max(28, s.height + 4)
264+
s.width += 12
265+
s.height = max(24, s.height + 2)
266266
return s
267267
}
268268

@@ -547,8 +547,7 @@ class NotificationManager {
547547

548548
NSLayoutConstraint.activate([
549549
contentView.leadingAnchor.constraint(equalTo: effectView.leadingAnchor, constant: 12),
550-
contentView.trailingAnchor.constraint(
551-
equalTo: effectView.trailingAnchor, constant: -35),
550+
contentView.trailingAnchor.constraint(equalTo: effectView.trailingAnchor, constant: -12),
552551
contentView.topAnchor.constraint(equalTo: effectView.topAnchor, constant: 9),
553552
contentView.bottomAnchor.constraint(equalTo: effectView.bottomAnchor, constant: -9),
554553
])
@@ -617,8 +616,14 @@ class NotificationManager {
617616
textStack.addArrangedSubview(titleLabel)
618617
textStack.addArrangedSubview(bodyLabel)
619618

619+
let actionButton = ActionButton()
620+
actionButton.title = "Take notes"
621+
actionButton.notification = notification
622+
actionButton.setContentHuggingPriority(.required, for: .horizontal)
623+
620624
container.addArrangedSubview(iconContainer)
621625
container.addArrangedSubview(textStack)
626+
container.addArrangedSubview(actionButton)
622627

623628
return container
624629
}

0 commit comments

Comments
 (0)