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
This commit is contained in:
Bernd Steckmeister
2026-07-03 05:47:18 +02:00
parent d706ace35f
commit 25ed765a03
195 changed files with 47784 additions and 1236 deletions
+183
View File
@@ -0,0 +1,183 @@
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Self-hosted Sygnal instance on the Pi — must be configured with the
// Firebase service account for the 'chat-pyramid' Firebase project.
// push.element.io only supports Element's own apps and will actively reject
// chat.pyramid.pyramid pushkeys, causing the homeserver to auto-remove the pusher.
const _kGatewayUrl = 'https://push.steggi-matrix.work/_matrix/push/v1/notify';
const _kAppId = 'chat.pyramid.pyramid';
// ── Foreground-Setup & Pusher-Registrierung ───────────────────────────────────
Future<String> fcmDiagnostics(Client matrixClient) async {
if (!Platform.isAndroid) return 'Kein Android';
final buf = StringBuffer();
try {
await Firebase.initializeApp();
buf.writeln('✓ Firebase initialisiert');
} catch (e) {
buf.writeln('✗ Firebase-Init: $e');
return buf.toString();
}
try {
final settings = await FirebaseMessaging.instance.requestPermission();
buf.writeln('✓ Berechtigung: ${settings.authorizationStatus.name}');
} catch (e) {
buf.writeln('✗ Berechtigung: $e');
}
try {
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
buf.writeln('✓ FCM-Token: ${token.substring(0, 20)}');
} else {
buf.writeln('✗ FCM-Token: null');
}
} catch (e) {
buf.writeln('✗ FCM-Token: $e');
}
try {
final pushers = await matrixClient.getPushers() ?? [];
final fcmPushers = pushers.where((p) => p.appId == _kAppId).toList();
if (fcmPushers.isEmpty) {
buf.writeln('✗ Pusher: keiner bei Synapse registriert!');
} else {
buf.writeln('✓ Pusher: ${fcmPushers.length} registriert');
}
} catch (e) {
buf.writeln('✗ Pusher-Abfrage: $e');
}
return buf.toString();
}
Future<void> initFcm(Client matrixClient) async {
if (!Platform.isAndroid) return;
try {
await Firebase.initializeApp();
} catch (_) {
// Bereits initialisiert (main.dart) oder google-services.json fehlt weiter
}
// Background messages are handled by handleBackgroundMessage (background_push.dart)
// via FlutterFirebaseMessagingService. It decrypts E2EE events using the Matrix
// SDK and shows notifications via flutter_local_notifications with
// showsUserInterface:false so no Activity opens when the user replies.
// Notification-Berechtigung anfordern (Android 13+)
await FirebaseMessaging.instance.requestPermission();
// Foreground-Notifications unterdrücken — App zeigt selbst Nachrichten
await FirebaseMessaging.instance
.setForegroundNotificationPresentationOptions(
alert: false,
badge: false,
sound: false,
);
// Re-register pusher if token was refreshed while the app was killed.
final prefs = await SharedPreferences.getInstance();
final pendingToken = prefs.getString('flutter.fcm_pending_token');
if (pendingToken != null && matrixClient.isLogged()) {
await _registerPusher(matrixClient, pendingToken);
await prefs.remove('flutter.fcm_pending_token');
}
final token = pendingToken ?? await _getToken();
if (token != null && matrixClient.isLogged()) {
await _registerPusher(matrixClient, token);
// Save homeserver + token for PushService.kt (Kotlin native notification handler)
await _saveNotifCredentials(matrixClient);
}
// Bei Token-Refresh: Pusher + gespeicherte Credentials aktualisieren
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
if (matrixClient.isLogged()) {
await _registerPusher(matrixClient, newToken);
await _saveNotifCredentials(matrixClient);
}
});
}
Future<void> _saveNotifCredentials(Client client) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('notif_homeserver', client.homeserver.toString());
await prefs.setString('notif_access_token', client.accessToken ?? '');
} catch (_) {}
}
Future<String?> _getToken() async {
try {
return await FirebaseMessaging.instance.getToken();
} catch (_) {
return null;
}
}
Future<void> _registerPusher(Client client, String fcmToken) async {
try {
await client.postPusher(
Pusher(
pushkey: fcmToken,
kind: 'http',
appId: _kAppId,
appDisplayName: 'Pyramid',
deviceDisplayName: client.deviceName ?? 'Android',
lang: 'de',
data: PusherData(
url: Uri.parse(_kGatewayUrl),
format: 'event_id_only',
),
),
append: true,
);
} catch (e) {
debugPrint('[FCM] Pusher-Registrierung fehlgeschlagen: $e');
}
}
/// Tries to register the FCM pusher and returns a human-readable result string.
Future<String> retryPusherRegistration(Client client) async {
if (!Platform.isAndroid) return 'Kein Android';
final buf = StringBuffer();
try {
String? token;
try {
token = await FirebaseMessaging.instance.getToken();
} catch (e) {
return '✗ FCM-Token: $e';
}
if (token == null) {
return '✗ FCM-Token: null (Google Play Services Problem?)';
}
buf.writeln('✓ FCM-Token: ${token.substring(0, 20)}');
buf.writeln(' Gateway: $_kGatewayUrl');
await client.postPusher(
Pusher(
pushkey: token,
kind: 'http',
appId: _kAppId,
appDisplayName: 'Pyramid',
deviceDisplayName: client.deviceName ?? 'Android',
lang: 'de',
data: PusherData(
url: Uri.parse(_kGatewayUrl),
format: 'event_id_only',
),
),
append: true,
);
buf.writeln('✓ Pusher registriert');
await _saveNotifCredentials(client);
buf.writeln('✓ Credentials gespeichert');
} catch (e) {
buf.writeln('✗ Fehler: $e');
}
return buf.toString().trim();
}