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
@@ -12,6 +12,7 @@ import 'package:pyramid/core/app_state.dart';
|
||||
import 'package:pyramid/core/background_push.dart';
|
||||
import 'package:pyramid/core/fcm_push_service.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
import 'package:pyramid/core/notification_content.dart';
|
||||
import 'package:pyramid/core/settings_prefs.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:windows_taskbar/windows_taskbar.dart';
|
||||
@@ -197,6 +198,21 @@ Future<void> _initAndroid(WidgetRef ref) async {
|
||||
if (!share.isEmpty) {
|
||||
ref.read(pendingShareProvider.notifier).state = share;
|
||||
}
|
||||
} else if (call.method == 'markReadFromNotification') {
|
||||
// „Gelesen“ in der Benachrichtigung (ReplyReceiver, App lebt).
|
||||
final roomId = (args is Map ? args['room_id'] : null) as String?;
|
||||
final eventId = (args is Map ? args['event_id'] : null) as String?;
|
||||
if (roomId != null && eventId != null) {
|
||||
try {
|
||||
final client = await ref.read(matrixClientProvider.future);
|
||||
await client
|
||||
.getRoomById(roomId)
|
||||
?.setReadMarker(eventId, mRead: eventId);
|
||||
debugPrint('[NOTIF-FG] markReadFromNotification: $roomId bis $eventId');
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF-FG] markReadFromNotification error: $e');
|
||||
}
|
||||
}
|
||||
} else if (call.method == 'decryptAndUpdateNotification') {
|
||||
// Called by PushService.kt after showing a "Neue Nachricht" placeholder.
|
||||
// We decrypt the event here (the Matrix client has the keys) and overwrite
|
||||
@@ -458,57 +474,37 @@ Future<void> _decryptAndUpdateNotification(
|
||||
final client = await ref
|
||||
.read(matrixClientProvider.future)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
var room = client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate: room $roomId not found');
|
||||
if (eventId == null) return;
|
||||
|
||||
// Genau das gepushte Event holen (lokal oder vom Server, entschlüsselt) –
|
||||
// nicht room.lastEvent, das kann inzwischen eine Reaktion o. Ä. sein.
|
||||
final event = await client
|
||||
.getEventByPushNotification(
|
||||
PushNotification(roomId: roomId, eventId: eventId, devices: const []),
|
||||
storeInDatabase: false,
|
||||
returnNullIfSeen: false,
|
||||
)
|
||||
.timeout(const Duration(seconds: 20));
|
||||
if (event == null || event.senderId == client.userID) return;
|
||||
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
event,
|
||||
showPreview: ref.read(notifPreviewProvider),
|
||||
);
|
||||
if (content == null) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate: nichts Anzeigbares, Platzhalter bleibt');
|
||||
return;
|
||||
}
|
||||
|
||||
// The event might not have synced yet — wait for the next sync (max 12 s).
|
||||
// We use lastEvent as a proxy: after sync it will be the event we want.
|
||||
Event? event = room.lastEvent;
|
||||
final bool alreadyHave =
|
||||
eventId == null || event?.eventId == eventId;
|
||||
if (!alreadyHave || event == null) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate: waiting for sync to deliver $eventId');
|
||||
await client.onSync.stream.first.timeout(const Duration(seconds: 12));
|
||||
room = client.getRoomById(roomId);
|
||||
if (room == null) return;
|
||||
event = room.lastEvent;
|
||||
}
|
||||
|
||||
if (event == null) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate: event still null after sync');
|
||||
return;
|
||||
}
|
||||
if (event.type != EventTypes.Message) return;
|
||||
if (event.senderId == client.userID) return;
|
||||
|
||||
final rawBody = event.body;
|
||||
// If decryption failed the body starts with "** Unable to decrypt"
|
||||
if (rawBody.isEmpty || rawBody.startsWith('**')) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate: decryption failed, keeping placeholder');
|
||||
return;
|
||||
}
|
||||
|
||||
final notifBody =
|
||||
rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody;
|
||||
final sender = room.unsafeGetUserFromMemoryOrFallback(event.senderId);
|
||||
final displayName = sender.displayName ??
|
||||
event.senderId.split(':').first.replaceFirst('@', '');
|
||||
final title = room.isDirectChat
|
||||
? displayName
|
||||
: '$displayName · ${room.getLocalizedDisplayname()}';
|
||||
|
||||
// Overwrite the "Neue Nachricht" placeholder — same notifId so Android
|
||||
// replaces the existing notification rather than stacking a new one.
|
||||
await _kInstallChannel.invokeMethod('showNativeNotification', {
|
||||
'title': title,
|
||||
'body': notifBody,
|
||||
'roomId': roomId,
|
||||
'notifId': notifId ?? _stableRoomId(roomId),
|
||||
});
|
||||
debugPrint('[NOTIF] decryptAndUpdate: updated notification → "$title: $notifBody"');
|
||||
// Same eventId → NotificationHelper ersetzt den „Neue Nachricht“-
|
||||
// Platzhalter im Verlauf, statt eine zweite Nachricht anzuhängen.
|
||||
await deliverMessageNotification(
|
||||
event,
|
||||
content,
|
||||
notifId: notifId ?? _stableRoomId(roomId),
|
||||
send: (a) => _kInstallChannel.invokeMethod('showNativeNotification', a),
|
||||
);
|
||||
debugPrint('[NOTIF] decryptAndUpdate: updated notification for $eventId');
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF] decryptAndUpdate error: $e');
|
||||
}
|
||||
@@ -516,27 +512,30 @@ Future<void> _decryptAndUpdateNotification(
|
||||
|
||||
// ─── Show notification ────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> showMessageNotification({
|
||||
required String roomId,
|
||||
String? roomName,
|
||||
required String senderName,
|
||||
required String body,
|
||||
}) async {
|
||||
final title = roomName != null ? '$senderName · $roomName' : senderName;
|
||||
Future<void> showMessageNotification(
|
||||
Event event,
|
||||
MessageNotificationContent content,
|
||||
) async {
|
||||
final roomId = content.roomId;
|
||||
if (Platform.isAndroid && _androidInitialized) {
|
||||
// Use native Kotlin notification so the reply PendingIntent targets
|
||||
// ReplyReceiver directly — the app never opens when the user replies.
|
||||
try {
|
||||
await _kInstallChannel.invokeMethod('showNativeNotification', {
|
||||
'title': title,
|
||||
'body': body,
|
||||
'roomId': roomId,
|
||||
'notifId': _stableRoomId(roomId),
|
||||
});
|
||||
await deliverMessageNotification(
|
||||
event,
|
||||
content,
|
||||
notifId: _stableRoomId(roomId),
|
||||
send: (a) => _kInstallChannel.invokeMethod('showNativeNotification', a),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[NOTIF] showNativeNotification failed: $e');
|
||||
}
|
||||
} else if (Platform.isWindows) {
|
||||
final title = content.title;
|
||||
// Windows-Toasts zeigen ohnehin nur wenige Zeilen.
|
||||
final body = content.body.length > 300
|
||||
? '${content.body.substring(0, 300)}…'
|
||||
: content.body;
|
||||
// Close the previous notification for this room before showing a new one.
|
||||
_windowsNotifs.remove(roomId)?.close();
|
||||
|
||||
@@ -624,23 +623,13 @@ void _watchEvents(Ref ref, Client client) {
|
||||
if (!mentionedIds.contains(myId) && !bodyMentionsUs) return;
|
||||
}
|
||||
|
||||
// ── Build notification body ───────────────────────────────────────────
|
||||
final rawBody = event.body;
|
||||
final showPreview = ref.read(notifPreviewProvider);
|
||||
final notifBody = showPreview
|
||||
? (rawBody.length > 100 ? '${rawBody.substring(0, 100)}…' : rawBody)
|
||||
: 'Neue Nachricht';
|
||||
|
||||
final sender = room.unsafeGetUserFromMemoryOrFallback(event.senderId);
|
||||
final displayName = sender.displayName ??
|
||||
event.senderId.split(':').first.replaceFirst('@', '');
|
||||
|
||||
showMessageNotification(
|
||||
roomId: roomId,
|
||||
roomName: room.isDirectChat ? null : room.getLocalizedDisplayname(),
|
||||
senderName: displayName,
|
||||
body: notifBody,
|
||||
// ── Build notification content ────────────────────────────────────────
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
event,
|
||||
showPreview: ref.read(notifPreviewProvider),
|
||||
);
|
||||
if (content == null) return;
|
||||
showMessageNotification(event, content);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user