feat(profile): show other people's avatars in chat, chat list, your people
Follow-up to the avatar feature: peers' published photos/illustrations now actually render where people appear. - CachedAvatar: a PeerAvatar that resolves the person's picture from the ProfileCache (FutureBuilder), falling back to the coloured-initial disc. For list/row sites — one avatar each. - Chat bubbles: the peer's and my own avatar are resolved once into state (_peerPicture/_selfPicture — from the cache / ProfileStore, freshened from the peer's published profile) and threaded to each bubble, so many bubbles of two people don't each hit the cache. - Chat list + your-people rows: swapped the static person-icon disc for CachedAvatar, so photos/illustrations show there too. - Tests: CachedAvatar render modes (cached illustration / initial / no cache). analyze clean; chat + your-people suites green. Market offers show no author avatar today (would be a new element, not a swap) — left out of scope.
This commit is contained in:
parent
ec56819fbf
commit
51d6924464
5 changed files with 150 additions and 16 deletions
|
|
@ -7,6 +7,7 @@ import '../i18n/strings.g.dart';
|
|||
import '../services/inbox_service.dart';
|
||||
import '../services/message_store.dart';
|
||||
import '../services/profile_cache.dart';
|
||||
import 'peer_avatar.dart';
|
||||
import '../services/social_connection.dart';
|
||||
import 'theme.dart';
|
||||
import 'unread_badge.dart';
|
||||
|
|
@ -119,9 +120,11 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
|||
return ListTile(
|
||||
leading: UnreadBadge(
|
||||
peer: c.peerPubkey,
|
||||
child: const CircleAvatar(
|
||||
backgroundColor: seedAvatar,
|
||||
child: Icon(Icons.person, color: seedOnAvatar),
|
||||
child: CachedAvatar(
|
||||
pubkey: c.peerPubkey,
|
||||
name: _names[c.peerPubkey],
|
||||
cache: widget.profileCache,
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
title: Text(_names[c.peerPubkey] ??
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
bool _loading = true;
|
||||
String? _peerName;
|
||||
String? _selfName;
|
||||
String? _peerPicture;
|
||||
String? _selfPicture;
|
||||
String? _peerG1;
|
||||
final _input = TextEditingController();
|
||||
|
||||
|
|
@ -86,10 +88,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
// connected / unreachable) → null transports and the screen degrades.
|
||||
final session = await widget.connection.session();
|
||||
final cachedName = await widget.profileCache?.name(widget.peerPubkey);
|
||||
// My own display name, so my avatar shows my initial (not a bare glyph).
|
||||
final selfName = getIt.isRegistered<ProfileStore>()
|
||||
? await getIt<ProfileStore>().name()
|
||||
: '';
|
||||
final cachedPicture = await widget.profileCache?.picture(widget.peerPubkey);
|
||||
// My own name + avatar, so my side shows me (not a bare glyph).
|
||||
final selfStore = getIt.isRegistered<ProfileStore>()
|
||||
? getIt<ProfileStore>()
|
||||
: null;
|
||||
final selfName = selfStore == null ? '' : await selfStore.name();
|
||||
final selfAvatar = selfStore == null ? '' : await selfStore.avatar();
|
||||
if (!mounted) return; // shared session is owned by the connection, not us
|
||||
final self = widget.social.publicKeyHex;
|
||||
final messages = MessagesCubit(
|
||||
|
|
@ -117,6 +122,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
_rating = rating;
|
||||
_peerName = cachedName;
|
||||
_selfName = selfName.isEmpty ? null : selfName;
|
||||
_peerPicture = cachedPicture;
|
||||
_selfPicture = selfAvatar.isEmpty ? null : selfAvatar;
|
||||
_loading = false;
|
||||
});
|
||||
|
||||
|
|
@ -133,8 +140,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
profile.name,
|
||||
);
|
||||
}
|
||||
if (profile.picture.isNotEmpty) {
|
||||
await widget.profileCache!.setPicture(
|
||||
widget.peerPubkey,
|
||||
profile.picture,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
if (profile.name.isNotEmpty) _peerName = profile.name;
|
||||
if (profile.picture.isNotEmpty) _peerPicture = profile.picture;
|
||||
if (profile.g1.isNotEmpty) _peerG1 = profile.g1;
|
||||
});
|
||||
}
|
||||
|
|
@ -230,6 +244,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
onSend: _send,
|
||||
peerName: _peerName,
|
||||
selfName: _selfName,
|
||||
peerPicture: _peerPicture,
|
||||
selfPicture: _selfPicture,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -242,12 +258,16 @@ class _ChatBody extends StatelessWidget {
|
|||
required this.onSend,
|
||||
this.peerName,
|
||||
this.selfName,
|
||||
this.peerPicture,
|
||||
this.selfPicture,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final Future<void> Function() onSend;
|
||||
final String? peerName;
|
||||
final String? selfName;
|
||||
final String? peerPicture;
|
||||
final String? selfPicture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -278,7 +298,12 @@ class _ChatBody extends StatelessWidget {
|
|||
const _TrustBanner(),
|
||||
const _RatingStrip(),
|
||||
Expanded(
|
||||
child: ChatMessageList(peerName: peerName, selfName: selfName),
|
||||
child: ChatMessageList(
|
||||
peerName: peerName,
|
||||
selfName: selfName,
|
||||
peerPicture: peerPicture,
|
||||
selfPicture: selfPicture,
|
||||
),
|
||||
),
|
||||
_Composer(controller: controller, onSend: onSend),
|
||||
],
|
||||
|
|
@ -292,7 +317,13 @@ class _ChatBody extends StatelessWidget {
|
|||
/// in place instead of landing below the fold. Reads the [MessagesCubit] from
|
||||
/// context.
|
||||
class ChatMessageList extends StatelessWidget {
|
||||
const ChatMessageList({this.peerName, this.selfName, super.key});
|
||||
const ChatMessageList({
|
||||
this.peerName,
|
||||
this.selfName,
|
||||
this.peerPicture,
|
||||
this.selfPicture,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The peer's display name, for their avatar's initial (null → a glyph).
|
||||
final String? peerName;
|
||||
|
|
@ -300,6 +331,10 @@ class ChatMessageList extends StatelessWidget {
|
|||
/// My own display name, for my avatar's initial (null → a glyph).
|
||||
final String? selfName;
|
||||
|
||||
/// The peer's / my published avatar (null → the coloured-initial disc).
|
||||
final String? peerPicture;
|
||||
final String? selfPicture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
|
|
@ -329,6 +364,8 @@ class ChatMessageList extends StatelessWidget {
|
|||
mine: cubit.isMine(message),
|
||||
peerName: peerName,
|
||||
selfName: selfName,
|
||||
peerPicture: peerPicture,
|
||||
selfPicture: selfPicture,
|
||||
selfPubkey: cubit.selfPubkey,
|
||||
),
|
||||
};
|
||||
|
|
@ -494,6 +531,8 @@ class _MessageRow extends StatelessWidget {
|
|||
required this.mine,
|
||||
this.peerName,
|
||||
this.selfName,
|
||||
this.peerPicture,
|
||||
this.selfPicture,
|
||||
this.selfPubkey,
|
||||
});
|
||||
|
||||
|
|
@ -501,17 +540,27 @@ class _MessageRow extends StatelessWidget {
|
|||
final bool mine;
|
||||
final String? peerName;
|
||||
final String? selfName;
|
||||
final String? peerPicture;
|
||||
final String? selfPicture;
|
||||
|
||||
/// My own pubkey, so my messages carry my (distinctly coloured) avatar.
|
||||
final String? selfPubkey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Each avatar shows its owner's initial (falling back to a glyph when no
|
||||
// name is known) on a colour derived from their pubkey.
|
||||
// Each avatar shows its owner's photo/illustration when set, else their
|
||||
// initial (or a glyph) on a colour derived from their pubkey.
|
||||
final avatar = mine
|
||||
? PeerAvatar(pubkey: selfPubkey ?? message.fromPubkey, name: selfName)
|
||||
: PeerAvatar(pubkey: message.fromPubkey, name: peerName);
|
||||
? PeerAvatar(
|
||||
pubkey: selfPubkey ?? message.fromPubkey,
|
||||
name: selfName,
|
||||
picture: selfPicture,
|
||||
)
|
||||
: PeerAvatar(
|
||||
pubkey: message.fromPubkey,
|
||||
name: peerName,
|
||||
picture: peerPicture,
|
||||
);
|
||||
final bubble = Flexible(
|
||||
child: _Bubble(message: message, mine: mine),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../services/offer_thumbnail.dart' show decodeDataUri;
|
||||
import '../services/profile_cache.dart';
|
||||
import 'avatar.dart';
|
||||
import 'seed_glyph.dart';
|
||||
|
||||
|
|
@ -71,6 +72,43 @@ class PeerAvatar extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// A [PeerAvatar] that looks the person's published avatar up from the
|
||||
/// [ProfileCache] (their kind:0 `picture`), falling back to the coloured-initial
|
||||
/// disc while it loads or when none is cached / no cache is available. Use at
|
||||
/// list/row sites (one avatar each); for many avatars of the same few people
|
||||
/// (chat bubbles) resolve the picture once into state instead.
|
||||
class CachedAvatar extends StatelessWidget {
|
||||
const CachedAvatar({
|
||||
required this.pubkey,
|
||||
this.name,
|
||||
this.cache,
|
||||
this.radius = 14,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String pubkey;
|
||||
final String? name;
|
||||
final ProfileCache? cache;
|
||||
final double radius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = cache;
|
||||
if (c == null) {
|
||||
return PeerAvatar(pubkey: pubkey, name: name, radius: radius);
|
||||
}
|
||||
return FutureBuilder<String?>(
|
||||
future: c.picture(pubkey),
|
||||
builder: (context, snap) => PeerAvatar(
|
||||
pubkey: pubkey,
|
||||
name: name,
|
||||
picture: snap.data,
|
||||
radius: radius,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A deterministic, readable avatar colour derived from [pubkey]. Hashes the
|
||||
/// key to a hue, then fixes saturation/lightness so white text stays legible on
|
||||
/// top. Pure and stable — exposed for testing.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
|||
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/profile_cache.dart';
|
||||
import 'peer_avatar.dart';
|
||||
import '../services/social_connection.dart';
|
||||
import '../services/social_service.dart';
|
||||
import 'theme.dart';
|
||||
|
|
@ -151,7 +152,9 @@ class _YourPeopleScreenState extends State<YourPeopleScreen> {
|
|||
for (final peer in _youVouchFor)
|
||||
_PersonTile(
|
||||
key: Key('yourPeople.given.$peer'),
|
||||
pubkey: peer,
|
||||
name: _names[peer] ?? shortPubkey(peer),
|
||||
cache: widget.profileCache,
|
||||
onOpen: () => context.push('/chat/$peer'),
|
||||
trailing: TextButton(
|
||||
onPressed: _busy ? null : () => _revoke(peer),
|
||||
|
|
@ -166,7 +169,9 @@ class _YourPeopleScreenState extends State<YourPeopleScreen> {
|
|||
for (final peer in _vouchForYou)
|
||||
_PersonTile(
|
||||
key: Key('yourPeople.received.$peer'),
|
||||
pubkey: peer,
|
||||
name: _names[peer] ?? shortPubkey(peer),
|
||||
cache: widget.profileCache,
|
||||
onOpen: () => context.push('/chat/$peer'),
|
||||
),
|
||||
],
|
||||
|
|
@ -217,20 +222,26 @@ class _PersonTile extends StatelessWidget {
|
|||
const _PersonTile({
|
||||
required this.name,
|
||||
required this.onOpen,
|
||||
required this.pubkey,
|
||||
this.cache,
|
||||
this.trailing,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String pubkey;
|
||||
final String name;
|
||||
final ProfileCache? cache;
|
||||
final VoidCallback onOpen;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: seedAvatar,
|
||||
child: Icon(Icons.person, color: seedOnAvatar),
|
||||
leading: CachedAvatar(
|
||||
pubkey: pubkey,
|
||||
name: name,
|
||||
cache: cache,
|
||||
radius: 20,
|
||||
),
|
||||
title: Text(name),
|
||||
trailing: trailing,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue