2eeae9a27b
Update notification calculation to use arrival buffer Add advanced settings toggle for arrival buffer and transport options Fix reverse geocode call in SettingsScreen
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import * as Notifications from 'expo-notifications';
|
|
import { SchedulableTriggerInputTypes } from 'expo-notifications';
|
|
import type { Event, Journey, ReminderSettings } from '@timetoleave/core';
|
|
|
|
// Register for push notification permissions
|
|
Notifications.setNotificationHandler({
|
|
handleNotification: async () => ({
|
|
shouldShowAlert: true,
|
|
shouldPlaySound: true,
|
|
shouldSetBadge: false,
|
|
shouldShowBanner: true,
|
|
shouldShowList: true,
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* Calculate leave-by time from event time and journey data.
|
|
* Uses earliest real departure time if journeys exist, otherwise event time minus buffer.
|
|
*
|
|
* @param event - The event to calculate leave-by time for
|
|
* @param journeys - Journey data for this event
|
|
* @param arrivalBufferMinutes - How many minutes before the event to arrive
|
|
* @param bufferMinutes - How many minutes before leaving to be reminded
|
|
*/
|
|
export function calculateLeaveByTime(
|
|
event: Event,
|
|
journeys: Journey[],
|
|
arrivalBufferMinutes: number,
|
|
bufferMinutes: number
|
|
): Date {
|
|
// Calculate target arrival time (event time minus arrival buffer)
|
|
const targetArrivalTime = new Date(event.eventTime);
|
|
targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes);
|
|
|
|
// If we have journeys, use the earliest non-cancelled real departure
|
|
if (journeys.length > 0) {
|
|
const best = journeys
|
|
.filter((j) => !j.cancelled)
|
|
.sort((a, b) => a.rD.getTime() - b.rD.getTime())[0];
|
|
|
|
if (best) {
|
|
// Leave by time = target arrival time - buffer minutes
|
|
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
|
}
|
|
}
|
|
|
|
// Fallback: event time minus arrival buffer minus buffer (no journey data)
|
|
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
|
}
|
|
|
|
/**
|
|
* Schedule notifications for an event
|
|
*
|
|
* @param event - The event to schedule notifications for
|
|
* @param journeys - Journey data for this event (optional)
|
|
* @param settings - Notification settings
|
|
*/
|
|
export async function scheduleNotificationsForEvent(
|
|
event: Event,
|
|
journeys: Journey[] = [],
|
|
settings: ReminderSettings
|
|
): Promise<void> {
|
|
// Don't schedule if notifications are disabled
|
|
if (!settings.enabled) {
|
|
return;
|
|
}
|
|
|
|
// Calculate leave-by time (when user should actually leave)
|
|
const leaveByTime = calculateLeaveByTime(event, journeys, settings.arrivalBufferMinutes, settings.bufferMinutes);
|
|
|
|
// Cancel existing notifications for this event - cancel one by one
|
|
const existing = await Notifications.getAllScheduledNotificationsAsync();
|
|
const toCancel = existing.filter(n => n.content.data?.eventId === event.id);
|
|
if (toCancel.length > 0) {
|
|
for (const notif of toCancel) {
|
|
await Notifications.cancelScheduledNotificationAsync(notif.identifier);
|
|
}
|
|
}
|
|
|
|
// Default reminders: 30min, 10min, and at leave-by time
|
|
// But respect the buffer time - we want reminders relative to when they should leave
|
|
const defaultReminders = [30, 10, 0];
|
|
|
|
// Schedule notifications
|
|
for (const minutesBefore of defaultReminders) {
|
|
const triggerTime = new Date(leaveByTime.getTime() - minutesBefore * 60 * 1000);
|
|
|
|
// Skip if trigger time is in the past
|
|
if (triggerTime <= new Date()) {
|
|
continue;
|
|
}
|
|
|
|
// Skip if this would be before the event actually starts (add some safety margin)
|
|
if (triggerTime < new Date(event.eventTime.getTime() - 2 * 60 * 60 * 1000)) {
|
|
continue;
|
|
}
|
|
|
|
// Use Date trigger with time property
|
|
await Notifications.scheduleNotificationAsync({
|
|
content: {
|
|
title: `🚆 ${event.title}`,
|
|
body: minutesBefore === 0
|
|
? 'Zeit zu gehen!'
|
|
: `${minutesBefore} Minuten bis du losmusst`,
|
|
data: { eventId: event.id },
|
|
},
|
|
trigger: { type: SchedulableTriggerInputTypes.DATE, date: triggerTime },
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reschedule notifications for all events
|
|
* Use this when origin station changes or notification settings are updated
|
|
*/
|
|
export async function rescheduleAllNotifications(
|
|
events: Event[],
|
|
journeysMap: Record<string, Journey[]>, // eventId -> journeys
|
|
settings: ReminderSettings
|
|
): Promise<void> {
|
|
// Cancel ALL existing notifications first
|
|
await Notifications.cancelAllScheduledNotificationsAsync();
|
|
|
|
// Schedule new notifications for each event
|
|
for (const event of events) {
|
|
const eventJourneys = journeysMap[event.id] || [];
|
|
await scheduleNotificationsForEvent(event, eventJourneys, settings);
|
|
}
|
|
}
|
|
|
|
// Request permissions if not already granted
|
|
let permissionsRequested = false;
|
|
|
|
export async function requestNotificationPermissions(): Promise<boolean> {
|
|
if (permissionsRequested) {
|
|
return true;
|
|
}
|
|
|
|
permissionsRequested = true;
|
|
const { status } = await Notifications.requestPermissionsAsync();
|
|
return status === 'granted';
|
|
}
|
|
|
|
// Request permissions automatically when app starts (for Android)
|
|
// This is called in App.tsx
|
|
|
|
export function setupNotifications() {
|
|
Notifications.setNotificationHandler({
|
|
handleNotification: async () => ({
|
|
shouldShowAlert: true,
|
|
shouldPlaySound: true,
|
|
shouldSetBadge: false,
|
|
shouldShowBanner: true,
|
|
shouldShowList: true,
|
|
}),
|
|
});
|
|
}
|