Skip to content

✨(valarm) implement reminders and VALARM#67

Open
sylvinus wants to merge 1 commit into
mainfrom
valarm
Open

✨(valarm) implement reminders and VALARM#67
sylvinus wants to merge 1 commit into
mainfrom
valarm

Conversation

@sylvinus

@sylvinus sylvinus commented Jun 22, 2026

Copy link
Copy Markdown
Member

Fixes #22

Summary by CodeRabbit

  • New Features
    • Event reminders can now be configured and managed directly in event details
    • Upcoming event reminders trigger browser notifications to keep you informed of important events
    • Browser notification permission is automatically requested when you add reminders to events
    • Reminders persist across page reloads and browser tabs to prevent duplicate notifications
    • Events with existing reminders display the reminders section expanded by default

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements end-to-end calendar event reminder delivery. Adds pure ICS alarm computation utilities, a browser Notifications API wrapper, a localStorage deduplication store, and a polling hook (useEventReminders) that fires browser and in-app notifications for due alarms. A headless NotificationManager component mounts the hook in the authenticated route tree. EventModal gains a "reminders" section pill wired to RemindersSection, with notification permission requested on save when alarms are present.

Changes

VALARM Reminder Delivery

Layer / File(s) Summary
Reminder computation utilities and tests
src/frontend/src/features/calendar/notifications/reminderUtils.ts, src/frontend/src/features/calendar/notifications/__tests__/reminderUtils.test.ts
Pure ICS alarm helpers (getEventInstantMs, getAlarmTriggerMs, collectDueReminders, buildAlarmMap, withMasterAlarms, DueReminder, reminderKey) with a full Jest suite covering all-day events, relative/absolute triggers, backfill windows, EMAIL alarm filtering, and recurring-instance alarm propagation.
Browser Notification API wrapper
src/frontend/src/features/calendar/notifications/browserNotifications.ts
SSR-safe module adding isNotificationSupported, ensureNotificationPermission (guards and swallows errors), and showBrowserNotification (permission-gated with fixed icon and optional deduplication tag).
localStorage fired-reminder deduplication store
src/frontend/src/features/calendar/notifications/firedReminderStore.ts
loadFiredReminders reads and prunes stale entries from localStorage; saveFiredReminders persists the current fired-key map. Both are best-effort with error swallowing.
useEventReminders polling hook and NotificationManager wiring
src/frontend/src/features/calendar/notifications/useEventReminders.tsx, src/frontend/src/features/calendar/notifications/NotificationManager.tsx, src/frontend/src/routes/index.tsx
Polling hook fetches expanded and master events, merges master VALARM alarms onto recurring instances, evaluates due reminders against a backfill window, fires browser notifications and in-app toasts, and persists the fired set. NotificationManager mounts the hook headlessly and is inserted into the authenticated calendar route tree.
EventModal RemindersSection pill, save flow, form type, and i18n
src/frontend/src/features/calendar/components/scheduler/types.ts, src/frontend/src/features/calendar/components/scheduler/EventModal.tsx, src/frontend/src/features/calendar/components/scheduler/hooks/useEventForm.ts, src/frontend/src/features/i18n/translations.json
EventFormSectionId gains "reminders"; EventModal adds a Bell pill and conditionally renders RemindersSection; doSave triggers ensureNotificationPermission when alarms exist; useEventForm auto-expands the reminders section for events with existing alarms; en/fr/nl i18n gain untitled and notificationBody keys.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant EventModal
  participant ensureNotificationPermission
  participant useEventReminders
  participant CalendarAPI
  participant showBrowserNotification
  participant Toast
  participant firedReminderStore

  User->>EventModal: Save event with alarms
  EventModal->>ensureNotificationPermission: request permission (fire-and-forget)

  Note over useEventReminders: Polling loop (fetch interval)
  useEventReminders->>CalendarAPI: fetch expanded events
  useEventReminders->>CalendarAPI: fetch master events
  useEventReminders->>useEventReminders: withMasterAlarms merge
  useEventReminders->>firedReminderStore: loadFiredReminders
  useEventReminders->>useEventReminders: collectDueReminders

  Note over useEventReminders: Tick interval
  useEventReminders->>showBrowserNotification: fire(title, body, tag)
  useEventReminders->>Toast: fire(title, body)
  useEventReminders->>firedReminderStore: saveFiredReminders
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'implement reminders and VALARM' clearly summarizes the main change: adding reminder and VALARM support to the calendar system.
Linked Issues check ✅ Passed The PR successfully addresses both objectives from issue #22: wiring RemindersSection into EventModal and implementing web notification delivery mechanisms.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives. The additions cover UI integration, notification infrastructure, reminder utilities, and internationalization—all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/src/features/calendar/notifications/useEventReminders.tsx`:
- Around line 48-50: The useEffect hook that initializes firedRef.current with
loadFiredReminders(Date.now()) only runs once on component mount, causing
concurrent tabs to have stale fired reminder data that isn't synchronized before
the dedup checks occur around line 84. To fix this, add cross-tab
synchronization by listening to storage events or implementing a mechanism to
refresh firedRef.current whenever the reminder state might have been updated in
another tab. Before performing the dedup checks, call loadFiredReminders again
to ensure firedRef reflects the current state across all tabs, preventing
duplicate notifications from being fired for the same reminder.
- Around line 92-148: Replace the manual polling implementation in
useEventReminders with React Query to align with project standards. Remove the
useCallback for fetchUpcoming and the useEffect that manages setInterval for
fetchTimer and tickTimer, along with the cancelled flag logic. Instead,
implement a useQuery hook that wraps the event fetching logic, using the
refetchInterval option to handle polling at the FETCH_INTERVAL_MS interval, and
remove references to upcomingRef.current by accessing the query data directly
from the useQuery return value. This will delegate interval management, caching,
and lifecycle handling to React Query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb253b2d-9bde-4ad4-a8a6-1db74bfa9269

