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
54
apps/app_seeds/lib/db/chat_database.dart
Normal file
54
apps/app_seeds/lib/db/chat_database.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'chat_database.g.dart';
|
||||
|
||||
/// A 1:1 chat message, held locally so a conversation survives leaving the
|
||||
/// screen and going offline.
|
||||
///
|
||||
/// This is deliberately NOT a [SyncColumns] inventory row: chat history is an
|
||||
/// ephemeral, per-device cache of what the relays delivered — not the user's
|
||||
/// CRDT-synced inventory. It never enters an inventory snapshot, backup, or the
|
||||
/// device-to-device sync, so it needs no HLC / tombstone / author metadata. It
|
||||
/// lives in its own encrypted database ([ChatDatabase]) to keep it fully
|
||||
/// isolated from the inventory schema, its migrations, and its sync stream.
|
||||
///
|
||||
/// [accountScope] namespaces rows per social identity (empty = the original
|
||||
/// identity), replacing the old per-key keystore prefix. The unique key makes
|
||||
/// de-duplication a DB invariant: a relay re-delivers the same gift wrap on
|
||||
/// every (re)subscribe, and an identical (scope, peer, sender, timestamp, text)
|
||||
/// row is simply not inserted twice.
|
||||
@TableIndex(
|
||||
name: 'messages_by_conversation',
|
||||
columns: {#accountScope, #peerPubkey, #sentAt},
|
||||
)
|
||||
class Messages extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get accountScope => text()();
|
||||
TextColumn get peerPubkey => text()();
|
||||
TextColumn get fromPubkey => text()();
|
||||
TextColumn get body => text()();
|
||||
|
||||
/// Milliseconds since epoch — the message's own timestamp (NIP-17), used for
|
||||
/// ordering and for the read/unread mark.
|
||||
IntColumn get sentAt => integer()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{accountScope, peerPubkey, fromPubkey, sentAt, body},
|
||||
];
|
||||
}
|
||||
|
||||
/// The encrypted local chat cache (SQLCipher via an injected executor, exactly
|
||||
/// like [AppDatabase]). Separate database on purpose — see [Messages]. There is
|
||||
/// no historical schema to migrate: version 1, created fresh.
|
||||
@DriftDatabase(tables: [Messages])
|
||||
class ChatDatabase extends _$ChatDatabase {
|
||||
ChatDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration =>
|
||||
MigrationStrategy(onCreate: (m) async => m.createAll());
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue