import React, { useEffect, useRef, useState } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { StatusBar, Alert, AppState, AppStateStatus } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { theme } from './theme'; import TodayScreen from './screens/TodayScreen'; import CalendarScreen from './screens/CalendarScreen'; import * as Notifications from 'expo-notifications'; import { getEvents } from './api'; import { computeUpcomingReminders, EventForReminder } from './utils/recurrence'; const Tab = createBottomTabNavigator(); const WINDOW_MS = 2 * 60 * 1000; // 2 分钟容错窗口 Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldShowBanner: true, shouldShowList: true, shouldPlaySound: true, shouldSetBadge: false, }), }); export default function App() { const notifiedKeysRef = useRef>(new Set()); const scheduledNotificationIdsRef = useRef>(new Set()); const heartbeatTimerRef = useRef(null); const appStateRef = useRef(AppState.currentState); useEffect(() => { requestNotificationPermissions(); initializeNotifications(); // 监听应用状态变化 const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription.remove(); cleanup(); }; }, []); const requestNotificationPermissions = async () => { const { status } = await Notifications.requestPermissionsAsync(); if (status !== 'granted') { Alert.alert('提示', '需要通知权限才能发送提醒'); } }; const initializeNotifications = async () => { await loadAndScheduleReminders(); startReminderHeartbeat(); }; const handleAppStateChange = (nextAppState: AppStateStatus) => { if (appStateRef.current.match(/inactive|background/) && nextAppState === 'active') { // 应用从后台回到前台,重新检查提醒 loadAndScheduleReminders(); } appStateRef.current = nextAppState; }; const cleanup = () => { if (heartbeatTimerRef.current) { clearInterval(heartbeatTimerRef.current); } // 取消所有已安排的通知 scheduledNotificationIdsRef.current.forEach(async (id) => { try { await Notifications.cancelScheduledNotificationAsync(id); } catch (e) { console.error('取消通知失败:', e); } }); }; const loadAndScheduleReminders = async () => { try { const now = Date.now(); const next24h = now + 24 * 3600 * 1000; // 加载事件(覆盖当前月和前后各一个月) const currentDate = new Date(); const events = await getEvents(currentDate); // 取消之前的所有通知 scheduledNotificationIdsRef.current.forEach(async (id) => { try { await Notifications.cancelScheduledNotificationAsync(id); } catch (e) { console.error('取消通知失败:', e); } }); scheduledNotificationIdsRef.current.clear(); // 补偿通知:检查过去 2 分钟内到期的提醒 catchUpNotifications(events, now); // 安排未来 24 小时的通知 scheduleNotifications(events, now, next24h); } catch (e) { console.error('加载提醒失败:', e); } }; const catchUpNotifications = (events: EventForReminder[], now: number) => { const since = now - WINDOW_MS; events.forEach((e) => { if (e.completed || e.deleted) return; const windowReminders = computeUpcomingReminders(e, since, now); windowReminders.forEach((remindIso) => { const key = `${e.id}|${remindIso}`; if (!notifiedKeysRef.current.has(key)) { triggerNotify(e, remindIso); notifiedKeysRef.current.add(key); } }); }); }; const scheduleNotifications = async ( events: EventForReminder[], now: number, next24h: number ) => { for (const e of events) { if (e.completed || e.deleted) continue; const upcoming = computeUpcomingReminders(e, now, next24h); for (const remindIso of upcoming) { const ts = new Date(remindIso).getTime(); const delay = ts - now; if (delay > 0) { const key = `${e.id}|${remindIso}`; if (notifiedKeysRef.current.has(key)) continue; try { const notificationId = await Notifications.scheduleNotificationAsync({ content: { title: '提醒', body: e.title, sound: true, }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: delay / 1000, } as any, }); scheduledNotificationIdsRef.current.add(notificationId); console.log('已安排通知:', e.title, '在', new Date(ts).toLocaleString()); } catch (e) { console.error('安排通知失败:', e); } } } } }; const triggerNotify = (e: EventForReminder, remindIso: string) => { Notifications.scheduleNotificationAsync({ content: { title: '提醒', body: e.title, sound: true, }, trigger: null, // 立即触发 }); }; const startReminderHeartbeat = () => { if (heartbeatTimerRef.current) { clearInterval(heartbeatTimerRef.current); } // 每分钟检查一次,捕捉睡眠/时间漂移导致的错过提醒 heartbeatTimerRef.current = setInterval(async () => { const now = Date.now(); const since = now - WINDOW_MS; try { const events = await getEvents(new Date()); events.forEach((e: EventForReminder) => { if (e.completed || e.deleted) return; const windowReminders = computeUpcomingReminders(e, since, now); windowReminders.forEach((remindIso) => { const key = `${e.id}|${remindIso}`; if (!notifiedKeysRef.current.has(key)) { triggerNotify(e, remindIso); notifiedKeysRef.current.add(key); } }); }); } catch (e) { console.error('心跳检查失败:', e); } }, 60 * 1000); }; return ( ( ), }} /> ( ), }} /> ); }