feat(messages): unread badges + OS notification for private messages
Private messages (NIP-17) arrived in the foreground with no signal: no unread count, no badge, no OS notification. Hook both into the single InboxService.ingest choke point. - UnreadService: per-peer "last read" in the keystore (timestamps+pubkey only, no message text), live count via a changes stream, active-peer suppression so the open chat never badges or notifies. - UnreadBadge: reusable, RTL-aware (Flutter Badge) — on the home hamburger, the drawer "Chat" item, and per-conversation in the messages list. - Mark-read on chat open (ChatScreen). - NotificationService over flutter_local_notifications: generic "New message from <name>" (no message text, for privacy), payload = pubkey, tap opens /chat/:pubkey. No-op on web/Windows; Android POST_NOTIFICATIONS. - i18n notifications.newMessageFrom (en/es/pt/ast). Background/push stays out of scope (foreground-only by design). Tests: UnreadService, NotificationService (mock plugin), InboxService hooks (new peer notifies/counts; duplicate, own, and open-chat do not), and a UnreadBadge widget test. dart analyze clean; commons_core green.
This commit is contained in:
parent
4d110e8082
commit
d026376993
27 changed files with 1238 additions and 114 deletions
74
apps/app_seeds/test/ui/unread_badge_test.dart
Normal file
74
apps/app_seeds/test/ui/unread_badge_test.dart
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/services/message_store.dart';
|
||||
import 'package:tane/services/unread_service.dart';
|
||||
import 'package:tane/ui/unread_badge.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// The badge is driven by [UnreadService]: it shows a count when there are
|
||||
/// unread messages and clears when they're read — live, over the service's
|
||||
/// `changes` stream. Injecting the service avoids the `getIt` lookup. The store
|
||||
/// reads are real async, so mutations run inside `runAsync`.
|
||||
void main() {
|
||||
late MessageStore store;
|
||||
late UnreadService unread;
|
||||
|
||||
setUp(() {
|
||||
store = MessageStore(InMemorySecretStore());
|
||||
unread = UnreadService(store, InMemorySecretStore());
|
||||
});
|
||||
|
||||
tearDown(() => unread.dispose());
|
||||
|
||||
Widget host() => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: UnreadBadge(
|
||||
peer: 'alice',
|
||||
service: unread,
|
||||
child: const Icon(Icons.chat_bubble),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> receive(int atMs) async {
|
||||
final m = PrivateMessage(
|
||||
fromPubkey: 'alice',
|
||||
text: 'hi',
|
||||
at: DateTime.fromMillisecondsSinceEpoch(atMs));
|
||||
await store.append('alice', m);
|
||||
await unread.onMessageReceived('alice', m);
|
||||
}
|
||||
|
||||
// Lets pending async (store reads + the widget's stream-driven refresh)
|
||||
// resolve in the real zone, then renders a frame.
|
||||
Future<void> pumpRefresh(WidgetTester tester) async {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 20)));
|
||||
await tester.pump();
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('shows an existing unread count on build', (tester) async {
|
||||
await tester.runAsync(() => receive(1000));
|
||||
await tester.pumpWidget(host());
|
||||
await pumpRefresh(tester);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('reacts to a new message and to being read', (tester) async {
|
||||
await tester.pumpWidget(host());
|
||||
await pumpRefresh(tester);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
await tester.runAsync(() => receive(1000));
|
||||
await pumpRefresh(tester);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
|
||||
await tester.runAsync(() => unread.markRead('alice'));
|
||||
await pumpRefresh(tester);
|
||||
expect(find.text('1'), findsNothing);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue