"use client"; import { useEffect, useRef, useCallback } from 'react'; import { useTranslations, useLocale } from 'next-intl'; import { parseISO } from 'date-fns'; import { useAuthStore } from '@/stores/auth-store'; import { useCalendarStore } from '@/stores/calendar-store'; import { useSettingsStore } from '@/stores/settings-store'; import { useTaskStore } from '@/stores/task-store'; import { useCalendarNotificationStore } from '@/stores/calendar-notification-store'; import { useToastStore } from '@/stores/toast-store'; import { getPendingAlerts, getPendingTaskAlerts, buildAlertKey } from '@/lib/calendar-alerts'; import { playNotificationSound } from '@/lib/notification-sound'; import { getPathPrefix } from '@/lib/browser-navigation'; import type { CalendarEvent } from '@/lib/jmap/types'; const CHECK_INTERVAL_MS = 60 * 1000; const PROACTIVE_FETCH_HOURS = 24; const PROACTIVE_THROTTLE_MS = CHECK_INTERVAL_MS * 5; export function useCalendarAlerts() { const { isAuthenticated, client } = useAuthStore(); const { events, calendars, supportsCalendar } = useCalendarStore(); const { calendarNotificationsEnabled, calendarNotificationSound, enableCalendarTasks, notificationSoundChoice } = useSettingsStore(); const { tasks: storeTasks } = useTaskStore(); const { acknowledgedAlerts, acknowledgeAlert, cleanupStaleAlerts } = useCalendarNotificationStore(); const addToast = useToastStore((s) => s.addToast); const t = useTranslations('calendar.notifications'); const locale = useLocale(); const lastProactiveFetchRef = useRef(0); const proactiveEventsRef = useRef([]); const shownKeysRef = useRef>(new Set()); const checkAlerts = useCallback(() => { if (!calendarNotificationsEnabled || !isAuthenticated) return; try { const now = Date.now(); const acknowledgedKeys = new Set(Object.keys(acknowledgedAlerts)); const allEvents = [...events, ...proactiveEventsRef.current]; const pending = getPendingAlerts(allEvents, calendars, acknowledgedKeys, now); for (const alert of pending) { const key = buildAlertKey(alert.eventId, alert.alertId, alert.fireTimeMs); if (shownKeysRef.current.has(key)) continue; shownKeysRef.current.add(key); acknowledgeAlert(key, alert.fireTimeMs); if (calendarNotificationSound) { playNotificationSound(notificationSoundChoice); } const diffMs = (alert.event.utcStart ? new Date(alert.event.utcStart).getTime() : parseISO(alert.event.start).getTime()) - now; const diffMin = Math.round(diffMs / 60000); const timeLabel = diffMin <= 0 ? t('alert_now') : t('alert_in_minutes', { count: diffMin }); const message = alert.calendarName ? `${timeLabel} · ${alert.calendarName}` : timeLabel; addToast({ type: 'info', title: alert.event.title || t('alert_title'), message, duration: 15000, onClick: () => { window.location.href = `${getPathPrefix(locale)}/${locale}/calendar`; }, }); } // Task alerts if (enableCalendarTasks && storeTasks.length > 0) { const pendingTaskAlerts = getPendingTaskAlerts(storeTasks, calendars, acknowledgedKeys, now); for (const taskAlert of pendingTaskAlerts) { const key = buildAlertKey(taskAlert.taskId, taskAlert.alertId, taskAlert.fireTimeMs); if (shownKeysRef.current.has(key)) continue; shownKeysRef.current.add(key); acknowledgeAlert(key, taskAlert.fireTimeMs); if (calendarNotificationSound) { playNotificationSound(notificationSoundChoice); } const taskMsg = taskAlert.calendarName ? `${t('task_due')} · ${taskAlert.calendarName}` : t('task_due'); addToast({ type: 'info', title: taskAlert.task.title || t('alert_title'), message: taskMsg, duration: 15000, onClick: () => { window.location.href = `${getPathPrefix(locale)}/${locale}/calendar`; }, }); } } } catch { // Silently ignore alert evaluation errors } }, [ calendarNotificationsEnabled, calendarNotificationSound, notificationSoundChoice, isAuthenticated, events, calendars, acknowledgedAlerts, acknowledgeAlert, addToast, t, locale, enableCalendarTasks, storeTasks, ]); const proactiveFetch = useCallback(async () => { if (!client || !supportsCalendar || !calendarNotificationsEnabled || !isAuthenticated) return; const now = Date.now(); if (now - lastProactiveFetchRef.current < PROACTIVE_THROTTLE_MS) return; try { const start = new Date(now - 10 * 60 * 1000).toISOString(); const end = new Date(now + PROACTIVE_FETCH_HOURS * 60 * 60 * 1000).toISOString(); const fetched = await client.queryCalendarEvents({ after: start, before: end }); proactiveEventsRef.current = fetched; lastProactiveFetchRef.current = Date.now(); } catch { // Silently ignore proactive fetch errors } }, [client, supportsCalendar, calendarNotificationsEnabled, isAuthenticated]); useEffect(() => { if (!calendarNotificationsEnabled || !isAuthenticated) return; cleanupStaleAlerts(); proactiveFetch(); const timer = setTimeout(() => checkAlerts(), 500); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated, calendarNotificationsEnabled]); useEffect(() => { if (!calendarNotificationsEnabled || !isAuthenticated) return; const interval = setInterval(() => { proactiveFetch(); checkAlerts(); }, CHECK_INTERVAL_MS); return () => clearInterval(interval); }, [calendarNotificationsEnabled, isAuthenticated, checkAlerts, proactiveFetch]); useEffect(() => { if (!calendarNotificationsEnabled || !isAuthenticated) return; checkAlerts(); }, [events, calendarNotificationsEnabled, isAuthenticated, checkAlerts]); }