feat: Push als Chat-Verlauf - mehrere Nachrichten, voller Text, Bildvorschau, echtes "Gelesen"
Bernds Beta-Feedback: pro Raum wurde jede Nachricht durch die naechste ersetzt und auf 100 Zeichen gekuerzt, ohne Bild. Jetzt: - NotificationHelper: MessagingStyle mit den letzten 8 Nachrichten pro Raum (Verlauf in SharedPreferences, nur solange die Benachrichtigung sichtbar ist), gleiche eventId ersetzt (Platzhalter -> Klartext), nur Neues klingelt. - notification_content.dart: Text bis 1000 Zeichen, Antwort-Zitat entfernt, Medien-Labels, Vorschau-Einstellung auch bei gekillter App; Bild (E2EE entschluesselt) + Avatar per FileProvider an die System-UI. - "Gelesen" setzt eine echte Lesebestaetigung (Haupt-Engine bzw. BgEngine). - Laufende App holt genau das gepushte Event statt room.lastEvent. - Debug-only DebugNotifyReceiver zum Testen per adb ohne Login. Emulator (API 36): Verlauf, Ersetzen, Bild, Avatar, Gelesen geprueft; 9 neue Tests. Echtes Geraet (Bernd) steht aus. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
460477204b
commit
ef1d881967
@@ -8,6 +8,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/app_database.dart';
|
||||
import 'package:pyramid/core/notification_content.dart';
|
||||
import 'package:pyramid/core/soft_logout_guard.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -64,6 +65,11 @@ void notificationEngineMain() {
|
||||
args['room_id'] as String?,
|
||||
args['text'] as String?,
|
||||
);
|
||||
case 'markRead':
|
||||
return await _bgMarkRead(
|
||||
args['room_id'] as String?,
|
||||
args['event_id'] as String?,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF-BGENGINE] handler error: $e');
|
||||
@@ -88,26 +94,27 @@ Future<bool> _bgDecryptAndShow(String? roomId, String? eventId, int? notifId) as
|
||||
)
|
||||
.timeout(const Duration(seconds: 20));
|
||||
if (event == null) return false;
|
||||
// Bad-encrypted body still starts with "**" — keep the native placeholder.
|
||||
if (event.body.startsWith('**')) return false;
|
||||
|
||||
final room = client.getRoomById(roomId);
|
||||
final sender = room?.unsafeGetUserFromMemoryOrFallback(event.senderId).displayName ??
|
||||
event.senderId.split(':').first.replaceFirst('@', '');
|
||||
final title = (room?.isDirectChat ?? true)
|
||||
? sender
|
||||
: '$sender · ${room?.getLocalizedDisplayname() ?? ''}';
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
event,
|
||||
showPreview: prefs.getBool('notif_preview') ?? true,
|
||||
);
|
||||
// Nicht entschlüsselt / nichts Anzeigbares → nativer Platzhalter bleibt.
|
||||
if (content == null) return false;
|
||||
|
||||
// Hand the decrypted content back to native so it re-shows the SAME
|
||||
// notification via NotificationHelper — that keeps the reliable
|
||||
// MainActivity-based reply action (the flutter_local_notifications reply
|
||||
// path doesn't fire dependably when the app is killed).
|
||||
await const MethodChannel(_kBgEngineChannel).invokeMethod('showDecrypted', {
|
||||
'title': title,
|
||||
'body': _buildBody(event),
|
||||
'room_id': roomId,
|
||||
'notif_id': notifId ?? _stableId(roomId),
|
||||
});
|
||||
// ReplyReceiver-based reply action (the flutter_local_notifications reply
|
||||
// path doesn't fire dependably when the app is killed). Erst der Text,
|
||||
// dann lautlos Avatar/Bild (deliverMessageNotification).
|
||||
await deliverMessageNotification(
|
||||
event,
|
||||
content,
|
||||
notifId: notifId ?? _stableId(roomId),
|
||||
send: (a) => const MethodChannel(_kBgEngineChannel)
|
||||
.invokeMethod('showDecrypted', a),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF-BGENGINE] decryptAndShow error: $e');
|
||||
@@ -152,6 +159,27 @@ Future<bool> _bgSendReply(String? roomId, String? text) async {
|
||||
}
|
||||
}
|
||||
|
||||
/// „Gelesen“ bei beendeter App: Lesebestätigung bis [eventId] setzen (reiner
|
||||
/// HTTP-Aufruf, keine Verschlüsselung nötig).
|
||||
Future<bool> _bgMarkRead(String? roomId, String? eventId) async {
|
||||
if (roomId == null || eventId == null) return false;
|
||||
final client = await _buildClient();
|
||||
if (client == null) return false;
|
||||
try {
|
||||
final room = client.getRoomById(roomId) ?? Room(id: roomId, client: client);
|
||||
await room
|
||||
.setReadMarker(eventId, mRead: eventId)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
debugPrint('[NOTIF-BGENGINE] read marker set for $roomId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF-BGENGINE] markRead error: $e');
|
||||
return false;
|
||||
} finally {
|
||||
await client.dispose().catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the raw callback handle of [notificationEngineMain] so native code can
|
||||
/// bootstrap the background engine. Call once at app startup.
|
||||
Future<void> registerBgEngineHandle() async {
|
||||
@@ -217,20 +245,6 @@ Future<Client?> _buildClient() async {
|
||||
}
|
||||
}
|
||||
|
||||
String _buildBody(Event event) {
|
||||
final msgType = event.messageType;
|
||||
final rawBody = event.body;
|
||||
return switch (msgType) {
|
||||
'm.text' => rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody,
|
||||
'm.image' => '📷 Bild',
|
||||
'm.video' => '🎥 Video',
|
||||
'm.audio' => '🎵 Audio',
|
||||
'm.file' => '📄 Datei',
|
||||
'm.sticker' => '🏷️ Sticker',
|
||||
_ => rawBody.isEmpty ? 'Neue Nachricht' : (rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Notification action handler ───────────────────────────────────────────────
|
||||
// Called in a fresh Dart isolate when the user presses Reply or Gelesen.
|
||||
// Must return quickly — Android keeps the notification's "sending" spinner
|
||||
|
||||
Reference in New Issue
Block a user