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
+16 -23
View File
@@ -4,44 +4,36 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../push/push_message_handler.dart';
import '../state/app/modules/chat/bloc/chat_bloc.dart';
import '../widget/debug/debug_tile.dart';
import '../widget/debug/json_viewer.dart';
import '../widget/info_dialog.dart';
import 'notification_tasks.dart';
// `vm:entry-point` keeps this alive through AOT tree-shaking — the FCM
// background isolate looks the class up by name from native code.
@pragma('vm:entry-point')
/// Bridges FCM lifecycle callbacks to the push pipeline. Background messages are
/// handled directly by [PushMessageHandler.onBackgroundMessage]; this class
/// covers the foreground and app-opened paths where a [BuildContext] is
/// available.
class NotificationController {
@pragma('vm:entry-point')
static Future<void> onBackgroundMessageHandler(RemoteMessage message) async {
NotificationTasks.updateBadgeCount(message);
}
static Future<void> onForegroundMessageHandler(
RemoteMessage message,
BuildContext context,
) async {
final pushToken = _extractChatToken(message);
final chatBloc = context.read<ChatBloc>();
// hasOpenChat, not currentToken: currentToken sticks around after
// leaveChat so didPopNext can re-claim a stacked chat.
final activeToken = chatBloc.state.data?.currentToken ?? '';
final chatIsOpen =
chatBloc.hasOpenChat &&
pushToken != null &&
pushToken.isNotEmpty &&
pushToken == activeToken;
NotificationTasks.updateBadgeCount(message);
if (chatIsOpen) {
// Long-poll handles the message; just dismiss any stray tray entry.
unawaited(NotificationTasks.clearNotificationsForChat(pushToken));
return;
}
final openChatToken = chatBloc.hasOpenChat
? (chatBloc.state.data?.currentToken ?? '')
: null;
await PushMessageHandler().handle(
message,
foreground: true,
openChatToken: openChatToken,
);
await NotificationTasks.refreshBadge();
if (!context.mounted) return;
NotificationTasks.updateProviders(context);
}
@@ -54,6 +46,7 @@ class NotificationController {
chatToken: _extractChatToken(message),
);
NotificationTasks.updateProviders(context);
unawaited(NotificationTasks.refreshBadge());
DebugTile(context).run(() {
InfoDialog.show(