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:
parent
b96b6e45cc
commit
9cc1a5e171
7 changed files with 149 additions and 13 deletions
|
|
@ -1,16 +1,30 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.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 'theme.dart';
|
||||
|
||||
/// The messages inbox: past conversations, newest first. Tapping one opens the
|
||||
/// chat. Reached from the drawer's "Chat" entry.
|
||||
/// The messages inbox: past conversations, newest first. Shows each peer's
|
||||
/// published name (falling back to a short key), and opens the chat on tap.
|
||||
class ChatListScreen extends StatefulWidget {
|
||||
const ChatListScreen({required this.store, super.key});
|
||||
const ChatListScreen({
|
||||
required this.store,
|
||||
this.social,
|
||||
this.settings,
|
||||
this.profileCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final MessageStore store;
|
||||
final SocialService? social;
|
||||
final SocialSettings? settings;
|
||||
final ProfileCache? profileCache;
|
||||
|
||||
@override
|
||||
State<ChatListScreen> createState() => _ChatListScreenState();
|
||||
|
|
@ -18,6 +32,7 @@ class ChatListScreen extends StatefulWidget {
|
|||
|
||||
class _ChatListScreenState extends State<ChatListScreen> {
|
||||
List<ChatSummary>? _items;
|
||||
final Map<String, String> _names = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -27,7 +42,43 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
|||
|
||||
Future<void> _load() async {
|
||||
final items = await widget.store.conversations();
|
||||
if (mounted) setState(() => _items = items);
|
||||
final cache = widget.profileCache;
|
||||
if (cache != null) {
|
||||
for (final c in items) {
|
||||
final name = await cache.name(c.peerPubkey);
|
||||
if (name != null) _names[c.peerPubkey] = name;
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _items = items);
|
||||
unawaited(_resolveMissingNames(items));
|
||||
}
|
||||
|
||||
/// Fetches published names for peers we don't have cached yet, over one
|
||||
/// session. Best effort; the list already shows short keys meanwhile.
|
||||
Future<void> _resolveMissingNames(List<ChatSummary> items) async {
|
||||
final social = widget.social;
|
||||
final settings = widget.settings;
|
||||
final cache = widget.profileCache;
|
||||
if (social == null || settings == null || cache == null) return;
|
||||
final missing =
|
||||
items.map((c) => c.peerPubkey).where((p) => !_names.containsKey(p));
|
||||
if (missing.isEmpty) return;
|
||||
final relays = await settings.relayUrls();
|
||||
if (relays.isEmpty) return;
|
||||
try {
|
||||
final session = await social.openSession(relays);
|
||||
for (final peer in missing) {
|
||||
final profile = await session.profile.fetch(peer);
|
||||
if (profile != null && profile.name.isNotEmpty) {
|
||||
await cache.setName(peer, profile.name);
|
||||
if (mounted) setState(() => _names[peer] = profile.name);
|
||||
}
|
||||
}
|
||||
await session.close();
|
||||
} catch (_) {
|
||||
// offline / unreachable — short keys stay.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -59,7 +110,8 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
|||
backgroundColor: seedAvatar,
|
||||
child: Icon(Icons.person, color: seedOnAvatar),
|
||||
),
|
||||
title: Text(_shortId(c.peerPubkey)),
|
||||
title: Text(_names[c.peerPubkey] ??
|
||||
shortPubkey(c.peerPubkey)),
|
||||
subtitle: Text(
|
||||
c.lastText,
|
||||
maxLines: 1,
|
||||
|
|
@ -72,8 +124,3 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)}';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue