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
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/notification_content.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
import 'support/synthetic_matrix_data.dart';
|
||||
|
||||
/// Vertragstests für den Inhalt der Push-Benachrichtigungen (Bernds
|
||||
/// Beta-Feedback 2026-10-07: „beim Aufklappen viel weniger Text“, „keine
|
||||
/// Bildvorschau“). Native Darstellung (Verlauf, Bild) prüft das Gerät.
|
||||
void main() {
|
||||
late Client client;
|
||||
late Room room;
|
||||
|
||||
setUp(() async {
|
||||
final factory = createDatabaseFactoryFfi(
|
||||
ffiInit: sqfliteFfiInit,
|
||||
noIsolate: true,
|
||||
);
|
||||
final db = await factory.openDatabase(inMemoryDatabasePath);
|
||||
client = await SyntheticMatrixData.openClient(db, name: 'PyramidNotifTest');
|
||||
room = Room(id: '!raum:bench.invalid', client: client);
|
||||
room.setState(
|
||||
Event(
|
||||
type: EventTypes.RoomName,
|
||||
content: {'name': 'Familie'},
|
||||
eventId: r'$name',
|
||||
senderId: '@uta:bench.invalid',
|
||||
stateKey: '',
|
||||
originServerTs: DateTime(2026),
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
room.setState(
|
||||
Event(
|
||||
type: EventTypes.RoomMember,
|
||||
content: {'membership': 'join', 'displayname': 'Uta'},
|
||||
eventId: r'$member',
|
||||
senderId: '@uta:bench.invalid',
|
||||
stateKey: '@uta:bench.invalid',
|
||||
originServerTs: DateTime(2026),
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
|
||||
Event message(
|
||||
Map<String, Object?> content, {
|
||||
String type = EventTypes.Message,
|
||||
String sender = '@uta:bench.invalid',
|
||||
}) => Event(
|
||||
type: type,
|
||||
content: content,
|
||||
eventId: r'$ev1',
|
||||
senderId: sender,
|
||||
originServerTs: DateTime(2026, 10, 7, 12, 30),
|
||||
room: room,
|
||||
);
|
||||
|
||||
test('Text wird NICHT mehr auf 100 Zeichen gekürzt', () {
|
||||
final text = 'Wort ' * 60; // 300 Zeichen
|
||||
final body = notificationBody(message({'msgtype': 'm.text', 'body': text}));
|
||||
expect(body, text.trim());
|
||||
});
|
||||
|
||||
test('sehr lange Texte werden bei 1000 Zeichen gekappt', () {
|
||||
final body = notificationBody(
|
||||
message({'msgtype': 'm.text', 'body': 'x' * 5000}),
|
||||
)!;
|
||||
expect(body.length, notificationBodyMaxChars + 1);
|
||||
expect(body.endsWith('…'), isTrue);
|
||||
});
|
||||
|
||||
test('Antwort-Zitat (Reply-Fallback) wird entfernt', () {
|
||||
final body = notificationBody(
|
||||
message({
|
||||
'msgtype': 'm.text',
|
||||
'body': '> <@bernd:bench.invalid> Kommst du heute?\n\nJa, um acht!',
|
||||
}),
|
||||
);
|
||||
expect(body, 'Ja, um acht!');
|
||||
});
|
||||
|
||||
test('Medien bekommen verständliche Bezeichnungen', () {
|
||||
expect(
|
||||
notificationBody(message({'msgtype': 'm.image', 'body': 'IMG_1.jpg'})),
|
||||
'📷 Foto',
|
||||
);
|
||||
expect(
|
||||
notificationBody(
|
||||
message({
|
||||
'msgtype': 'm.image',
|
||||
'body': 'Unser Garten',
|
||||
'filename': 'IMG_2.jpg',
|
||||
}),
|
||||
),
|
||||
'📷 Unser Garten',
|
||||
reason: 'MSC2530: abweichender body neben filename = Bildunterschrift',
|
||||
);
|
||||
expect(
|
||||
notificationBody(message({'msgtype': 'm.video', 'body': 'v.mp4'})),
|
||||
'🎥 Video',
|
||||
);
|
||||
expect(
|
||||
notificationBody(
|
||||
message({
|
||||
'msgtype': 'm.audio',
|
||||
'body': 'Sprachnachricht.ogg',
|
||||
'org.matrix.msc3245.voice': <String, Object?>{},
|
||||
}),
|
||||
),
|
||||
'🎤 Sprachnachricht',
|
||||
);
|
||||
expect(
|
||||
notificationBody(message({'msgtype': 'm.audio', 'body': 'lied.mp3'})),
|
||||
'🎵 Audio',
|
||||
);
|
||||
expect(
|
||||
notificationBody(
|
||||
message({
|
||||
'msgtype': 'm.file',
|
||||
'body': 'rechnung.pdf',
|
||||
'filename': 'rechnung.pdf',
|
||||
}),
|
||||
),
|
||||
'📎 rechnung.pdf',
|
||||
);
|
||||
expect(
|
||||
notificationBody(message({'body': 'x'}, type: EventTypes.Sticker)),
|
||||
'🏷️ Sticker',
|
||||
);
|
||||
});
|
||||
|
||||
test('nicht entschlüsselte Nachricht → null (Platzhalter bleibt)', () {
|
||||
expect(
|
||||
notificationBody(
|
||||
message({
|
||||
'algorithm': 'm.megolm.v1.aes-sha2',
|
||||
'ciphertext': 'abc',
|
||||
}, type: EventTypes.Encrypted),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
notificationBody(
|
||||
message({
|
||||
'msgtype': MessageTypes.BadEncrypted,
|
||||
'body': '** Unable to decrypt: …',
|
||||
}),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('Markdown-Fett am Anfang ist KEIN Entschlüsselungsfehler', () {
|
||||
// Früher galt jeder Text mit "**" am Anfang als nicht entschlüsselt.
|
||||
expect(
|
||||
notificationBody(
|
||||
message({'msgtype': 'm.text', 'body': '**Wichtig** heute'}),
|
||||
),
|
||||
'**Wichtig** heute',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Inhalt: Absender-Anzeigename, Raumname, Bild-Flag, Native-Argumente',
|
||||
() {
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
message({'msgtype': 'm.image', 'body': 'IMG_1.jpg'}),
|
||||
)!;
|
||||
expect(content.senderName, 'Uta');
|
||||
expect(content.roomName, 'Familie');
|
||||
expect(content.isGroup, isTrue);
|
||||
expect(content.isImage, isTrue);
|
||||
expect(content.title, 'Uta · Familie');
|
||||
|
||||
final args = content.toNativeArgs(notifId: 42, imagePath: '/x/img');
|
||||
expect(args['notifId'], 42);
|
||||
expect(args['eventId'], r'$ev1');
|
||||
expect(args['senderId'], '@uta:bench.invalid');
|
||||
expect(args['imagePath'], '/x/img');
|
||||
expect(
|
||||
args['timestamp'],
|
||||
DateTime(2026, 10, 7, 12, 30).millisecondsSinceEpoch,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('unbekannter Absender → Localpart statt leerem Namen', () {
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
message({
|
||||
'msgtype': 'm.text',
|
||||
'body': 'Hi',
|
||||
}, sender: '@jochen:bench.invalid'),
|
||||
)!;
|
||||
expect(content.senderName, 'jochen');
|
||||
});
|
||||
|
||||
test('Vorschau aus → kein Inhalt, kein Bild', () {
|
||||
final content = MessageNotificationContent.fromEvent(
|
||||
message({'msgtype': 'm.image', 'body': 'IMG_1.jpg'}),
|
||||
showPreview: false,
|
||||
)!;
|
||||
expect(content.body, notificationPlaceholderBody);
|
||||
expect(content.isImage, isFalse);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user