feat(chat): lighter bubbles, my own name on my avatar

Feedback: my bubbles were too dark, and avatars showed only a person
glyph, never the profile name. Now both bubbles are light with dark text
(mine a tonal green, the peer's white with a hairline) — the side and the
per-person avatar colour carry the "who", so nothing is heavy to read.
Load my own display name from ProfileStore so my avatar shows my initial
too, not just the peer's; each avatar still falls back to a glyph when no
name is known.
This commit is contained in:
vjrj 2026-07-11 07:08:54 +02:00
parent 73e96a3ce5
commit d481e62ff7

View file

@ -11,6 +11,7 @@ import '../domain/chat_timeline.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import '../services/message_store.dart'; import '../services/message_store.dart';
import '../services/profile_cache.dart'; import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/social_connection.dart'; import '../services/social_connection.dart';
import '../services/social_service.dart'; import '../services/social_service.dart';
import '../services/trust_referents.dart'; import '../services/trust_referents.dart';
@ -62,6 +63,7 @@ class _ChatScreenState extends State<ChatScreen> {
TrustCubit? _trust; TrustCubit? _trust;
bool _loading = true; bool _loading = true;
String? _peerName; String? _peerName;
String? _selfName;
String? _peerG1; String? _peerG1;
final _input = TextEditingController(); final _input = TextEditingController();
@ -88,6 +90,10 @@ class _ChatScreenState extends State<ChatScreen> {
// connected / unreachable) null transports and the screen degrades. // connected / unreachable) null transports and the screen degrades.
final session = await widget.connection.session(); final session = await widget.connection.session();
final cachedName = await widget.profileCache?.name(widget.peerPubkey); 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 referents = await widget.trustReferents?.all() ?? const <String>{}; final referents = await widget.trustReferents?.all() ?? const <String>{};
final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter; final wotParams = await widget.wotSettings?.params() ?? WotParams.duniter;
if (!mounted) return; // shared session is owned by the connection, not us if (!mounted) return; // shared session is owned by the connection, not us
@ -110,6 +116,7 @@ class _ChatScreenState extends State<ChatScreen> {
_messages = messages; _messages = messages;
_trust = trust; _trust = trust;
_peerName = cachedName; _peerName = cachedName;
_selfName = selfName.isEmpty ? null : selfName;
_loading = false; _loading = false;
}); });
@ -212,6 +219,7 @@ class _ChatScreenState extends State<ChatScreen> {
controller: _input, controller: _input,
onSend: _send, onSend: _send,
peerName: _peerName, peerName: _peerName,
selfName: _selfName,
), ),
), ),
); );
@ -223,11 +231,13 @@ class _ChatBody extends StatelessWidget {
required this.controller, required this.controller,
required this.onSend, required this.onSend,
this.peerName, this.peerName,
this.selfName,
}); });
final TextEditingController controller; final TextEditingController controller;
final Future<void> Function() onSend; final Future<void> Function() onSend;
final String? peerName; final String? peerName;
final String? selfName;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -256,7 +266,9 @@ class _ChatBody extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
const _TrustBanner(), const _TrustBanner(),
Expanded(child: ChatMessageList(peerName: peerName)), Expanded(
child: ChatMessageList(peerName: peerName, selfName: selfName),
),
_Composer(controller: controller, onSend: onSend), _Composer(controller: controller, onSend: onSend),
], ],
), ),
@ -269,11 +281,14 @@ class _ChatBody extends StatelessWidget {
/// in place instead of landing below the fold. Reads the [MessagesCubit] from /// in place instead of landing below the fold. Reads the [MessagesCubit] from
/// context. /// context.
class ChatMessageList extends StatelessWidget { class ChatMessageList extends StatelessWidget {
const ChatMessageList({this.peerName, super.key}); const ChatMessageList({this.peerName, this.selfName, super.key});
/// The peer's display name, for their avatar's initial (null a glyph). /// The peer's display name, for their avatar's initial (null a glyph).
final String? peerName; final String? peerName;
/// My own display name, for my avatar's initial (null → a glyph).
final String? selfName;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = context.t; final t = context.t;
@ -302,6 +317,7 @@ class ChatMessageList extends StatelessWidget {
message: message, message: message,
mine: cubit.isMine(message), mine: cubit.isMine(message),
peerName: peerName, peerName: peerName,
selfName: selfName,
selfPubkey: cubit.selfPubkey, selfPubkey: cubit.selfPubkey,
), ),
}; };
@ -417,22 +433,24 @@ class _MessageRow extends StatelessWidget {
required this.message, required this.message,
required this.mine, required this.mine,
this.peerName, this.peerName,
this.selfName,
this.selfPubkey, this.selfPubkey,
}); });
final PrivateMessage message; final PrivateMessage message;
final bool mine; final bool mine;
final String? peerName; final String? peerName;
final String? selfName;
/// My own pubkey, so my messages carry my (distinctly coloured) avatar. /// My own pubkey, so my messages carry my (distinctly coloured) avatar.
final String? selfPubkey; final String? selfPubkey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// My avatar has no name (it's me) → a glyph on my colour; the peer's shows // Each avatar shows its owner's initial (falling back to a glyph when no
// their initial on theirs. Different pubkeys different colours. // name is known) on a colour derived from their pubkey.
final avatar = mine final avatar = mine
? PeerAvatar(pubkey: selfPubkey ?? message.fromPubkey) ? PeerAvatar(pubkey: selfPubkey ?? message.fromPubkey, name: selfName)
: PeerAvatar(pubkey: message.fromPubkey, name: peerName); : PeerAvatar(pubkey: message.fromPubkey, name: peerName);
final bubble = Flexible( final bubble = Flexible(
child: _Bubble(message: message, mine: mine), child: _Bubble(message: message, mine: mine),
@ -468,9 +486,13 @@ class _Bubble extends StatelessWidget {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75, maxWidth: MediaQuery.of(context).size.width * 0.75,
), ),
// Soft, light bubbles both ways (dark text): mine a tonal green, the
// peer's white with a hairline. Sides + avatar colours carry the
// "who", so the bubbles themselves stay light and easy on the eye.
decoration: BoxDecoration( decoration: BoxDecoration(
color: mine ? seedGreen : seedPrimaryContainer, color: mine ? seedPrimaryContainer : Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: mine ? null : Border.all(color: seedOutline),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -479,10 +501,7 @@ class _Bubble extends StatelessWidget {
// Long text can be selected/copied useful for addresses, links. // Long text can be selected/copied useful for addresses, links.
SelectableText( SelectableText(
message.text, message.text,
style: TextStyle( style: const TextStyle(color: seedOnSurface, fontSize: 15),
color: mine ? Colors.white : seedOnSurface,
fontSize: 15,
),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// A subtle timestamp, trailing edge (RTL-aware). // A subtle timestamp, trailing edge (RTL-aware).
@ -490,10 +509,7 @@ class _Bubble extends StatelessWidget {
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: Text( child: Text(
time, time,
style: TextStyle( style: const TextStyle(color: seedMuted, fontSize: 11),
color: mine ? Colors.white70 : seedMuted,
fontSize: 11,
),
), ),
), ),
], ],