📥 Commits

Reviewing files that changed from the base of the PR and between d60e63c and 9f6119e.

📒 Files selected for processing (11)
  • src/frontend/src/features/calendar/components/scheduler/EventModal.tsx
  • src/frontend/src/features/calendar/components/scheduler/hooks/useEventForm.ts
  • src/frontend/src/features/calendar/components/scheduler/types.ts
  • src/frontend/src/features/calendar/notifications/NotificationManager.tsx
  • src/frontend/src/features/calendar/notifications/__tests__/reminderUtils.test.ts
  • src/frontend/src/features/calendar/notifications/browserNotifications.ts
  • src/frontend/src/features/calendar/notifications/firedReminderStore.ts
  • src/frontend/src/features/calendar/notifications/reminderUtils.ts
  • src/frontend/src/features/calendar/notifications/useEventReminders.tsx
  • src/frontend/src/features/i18n/translations.json
  • src/frontend/src/routes/index.tsx

Comment on lines +48 to +50
useEffect(() => {
firedRef.current = loadFiredReminders(Date.now());
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Synchronize fired-reminder state across tabs before dedup checks.

Line 48-50 loads firedRef only once, so a concurrently open tab won’t see newly fired keys before Line 84 and can fire duplicate notifications for the same reminder.

Suggested fix
 useEffect(() => {
   firedRef.current = loadFiredReminders(Date.now());
+  const onStorage = (e: StorageEvent) => {
+    if (e.key === "calendar-fired-reminders") {
+      firedRef.current = loadFiredReminders(Date.now());
+    }
+  };
+  window.addEventListener("storage", onStorage);
+  return () => window.removeEventListener("storage", onStorage);
 }, []);

Also applies to: 79-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/features/calendar/notifications/useEventReminders.tsx`
around lines 48 - 50, The useEffect hook that initializes firedRef.current with
loadFiredReminders(Date.now()) only runs once on component mount, causing
concurrent tabs to have stale fired reminder data that isn't synchronized before
the dedup checks occur around line 84. To fix this, add cross-tab
synchronization by listening to storage events or implementing a mechanism to
refresh firedRef.current whenever the reminder state might have been updated in
another tab. Before performing the dedup checks, call loadFiredReminders again
to ensure firedRef reflects the current state across all tabs, preventing
duplicate notifications from being fired for the same reminder.

Comment on lines +92 to +148
const fetchUpcoming = useCallback(async () => {
const urls = [...visibleCalendarUrls];
if (urls.length === 0) {
upcomingRef.current = [];
return;
}
const now = Date.now();
const timeRange = {
start: new Date(now - BACKFILL_MS),
end: new Date(now + LOOKAHEAD_MS),
};

const fetchCalendar = async (url: string) => {
// Expanded gives correct per-occurrence start times; non-expanded
// preserves the master's VALARM (sabre/vobject drops alarms from
// expanded instances). Merge the two so recurring reminders fire.
const [expanded, masters] = await Promise.all([
caldavService
.fetchEvents(url, { timeRange, expand: true })
.then((res) => (res.success && res.data ? res.data : []))
.catch(() => []),
caldavService
.fetchEvents(url, { timeRange, expand: false })
.then((res) => (res.success && res.data ? res.data : []))
.catch(() => []),
]);

const alarmsByUid = buildAlarmMap(masters.flatMap((ce) => ce.data.events ?? []));
const instances = expanded.flatMap((ce) =>
(ce.data.events ?? []).map((event) => ({ event, calendarUrl: ce.calendarUrl })),
);
return withMasterAlarms(instances, alarmsByUid);
};

const results = await Promise.all(urls.map(fetchCalendar));
upcomingRef.current = results.flat().filter(({ event }) => event.alarms?.length);
}, [caldavService, visibleCalendarUrls]);

useEffect(() => {
if (!isConnected) return;
let cancelled = false;

const refetchAndEvaluate = async () => {
await fetchUpcoming();
if (!cancelled) tick();
};

void refetchAndEvaluate();
const fetchTimer = setInterval(() => void refetchAndEvaluate(), FETCH_INTERVAL_MS);
const tickTimer = setInterval(tick, TICK_INTERVAL_MS);

return () => {
cancelled = true;
clearInterval(fetchTimer);
clearInterval(tickTimer);
};
}, [isConnected, fetchUpcoming, tick]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read its full content
wc -l src/frontend/src/features/calendar/notifications/useEventReminders.tsx

Repository: suitenumerique/calendars

Length of output: 143


🏁 Script executed:

# Check for React Query usage in the frontend codebase
rg -t ts -t tsx "useQuery|useMutation|useInfiniteQuery|React Query" src/frontend/src --max-count=20

Repository: suitenumerique/calendars

Length of output: 96


🏁 Script executed:

# Search for setInterval usage in hooks to understand polling patterns
rg -t ts -t tsx "setInterval" src/frontend/src

Repository: suitenumerique/calendars

Length of output: 96


🏁 Script executed:

# Check if React Query is installed as a dependency
cat package.json | grep -i "react-query\|`@tanstack`"

Repository: suitenumerique/calendars

Length of output: 113


🏁 Script executed:

# Find package.json files
fd package.json

Repository: suitenumerique/calendars

Length of output: 115


🏁 Script executed:

# Try searching for React Query with broader patterns
rg "react-query|`@tanstack`|useQuery|useMutation" src/frontend --max-count=20

Repository: suitenumerique/calendars

Length of output: 7728


🏁 Script executed:

# Read the full file
cat -n src/frontend/src/features/calendar/notifications/useEventReminders.tsx

Repository: suitenumerique/calendars

Length of output: 6753


🏁 Script executed:

# Search for setInterval usage more broadly
rg "setInterval" src/frontend

Repository: suitenumerique/calendars

Length of output: 357


Migrate this server-state polling to React Query to align with project standards.

This hook manually manages fetch lifecycle, polling, and cache state for CalDAV events using setInterval. Per project guidelines for src/frontend/src/**/*.{ts,tsx}, implement this using React Query to avoid duplicating query orchestration logic that the library provides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/features/calendar/notifications/useEventReminders.tsx`
around lines 92 - 148, Replace the manual polling implementation in
useEventReminders with React Query to align with project standards. Remove the
useCallback for fetchUpcoming and the useEffect that manages setInterval for
fetchTimer and tickTimer, along with the cancelled flag logic. Instead,
implement a useQuery hook that wraps the event fetching logic, using the
refetchInterval option to handle polling at the FETCH_INTERVAL_MS interval, and
remove references to upcomingRef.current by accessing the query data directly
from the useQuery return value. This will delegate interval management, caching,
and lifecycle handling to React Query.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for VALARM

1 participant