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
+30 -2
View File
@@ -25,6 +25,10 @@ import 'app.dart';
import 'background/widget_background_task.dart';
import 'firebase_options.dart';
import 'model/account_data.dart';
import 'notification/notification_service.dart';
import 'push/push_message_handler.dart';
import 'push/push_registration.dart';
import 'push/push_renderer.dart';
import 'routing/app_routes.dart';
import 'share_intent/share_intent_listener.dart';
import 'state/app/modules/account/bloc/account_bloc.dart';
@@ -87,6 +91,13 @@ Future<void> main() async {
await Future.wait(initialisationTasks);
log('app initialisation done!');
// Local notifications: init the plugin (with tap/action callbacks) and the
// Android channels, then register the FCM background isolate handler that
// decrypts and renders Nextcloud pushes while the app is not in foreground.
await NotificationService().initializeNotifications();
await PushRenderer.ensureChannels();
FirebaseMessaging.onBackgroundMessage(PushMessageHandler.onBackgroundMessage);
// Wire up the home-screen widget bridge before runApp so any widget render
// triggered during startup hits initialised native storage.
await WidgetSync.ensureInitialized();
@@ -207,8 +218,14 @@ class _MainState extends State<Main> {
_scheduleSessionValidation(accountBloc);
// Cold start while already logged in: the account status doesn't
// change, so the loggedIn listener below never fires — refresh
// capabilities here.
unawaited(context.read<CapabilitiesCubit>().load());
// capabilities here, then self-heal the push registration.
final settingsCubit = context.read<SettingsCubit>();
unawaited(
context.read<CapabilitiesCubit>().load().then((_) {
if (!mounted) return;
_syncPush(settingsCubit, context.read<CapabilitiesCubit>());
}),
);
unawaited(context.read<NextcloudCapabilitiesCubit>().load());
}
});
@@ -225,6 +242,17 @@ class _MainState extends State<Main> {
unawaited(ListFilesCache.prefetchRootListing());
}
/// Registers/self-heals the push subscription when push is user-enabled and
/// the backend advertises the capability. Fire-and-forget.
void _syncPush(SettingsCubit settings, CapabilitiesCubit capabilities) {
unawaited(
PushRegistration.syncSubscription(
enabled: settings.val().notificationSettings.enabled,
capable: capabilities.canReceivePushNotifications,
),
);
}
void _scheduleSessionValidation(AccountBloc accountBloc) {
unawaited(
SessionValidator.probeStored(