feat(block2): persist chat history (messages survive leaving the chat)
Messages were in-memory only — gone on leaving the conversation. - MessageStore: keystore-backed (no plaintext), a capped per-peer JSON list (last 200). Simple; the encrypted Drift DB is the eventual home at scale. - MessagesCubit: loads saved history on start (subscribes FIRST, then loads, so a message arriving during the load isn't dropped) and persists every sent and received message. Wired via DI + TaneApp(messageStore). Tests (plain 'test', no hang risk): MessageStore round-trip/per-peer/cap, and a cubit test that reopens a fresh cubit and sees the saved conversation. Analyzer clean; run 'flutter test' locally to confirm.
This commit is contained in:
parent
847590ff11
commit
6d16656911
8 changed files with 186 additions and 22 deletions
56
apps/app_seeds/lib/services/message_store.dart
Normal file
56
apps/app_seeds/lib/services/message_store.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
|
||||
import '../security/secret_store.dart';
|
||||
|
||||
/// Persists a 1:1 chat history, keystore-backed (so no plaintext at rest). A
|
||||
/// capped, per-peer JSON list — recent history survives leaving the chat. Small
|
||||
/// and simple on purpose; the encrypted Drift DB is the eventual home if chats
|
||||
/// grow large.
|
||||
class MessageStore {
|
||||
MessageStore(this._store);
|
||||
|
||||
final SecretStore _store;
|
||||
static const _prefix = 'tane.social.chat.';
|
||||
|
||||
/// Keep only the most recent [_cap] messages per conversation, to bound the
|
||||
/// keystore entry size.
|
||||
static const _cap = 200;
|
||||
|
||||
String _key(String peerPubkey) => '$_prefix$peerPubkey';
|
||||
|
||||
/// Messages exchanged with [peerPubkey], oldest first.
|
||||
Future<List<PrivateMessage>> history(String peerPubkey) async {
|
||||
final raw = await _store.read(_key(peerPubkey));
|
||||
if (raw == null || raw.isEmpty) return const [];
|
||||
final list = jsonDecode(raw) as List;
|
||||
return [
|
||||
for (final m in list.cast<Map<String, dynamic>>())
|
||||
PrivateMessage(
|
||||
fromPubkey: m['from'] as String,
|
||||
text: m['text'] as String,
|
||||
at: DateTime.fromMillisecondsSinceEpoch(m['at'] as int),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Appends [message] to the conversation with [peerPubkey] (trimming to the
|
||||
/// cap).
|
||||
Future<void> append(String peerPubkey, PrivateMessage message) async {
|
||||
final next = [...await history(peerPubkey), message];
|
||||
final capped =
|
||||
next.length > _cap ? next.sublist(next.length - _cap) : next;
|
||||
await _store.write(
|
||||
_key(peerPubkey),
|
||||
jsonEncode([
|
||||
for (final m in capped)
|
||||
{
|
||||
'from': m.fromPubkey,
|
||||
'text': m.text,
|
||||
'at': m.at.millisecondsSinceEpoch,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue