feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated
- Anchor the message list to the bottom (`reverse: true`) so new messages stay in view instead of landing below the fold. - Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`): indexed append, uncapped history, dedup as a unique-key invariant. It's an ephemeral per-device cache, isolated from the inventory schema, its migrations, and its sync. No data migration (pre-release). - Add day separators (Today/Yesterday/locale date) and a per-bubble time, all via ICU (12/24h per locale; Localizations locale maps Asturian → Spanish for intl date symbols). - Add peer avatars (deterministic colour from the pubkey + name initial), surface send failures that were previously silent, and make bubble text selectable (addresses, links). - New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars, scroll anchoring, storage and send errors. Docs: docs/design/chat-storage.md + open-decisions.md.
This commit is contained in:
parent
171daabce3
commit
6cff0d0b11
27 changed files with 1793 additions and 264 deletions
38
apps/app_seeds/test/ui/peer_avatar_test.dart
Normal file
38
apps/app_seeds/test/ui/peer_avatar_test.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/ui/peer_avatar.dart';
|
||||
|
||||
void main() {
|
||||
group('peerAvatarColor', () {
|
||||
test('is deterministic for the same pubkey', () {
|
||||
expect(peerAvatarColor('ab' * 32), peerAvatarColor('ab' * 32));
|
||||
});
|
||||
|
||||
test('differs for different pubkeys', () {
|
||||
expect(peerAvatarColor('ab' * 32), isNot(peerAvatarColor('cd' * 32)));
|
||||
});
|
||||
|
||||
test('is fully opaque (legible disc)', () {
|
||||
expect(peerAvatarColor('feed').a, 1.0);
|
||||
});
|
||||
});
|
||||
|
||||
group('PeerAvatar', () {
|
||||
Widget host(Widget child) => MaterialApp(
|
||||
home: Scaffold(body: Center(child: child)),
|
||||
);
|
||||
|
||||
testWidgets('shows the name initial when known', (tester) async {
|
||||
await tester.pumpWidget(host(PeerAvatar(pubkey: 'aa', name: 'rosa')));
|
||||
expect(find.text('R'), findsOneWidget); // uppercased first letter
|
||||
});
|
||||
|
||||
testWidgets('falls back to a glyph when the name is unknown', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(host(const PeerAvatar(pubkey: 'aa')));
|
||||
expect(find.byIcon(Icons.person_outline), findsOneWidget);
|
||||
expect(find.byType(Text), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue