Merge branch 'claude/awesome-golick-007e7d': message unread badges + OS notifications

# Conflicts:
#	apps/app_seeds/lib/di/injector.dart
#	apps/app_seeds/lib/i18n/ast.i18n.json
#	apps/app_seeds/lib/i18n/en.i18n.json
#	apps/app_seeds/lib/i18n/es.i18n.json
#	apps/app_seeds/lib/i18n/pt.i18n.json
#	apps/app_seeds/lib/i18n/strings.g.dart
#	apps/app_seeds/lib/i18n/strings_ast.g.dart
#	apps/app_seeds/lib/i18n/strings_en.g.dart
#	apps/app_seeds/lib/i18n/strings_es.g.dart
#	apps/app_seeds/lib/i18n/strings_pt.g.dart
#	apps/app_seeds/lib/ui/chat_screen.dart
This commit is contained in:
vjrj 2026-07-10 21:15:59 +02:00
commit d2b98af36d
27 changed files with 1238 additions and 114 deletions

View 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);
});
}