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