feat(block2): show contact names (resolve peers' published profiles)

Closes the profile loop — peers now appear by name, not a raw key.
- ProfileCache: keystore-backed cache of peers' published display names (works
  offline) + shortPubkey() fallback.
- Chat: the app bar shows the peer's name (cached first, then freshened from
  their kind:0 over the session); falls back to a short key.
- Inbox: each conversation shows the cached name, and missing names are resolved
  over one session and cached.
- Wired via DI + TaneApp(profileCache).

Analyzer clean; ProfileCache covered by a plain test.
This commit is contained in:
vjrj 2026-07-10 12:47:48 +02:00
parent 458502b12b
commit 48228c0bc6
7 changed files with 149 additions and 13 deletions

View file

@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../i18n/strings.g.dart';
import '../services/message_store.dart';
import '../services/profile_cache.dart';
import '../services/social_service.dart';
import '../services/social_settings.dart';
import '../state/messages_cubit.dart';
@ -20,6 +21,7 @@ class ChatScreen extends StatefulWidget {
required this.settings,
required this.peerPubkey,
this.messageStore,
this.profileCache,
super.key,
});
@ -30,6 +32,9 @@ class ChatScreen extends StatefulWidget {
/// Optional persistence for chat history (keystore-backed); null in tests.
final MessageStore? messageStore;
/// Optional cache of peer display names; null in tests.
final ProfileCache? profileCache;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
@ -39,6 +44,7 @@ class _ChatScreenState extends State<ChatScreen> {
MessagesCubit? _messages;
TrustCubit? _trust;
bool _loading = true;
String? _peerName;
final _input = TextEditingController();
@override
@ -59,6 +65,7 @@ class _ChatScreenState extends State<ChatScreen> {
session = null;
}
}
final cachedName = await widget.profileCache?.name(widget.peerPubkey);
if (!mounted) {
await session?.close();
return;
@ -77,8 +84,25 @@ class _ChatScreenState extends State<ChatScreen> {
_session = session;
_messages = messages;
_trust = trust;
_peerName = cachedName;
_loading = false;
});
// Freshen the peer's name from their published profile.
final live = session;
if (live != null && widget.profileCache != null) {
unawaited(() async {
try {
final profile = await live.profile.fetch(widget.peerPubkey);
if (profile != null && profile.name.isNotEmpty && mounted) {
await widget.profileCache!.setName(widget.peerPubkey, profile.name);
setState(() => _peerName = profile.name);
}
} catch (_) {
// best effort
}
}());
}
}
Future<void> _send() async {
@ -100,11 +124,12 @@ class _ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
final t = context.t;
final messages = _messages;
final trust = _trust;
return Scaffold(
appBar: AppBar(title: Text(t.chat.title)),
appBar: AppBar(
title: Text(_peerName ?? shortPubkey(widget.peerPubkey)),
),
body: _loading || messages == null || trust == null
? const Center(child: CircularProgressIndicator())
: MultiBlocProvider(