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:
vjrj 2026-07-11 06:39:39 +02:00
parent 171daabce3
commit 6cff0d0b11
27 changed files with 1793 additions and 264 deletions

View file

@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/data/species_repository.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/chat_database.dart';
import 'package:tane/db/database.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/security/secret_store.dart';
@ -19,6 +20,9 @@ import 'package:tane/ui/variety_detail_screen.dart';
/// verified separately in the SQLCipher-only security test).
AppDatabase newTestDatabase() => AppDatabase(NativeDatabase.memory());
/// A fresh in-memory chat cache for host tests (unencrypted; see above).
ChatDatabase newTestChatDatabase() => ChatDatabase(NativeDatabase.memory());
/// Unmounts the widget tree *inside* the test and drains the zero-duration
/// timer Drift schedules when its stream subscription is cancelled. Without
/// this, flutter_test reports "a Timer is still pending after disposal". Call