feat(block2): messages inbox + drawer 'Chat' link (was unreachable)

Chat was only reachable via an offer's 'Message' button, and the drawer 'Chat'
sat on 'coming soon' — so messaging/trust were effectively hidden.
- MessageStore keeps a conversation index (the keystore has no key enumeration)
  and exposes conversations() — peers newest-active-first with their last line.
- ChatListScreen: the messages inbox (tap a conversation → /chat/:pubkey).
- Drawer 'Chat' is now a live destination -> /messages (gated like Market).
- i18n en/es/pt. conversations() covered by a plain test (no widget hang).

Analyzer clean.
This commit is contained in:
vjrj 2026-07-10 12:26:33 +02:00
parent 6d16656911
commit 7f1c520960
12 changed files with 211 additions and 3 deletions

View file

@ -8,12 +8,29 @@ import '../security/secret_store.dart';
/// 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;
@ -36,7 +53,7 @@ class MessageStore {
}
/// Appends [message] to the conversation with [peerPubkey] (trimming to the
/// cap).
/// cap) and records the peer in the conversation index.
Future<void> append(String peerPubkey, PrivateMessage message) async {
final next = [...await history(peerPubkey), message];
final capped =
@ -52,5 +69,35 @@ class MessageStore {
},
]),
);
await _rememberPeer(peerPubkey);
}
/// Conversations, most-recently-active first (for the messages inbox).
Future<List<ChatSummary>> conversations() async {
final summaries = <ChatSummary>[];
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<List<String>> _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<void> _rememberPeer(String peer) async {
final peers = await _peers();
if (peers.contains(peer)) return;
await _store.write(_indexKey, [...peers, peer].join('\n'));
}
}