implemented an E2E-encrypted Nextcloud push-v2 notification system with support for RSA decryption and signature verification; introduced an iOS Notification Service Extension and native AppDelegate handlers for Talk actions (inline reply and mark-as-read); replaced the legacy notification registration with a new lifecycle managing app passwords and secure keypair storage; added background message handling with tray synchronization and a test notification utility in the settings.

This commit is contained in:
2026-07-04 22:50:18 +02:00
parent 32f7c311bc
commit 74a2ddd17f
56 changed files with 2987 additions and 285 deletions
+28 -9
View File
@@ -12,7 +12,8 @@ import 'main.dart';
import 'model/data_cleaner.dart';
import 'notification/notification_controller.dart';
import 'notification/notification_tasks.dart';
import 'notification/notify_updater.dart';
import 'push/push_registration.dart';
import 'push/push_tap_router.dart';
import 'routing/app_routes.dart';
import 'share_intent/share_intent_listener.dart';
import 'state/app/modules/app_modules.dart';
@@ -85,6 +86,13 @@ class _AppState extends State<App> with WidgetsBindingObserver {
}
}
void _onPushTapPending() {
final token = PushTapRouter.pendingChatToken.value;
if (token == null || !mounted) return;
PushTapRouter.pendingChatToken.value = null;
NotificationTasks.navigateToTalk(context, chatToken: token);
}
Future<void> _handlePendingWidgetNavigation() async {
final pending = await WidgetNavigation.consumePendingTimetableTap();
if (!pending || !mounted) return;
@@ -165,22 +173,32 @@ class _AppState extends State<App> with WidgetsBindingObserver {
UpdateUserIndex.index();
// A refreshed FCM token invalidates the existing push subscription — the
// NC device identifier stays stable, so we simply re-register (NC first,
// then the proxy). Debounced so a burst of refreshes triggers one call.
if (context.read<SettingsCubit>().val().notificationSettings.enabled) {
void update() => NotifyUpdater.registerToServer();
_fcmTokenRefreshSub = FirebaseMessaging.instance.onTokenRefresh.listen(
(_) => update(),
);
update();
_fcmTokenRefreshSub = FirebaseMessaging.instance.onTokenRefresh.listen((
_,
) {
Debouncer.debounce(
'pushTokenRefresh',
const Duration(seconds: 3),
() => unawaited(PushRegistration().onTokenRefresh()),
);
});
}
// Android renders pushes locally, so a tap arrives via the local
// notifications callback (PushTapRouter) rather than onMessageOpenedApp.
PushTapRouter.pendingChatToken.addListener(_onPushTapPending);
_onMessageSub = FirebaseMessaging.onMessage.listen((message) {
if (!mounted) return;
NotificationController.onForegroundMessageHandler(message, context);
});
FirebaseMessaging.onBackgroundMessage(
NotificationController.onBackgroundMessageHandler,
);
// iOS delivers alert pushes (Connect direct pushes, and NC pushes rendered
// by the NSE) natively; a tap surfaces here.
_onMessageOpenedAppSub = FirebaseMessaging.onMessageOpenedApp.listen((
message,
) {
@@ -202,6 +220,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
_onMessageSub?.cancel();
_onMessageOpenedAppSub?.cancel();
_fcmTokenRefreshSub?.cancel();
PushTapRouter.pendingChatToken.removeListener(_onPushTapPending);
ShareIntentListener.pending.removeListener(_handlePendingShare);
ShareIntentListener.instance.detach();
Main.bottomNavigator.removeListener(_onTabControllerChanged);