# 07 — Push & Benachrichtigungen --- ## Inhaltsverzeichnis - [Pusher registrieren (FCM)](#pusher-registrieren-fcm) - [Pusher abrufen & verwalten](#pusher-abrufen--verwalten) - [Push Rules](#push-rules) - [onNotification Stream](#onnotification-stream) - [Lokale Benachrichtigungen (Android)](#lokale-benachrichtigungen-android) - [Benachrichtigungen: Pyramid-Architektur](#benachrichtigungen-pyramid-architektur) - [Notification Counts](#notification-counts) --- ## Pusher registrieren (FCM) ```dart // FCM-Token holen import 'package:firebase_messaging/firebase_messaging.dart'; final fcmToken = await FirebaseMessaging.instance.getToken(); // Pusher beim Homeserver registrieren await client.postPusher( Pusher( pushkey: fcmToken!, kind: 'http', appId: 'chat.pyramid.pyramid', // Unique App-ID (muss zu Push-Gateway passen) appDisplayName: 'Pyramid', deviceDisplayName: 'Mein Android', lang: 'de', data: PusherData( url: Uri.parse('https://push.example.org/_matrix/push/v1/notify'), format: 'event_id_only', // Empfohlen (nur Event-ID, nicht der Inhalt) ), ), append: true, // Nicht andere Pusher dieses Geräts löschen ); // Token-Refresh überwachen FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async { await client.postPusher( Pusher(pushkey: newToken, /* ... */ ), append: true, ); }); ``` --- ## Pusher abrufen & verwalten ```dart // Alle Pusher dieses Users final pushers = await client.getPushers() ?? []; for (final pusher in pushers) { print(pusher.appId); print(pusher.pushkey); print(pusher.data.url); } // Pusher entfernen await client.deletePusher( Pusher( pushkey: fcmToken, appId: 'chat.pyramid.pyramid', kind: 'http', // Pflichtfeld beim Löschen // ... ), ); // Beim Logout alle Pusher entfernen final myPushers = (await client.getPushers() ?? []) .where((p) => p.appId == 'chat.pyramid.pyramid'); for (final p in myPushers) { await client.deletePusher(p); } ``` --- ## Push Rules ```dart // Alle Push Rules abrufen final ruleset = await client.getPushRules(); // Globale Regeln for (final rule in ruleset.global.content ?? []) { print(rule.ruleId); print(rule.enabled); print(rule.actions); } // Kategorien der Regeln: // .override — höchste Priorität (z.B. DND-Modus) // .content — Inhalt-basiert (z.B. eigener Name erwähnt) // .room — Raum-spezifisch // .sender — Sender-spezifisch // .underride — niedrigste Priorität (Fallback) // Push Rule aktivieren/deaktivieren await client.setPushRuleEnabled( 'global', PushRuleKind.override, '.m.rule.master', // DND: stumm schalten false, // false = Regel deaktivieren ); // Eigene Push Rule erstellen (z.B. Raum stumm schalten) await client.setPushRule( 'global', PushRuleKind.room, '!roomId:server', // RuleId = RoomId für Raum-Regeln [PushRuleAction.dontNotify], ); // Push Rule löschen await client.deletePushRule( 'global', PushRuleKind.room, '!roomId:server', ); // Standard-Rules für Erwähnungen // Pyramid prüft lokal ob ein Event eine Mention enthält: final myId = client.userID!; final mentions = event.content .tryGetMap('m.mentions') ?.tryGetList('user_ids') ?? []; final isMentioned = mentions.contains(myId) || event.body.contains(myId); // Raum stumm schalten await client.setPushRule( 'global', PushRuleKind.override, '!roomId:server', [PushRuleAction.dontNotify], conditions: [ PushCondition(kind: 'event_match', key: 'room_id', pattern: '!roomId:server'), ], ); ``` --- ## onNotification Stream ```dart // Matrix-SDK emittiert Events die Benachrichtigungen auslösen sollen // (nach Push-Rule-Matching, entschlüsselt) client.onNotification.stream.listen((Event event) { final roomId = event.room.id; final room = client.getRoomById(roomId)!; final sender = room.unsafeGetUserFromMemoryOrFallback(event.senderId); final title = room.isDirectChat ? sender.displayName ?? event.senderId : '${sender.displayName} · ${room.getLocalizedDisplayname()}'; final body = event.body; // Lokale Benachrichtigung anzeigen showLocalNotification(title, body, roomId); }); ``` --- ## Lokale Benachrichtigungen (Android) ```dart // Siehe auch: lib/core/notification_service.dart und PushService.kt // flutter_local_notifications initialisieren final plugin = FlutterLocalNotificationsPlugin(); await plugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('@drawable/ic_notification'), ), onDidReceiveNotificationResponse: _handleNotificationTap, onDidReceiveBackgroundNotificationResponse: handleBackgroundResponse, ); // Notification-Kanal erstellen (Android 8+) await plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel( const AndroidNotificationChannel( 'pyramid_messages', 'Nachrichten', description: 'Neue Nachrichten', importance: Importance.high, ), ); // Benachrichtigung anzeigen (mit Reply-Action) await plugin.show( notifId, 'Absender', 'Nachrichtentext', NotificationDetails( android: AndroidNotificationDetails( 'pyramid_messages', 'Nachrichten', importance: Importance.high, priority: Priority.high, icon: '@drawable/ic_notification', actions: [ AndroidNotificationAction( 'reply', 'Antworten', inputs: [AndroidNotificationActionInput(label: 'Antworten…')], allowGeneratedReplies: true, showsUserInterface: false, // BroadcastReceiver-Pfad ), AndroidNotificationAction('read', 'Gelesen', showsUserInterface: false), ], ), ), payload: roomId, ); // Benachrichtigung schließen await plugin.cancel(notifId); ``` --- ## Benachrichtigungen: Pyramid-Architektur ``` FCM-Payload → PushService.kt (Kotlin, kein Dart-Overhead) │ ├─ App im Vordergrund? → Heartbeat-Check → Skip │ ├─ HTTP-Fetch: Absender + Raumname (ohne Entschlüsselung möglich) │ ├─ Native Notification anzeigen: "Neue Nachricht" │ └─ Reply-Action → getActivity(MainActivity) → onNewIntent │ └─ Flutter-Engine im Cache? → MethodChannel "decryptAndUpdateNotification" └─ Dart: decryptAndUpdateNotification() → showNativeNotification() [mit echtem Text] Reply-Flow: User tippt Antwort → Reply-PendingIntent (getActivity) → MainActivity.onNewIntent → MethodChannel "replyFromNotification" → Dart: room.sendTextEvent(text) → moveTaskToBack(true) ``` ```dart // Heartbeat (App aktiv?) setzen: void _pingHeartbeat() { SharedPreferences.getInstance().then( (p) => p.setInt('notif_app_heartbeat', DateTime.now().millisecondsSinceEpoch), ); } // Heartbeat löschen (App im Hintergrund): void setAppForeground(bool value) { if (!value) { SharedPreferences.getInstance() .then((p) => p.setInt('notif_app_heartbeat', 0)); } } ``` --- ## Notification Counts ```dart // Pro Raum print(room.notificationCount); // Ungelesene Nachrichten print(room.highlightCount); // Mentions / Highlights // Gesamt final totalUnread = client.rooms.fold( 0, (sum, r) => sum + r.notificationCount); final totalHighlights = client.rooms.fold( 0, (sum, r) => sum + r.highlightCount); // Badge für Windows Taskbar import 'package:windows_taskbar/windows_taskbar.dart'; if (totalUnread > 0) { WindowsTaskbar.setProgressMode(TaskbarProgressMode.error); } else { WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } ```