Files
pyramid/docs/matrix-sdk/07-push.md
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

7.6 KiB

07 — Push & Benachrichtigungen


Inhaltsverzeichnis


Pusher registrieren (FCM)

// 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

// 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

// 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<String, dynamic>('m.mentions')
    ?.tryGetList<String>('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

// 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)

// 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)
// 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

// Pro Raum
print(room.notificationCount);  // Ungelesene Nachrichten
print(room.highlightCount);     // Mentions / Highlights

// Gesamt
final totalUnread = client.rooms.fold<int>(
  0, (sum, r) => sum + r.notificationCount);
final totalHighlights = client.rooms.fold<int>(
  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);
}