64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { useEffect, useRef, useCallback } from "react";
|
|
import { useEventsStore } from "./useEventsStore";
|
|
import { useReminderSettings } from "./useReminderSettings";
|
|
|
|
const POLLS_MS = 30_000; // 30s — matches useClock cadence
|
|
|
|
/**
|
|
* Browser notification reminder engine. Polls every 30s and fires a
|
|
* `Notification` when an event's leave-by time is reached (bufferMinutes
|
|
* before the event). Uses a ref-based fired-set to avoid duplicate alerts
|
|
* for the same event across polls.
|
|
*/
|
|
export function useReminder() {
|
|
const { events } = useEventsStore();
|
|
const { bufferMinutes, enabled } = useReminderSettings();
|
|
const firedRef = useRef(new Set<string>());
|
|
|
|
const check = useCallback(() => {
|
|
if (!enabled) return;
|
|
if (typeof window === "undefined") return;
|
|
if (typeof Notification === "undefined") return;
|
|
|
|
const now = new Date();
|
|
const upcoming = events
|
|
.filter((e) => e.eventTime > now)
|
|
.sort((a, b) => a.eventTime.getTime() - b.eventTime.getTime());
|
|
|
|
for (const event of upcoming) {
|
|
const reminderTime = new Date(event.eventTime.getTime() - bufferMinutes * 60_000);
|
|
if (now >= reminderTime && !firedRef.current.has(event.id)) {
|
|
firedRef.current.add(event.id);
|
|
|
|
if (Notification.permission === "granted") {
|
|
new Notification("Time to leave!", {
|
|
body: `${event.title} starts in ${bufferMinutes} minutes`,
|
|
tag: `reminder-${event.id}`,
|
|
});
|
|
} else if (Notification.permission !== "denied") {
|
|
Notification.requestPermission().then((perm) => {
|
|
if (perm === "granted") {
|
|
new Notification("Time to leave!", {
|
|
body: `${event.title} starts in ${bufferMinutes} minutes`,
|
|
tag: `reminder-${event.id}`,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear entries for events that no longer exist
|
|
const ids = new Set(upcoming.map((e) => e.id));
|
|
for (const id of firedRef.current) {
|
|
if (!ids.has(id)) firedRef.current.delete(id);
|
|
}
|
|
}, [events, bufferMinutes, enabled]);
|
|
|
|
useEffect(() => {
|
|
check();
|
|
const id = setInterval(check, POLLS_MS);
|
|
return () => clearInterval(id);
|
|
}, [check]);
|
|
}
|