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. /// A conversation preview for the messages inbox. class ChatSummary { const ChatSummary({ required this.peerPubkey, required this.lastText, required this.lastAt, }); final String peerPubkey; final String lastText; final DateTime lastAt; } class MessageStore { MessageStore(this._store); final SecretStore _store; static const _prefix = 'tane.social.chat.'; /// Index of peers we have a conversation with (the keystore is key/value with /// no key enumeration, so we track the list ourselves). static const _indexKey = 'tane.social.chats'; /// 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> 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>()) 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) and records the peer in the conversation index. Future 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, }, ]), ); await _rememberPeer(peerPubkey); } /// Conversations, most-recently-active first (for the messages inbox). Future> conversations() async { final summaries = []; for (final peer in await _peers()) { final history = await this.history(peer); if (history.isEmpty) continue; final last = history.last; summaries.add(ChatSummary( peerPubkey: peer, lastText: last.text, lastAt: last.at, )); } summaries.sort((a, b) => b.lastAt.compareTo(a.lastAt)); return summaries; } Future> _peers() async { final raw = await _store.read(_indexKey); if (raw == null || raw.isEmpty) return const []; return raw.split('\n').where((s) => s.isNotEmpty).toList(); } Future _rememberPeer(String peer) async { final peers = await _peers(); if (peers.contains(peer)) return; await _store.write(_indexKey, [...peers, peer].join('\n')); } }