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

@ -49,6 +49,12 @@ class AppDrawer extends StatelessWidget {
_DrawerItem(
icon: const Icon(Icons.chat_bubble),
label: t.menu.chat,
onTap: marketEnabled
? () {
Navigator.of(context).pop();
context.go('/messages');
}
: null,
),
_DrawerItem(
icon: const Icon(Icons.favorite),

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../i18n/strings.g.dart';
import '../services/message_store.dart';
import 'theme.dart';
/// The messages inbox: past conversations, newest first. Tapping one opens the
/// chat. Reached from the drawer's "Chat" entry.
class ChatListScreen extends StatefulWidget {
const ChatListScreen({required this.store, super.key});
final MessageStore store;
@override
State<ChatListScreen> createState() => _ChatListScreenState();
}
class _ChatListScreenState extends State<ChatListScreen> {
List<ChatSummary>? _items;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final items = await widget.store.conversations();
if (mounted) setState(() => _items = items);
}
@override
Widget build(BuildContext context) {
final t = context.t;
final items = _items;
return Scaffold(
appBar: AppBar(title: Text(t.chatList.title)),
body: items == null
? const Center(child: CircularProgressIndicator())
: items.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text(
t.chatList.empty,
textAlign: TextAlign.center,
style: const TextStyle(color: seedMuted, fontSize: 15),
),
),
)
: ListView.separated(
itemCount: items.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, i) {
final c = items[i];
return ListTile(
leading: const CircleAvatar(
backgroundColor: seedAvatar,
child: Icon(Icons.person, color: seedOnAvatar),
),
title: Text(_shortId(c.peerPubkey)),
subtitle: Text(
c.lastText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => context.go('/chat/${c.peerPubkey}'),
);
},
),
);
}
}
/// A compact, human-ish rendering of a public key until we resolve names.
String _shortId(String pubkey) => pubkey.length <= 12
? pubkey
: '${pubkey.substring(0, 6)}${pubkey.substring(pubkey.length - 4)}';