fix(apps): 修复通知点击不显示ReminderOverlay和日历编辑后不刷新问题

- AppDelegate: 只存储payload字段而非整个userInfo字典
- LocalNotificationService: 移除旧的取消/稍后提醒action按钮配置
- ReminderNotificationCallbacks: 添加onNotificationPayloadReceived静态回调
- IOSNotificationPayloadBridge: 添加setPendingPayload方法
- main.dart: 设置onNotificationPayloadReceived触发ReminderOverlay显示,添加WidgetsBindingObserver处理后台恢复
- CalendarEventDetailScreen: 编辑保存后正确传递刷新信号给日视图
This commit is contained in:
qzl
2026-03-20 19:34:06 +08:00
parent fcf98b1142
commit 20b9e70e84
6 changed files with 62 additions and 78 deletions
+2 -3
View File
@@ -25,9 +25,8 @@ import UserNotifications
withCompletionHandler completionHandler: @escaping () -> Void withCompletionHandler completionHandler: @escaping () -> Void
) { ) {
let userInfo = response.notification.request.content.userInfo let userInfo = response.notification.request.content.userInfo
if let jsonData = try? JSONSerialization.data(withJSONObject: userInfo, options: []), if let payloadString = userInfo["payload"] as? String {
let jsonString = String(data: jsonData, encoding: .utf8) { UserDefaults.standard.set(payloadString, forKey: "pending_notification_payload")
UserDefaults.standard.set(jsonString, forKey: "pending_notification_payload")
} }
completionHandler() completionHandler()
} }
@@ -21,6 +21,10 @@ class IOSNotificationPayloadBridge {
} }
} }
Future<void> setPendingPayload(ReminderPayload payload) async {
await _prefs.setString(_key, jsonEncode(payload.toJson()));
}
Future<void> clearPendingPayload() async { Future<void> clearPendingPayload() async {
await _prefs.remove(_key); await _prefs.remove(_key);
} }
@@ -7,31 +7,15 @@ import 'package:timezone/timezone.dart' as tz;
import 'reminder_notification_callbacks.dart'; import 'reminder_notification_callbacks.dart';
import '../../features/calendar/data/models/schedule_item_model.dart'; import '../../features/calendar/data/models/schedule_item_model.dart';
import '../../features/calendar/reminders/models/reminder_action.dart';
import '../../features/calendar/reminders/models/reminder_payload.dart'; import '../../features/calendar/reminders/models/reminder_payload.dart';
typedef ReminderNotificationActionHandler =
Future<void> Function({
required ReminderAction action,
required ReminderPayload payload,
});
class LocalNotificationService { class LocalNotificationService {
static const String _iosCategoryId = 'calendar_reminder_v2';
static const String _actionCancel = 'cancel';
static const String _actionSnooze = 'snooze10m';
final FlutterLocalNotificationsPlugin _plugin; final FlutterLocalNotificationsPlugin _plugin;
bool _initialized = false; bool _initialized = false;
ReminderNotificationActionHandler? _actionHandler;
LocalNotificationService({FlutterLocalNotificationsPlugin? plugin}) LocalNotificationService({FlutterLocalNotificationsPlugin? plugin})
: _plugin = plugin ?? FlutterLocalNotificationsPlugin(); : _plugin = plugin ?? FlutterLocalNotificationsPlugin();
void bindActionHandler(ReminderNotificationActionHandler handler) {
_actionHandler = handler;
}
Future<void> initialize() async { Future<void> initialize() async {
if (_initialized) { if (_initialized) {
return; return;
@@ -39,25 +23,10 @@ class LocalNotificationService {
tz_data.initializeTimeZones(); tz_data.initializeTimeZones();
const android = AndroidInitializationSettings('@mipmap/ic_launcher'); const android = AndroidInitializationSettings('@mipmap/ic_launcher');
final ios = DarwinInitializationSettings( const ios = DarwinInitializationSettings(
requestAlertPermission: false, requestAlertPermission: false,
requestBadgePermission: false, requestBadgePermission: false,
requestSoundPermission: false, requestSoundPermission: false,
notificationCategories: [
DarwinNotificationCategory(
_iosCategoryId,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain(_actionCancel, '取消'),
DarwinNotificationAction.plain(
_actionSnooze,
'稍后提醒',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
],
),
],
); );
final settings = InitializationSettings(android: android, iOS: ios); final settings = InitializationSettings(android: android, iOS: ios);
@@ -190,20 +159,11 @@ class LocalNotificationService {
timeoutAfter: 30000, timeoutAfter: 30000,
autoCancel: true, autoCancel: true,
groupKey: _getGroupKey(fireAt), groupKey: _getGroupKey(fireAt),
actions: <AndroidNotificationAction>[
AndroidNotificationAction(_actionCancel, '取消'),
AndroidNotificationAction(
_actionSnooze,
'稍后提醒',
showsUserInterface: true,
),
],
), ),
iOS: DarwinNotificationDetails( iOS: DarwinNotificationDetails(
presentAlert: true, presentAlert: true,
presentSound: true, presentSound: true,
presentBadge: true, presentBadge: true,
categoryIdentifier: _iosCategoryId,
threadIdentifier: _getThreadIdentifier(fireAt), threadIdentifier: _getThreadIdentifier(fireAt),
), ),
); );
@@ -341,23 +301,6 @@ class LocalNotificationService {
return; return;
} }
final actionId = response.actionId; ReminderNotificationCallbacks.onNotificationPayloadReceived?.call(payload);
ReminderAction? action;
if (actionId == _actionCancel) {
action = ReminderAction.archive;
} else if (actionId == _actionSnooze) {
action = ReminderAction.snooze10m;
}
if (action == null) {
return;
}
final handler = _actionHandler;
if (handler == null) {
return;
}
await handler(action: action, payload: payload);
} }
} }
@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../features/calendar/reminders/models/reminder_payload.dart';
typedef ReminderNotificationResponseHandler = typedef ReminderNotificationResponseHandler =
Future<void> Function(NotificationResponse response); Future<void> Function(NotificationResponse response);
@@ -13,6 +15,7 @@ class ReminderNotificationCallbacks {
'calendar_reminder_pending_notification_responses_v1'; 'calendar_reminder_pending_notification_responses_v1';
static ReminderNotificationResponseHandler? _responseHandler; static ReminderNotificationResponseHandler? _responseHandler;
static Future<void> _pendingStorageLock = Future<void>.value(); static Future<void> _pendingStorageLock = Future<void>.value();
static void Function(ReminderPayload)? onNotificationPayloadReceived;
@visibleForTesting @visibleForTesting
static Future<void> resetForTest() async { static Future<void> resetForTest() async {
@@ -218,15 +218,19 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
return event.startAt.isBefore(now); return event.startAt.isBefore(now);
} }
void _handleHeaderAction( Future<void> _handleHeaderAction(
_CalendarHeaderAction action, _CalendarHeaderAction action,
ScheduleItemModel event, ScheduleItemModel event,
) { ) async {
switch (action) { switch (action) {
case _CalendarHeaderAction.edit: case _CalendarHeaderAction.edit:
context.push(AppRoutes.calendarEventEdit(event.id)).then((_) { final changed = await context.push<bool>(
AppRoutes.calendarEventEdit(event.id),
);
if (changed == true) {
_loadEvent(); _loadEvent();
}); if (mounted) context.pop(true);
}
return; return;
case _CalendarHeaderAction.delete: case _CalendarHeaderAction.delete:
_showDeleteConfirmation(); _showDeleteConfirmation();
+43 -12
View File
@@ -18,7 +18,6 @@ import 'features/auth/presentation/bloc/auth_event.dart';
import 'features/auth/presentation/bloc/auth_state.dart'; import 'features/auth/presentation/bloc/auth_state.dart';
import 'features/calendar/data/services/calendar_service.dart'; import 'features/calendar/data/services/calendar_service.dart';
import 'features/calendar/data/services/calendar_repository.dart'; import 'features/calendar/data/services/calendar_repository.dart';
import 'features/calendar/reminders/reminder_action_executor.dart';
import 'features/calendar/reminders/reminder_queue_manager.dart'; import 'features/calendar/reminders/reminder_queue_manager.dart';
import 'features/calendar/reminders/ui/reminder_overlay.dart'; import 'features/calendar/reminders/ui/reminder_overlay.dart';
import 'features/calendar/ui/calendar_state_manager.dart'; import 'features/calendar/ui/calendar_state_manager.dart';
@@ -32,15 +31,6 @@ void main() async {
await AppConstants.init(); await AppConstants.init();
final rootNavigatorKey = GlobalKey<NavigatorState>(); final rootNavigatorKey = GlobalKey<NavigatorState>();
sl<LocalNotificationService>().bindActionHandler(({
required action,
required payload,
}) {
return sl<ReminderActionExecutor>().handleAction(
action: action,
payload: payload,
);
});
await sl<LocalNotificationService>().initialize(); await sl<LocalNotificationService>().initialize();
final authBloc = sl<AuthBloc>(); final authBloc = sl<AuthBloc>();
@@ -74,8 +64,23 @@ void main() async {
await payloadBridge.clearPendingPayload(); await payloadBridge.clearPendingPayload();
} }
final linksyAppKey = GlobalKey();
ReminderNotificationCallbacks.onNotificationPayloadReceived =
(payload) async {
await payloadBridge.setPendingPayload(payload);
queueManager.enqueueFromClick(payload);
WidgetsBinding.instance.addPostFrameCallback((_) {
final state = linksyAppKey.currentState;
if (state != null && state is _LinksyAppState) {
state.showReminderOverlayFromNotification();
}
});
};
runApp( runApp(
LinksyApp( LinksyApp(
key: linksyAppKey,
authBloc: authBloc, authBloc: authBloc,
rootNavigatorKey: rootNavigatorKey, rootNavigatorKey: rootNavigatorKey,
sessionBootstrapper: AuthSessionBootstrapper( sessionBootstrapper: AuthSessionBootstrapper(
@@ -116,18 +121,39 @@ class LinksyApp extends StatefulWidget {
State<LinksyApp> createState() => _LinksyAppState(); State<LinksyApp> createState() => _LinksyAppState();
} }
class _LinksyAppState extends State<LinksyApp> { class _LinksyAppState extends State<LinksyApp> with WidgetsBindingObserver {
OverlayEntry? _reminderOverlay; OverlayEntry? _reminderOverlay;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_checkAndShowReminderOverlay(); _checkAndShowReminderOverlay();
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkAndShowReminderOverlay();
}
}
Future<void> _checkAndShowReminderOverlay() async { Future<void> _checkAndShowReminderOverlay() async {
if (widget.queueManager.currentPayload != null) { if (widget.queueManager.currentPayload != null) {
_showReminderOverlay(); _showReminderOverlay();
return;
}
final pendingPayload = await widget.payloadBridge.getPendingPayload();
if (pendingPayload != null) {
widget.queueManager.enqueueFromClick(pendingPayload);
await widget.payloadBridge.clearPendingPayload();
_showReminderOverlay();
} }
} }
@@ -147,10 +173,15 @@ class _LinksyAppState extends State<LinksyApp> {
), ),
), ),
); );
Overlay.of(context).insert(_reminderOverlay!); Overlay.of(context).insert(_reminderOverlay!);
} }
void showReminderOverlayFromNotification() {
if (widget.queueManager.currentPayload != null) {
_showReminderOverlay();
}
}
void _onReminderComplete() { void _onReminderComplete() {
_reminderOverlay?.remove(); _reminderOverlay?.remove();
_reminderOverlay = null; _reminderOverlay = null;