From dc40640682dc630ecc9a85c4c9560b75c6e23d1b Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 11 Jul 2026 06:59:24 +0200 Subject: [PATCH] feat(chat): sender avatar on both sides, coloured per person MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feedback: avatars only on the left, all the same colour, so it was hard to tell who said what. In a 1:1 only the peer had an avatar (one colour). Now each message carries its sender's avatar on their own side — mine trailing, the peer's leading — each tinted by a deterministic colour from their pubkey. Two sides, two colours: who-said-what is obvious at a glance. --- apps/app_seeds/lib/ui/chat_screen.dart | 41 ++++++++++++------- .../test/ui/chat_message_list_test.dart | 14 ++++--- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/apps/app_seeds/lib/ui/chat_screen.dart b/apps/app_seeds/lib/ui/chat_screen.dart index 1bdcac8..eb00ab9 100644 --- a/apps/app_seeds/lib/ui/chat_screen.dart +++ b/apps/app_seeds/lib/ui/chat_screen.dart @@ -302,6 +302,7 @@ class ChatMessageList extends StatelessWidget { message: message, mine: cubit.isMine(message), peerName: peerName, + selfPubkey: cubit.selfPubkey, ), }; }, @@ -408,32 +409,44 @@ class _DaySeparator extends StatelessWidget { } } -/// One message line: my messages align to the trailing edge with no avatar; -/// the peer's align to the leading edge with their avatar (1:1 convention). +/// One message line. Each sender gets their own coloured avatar on their side — +/// mine on the trailing edge, the peer's on the leading edge — so at a glance +/// the two sides are two people, not one column of look-alike bubbles. class _MessageRow extends StatelessWidget { - const _MessageRow({required this.message, required this.mine, this.peerName}); + const _MessageRow({ + required this.message, + required this.mine, + this.peerName, + this.selfPubkey, + }); final PrivateMessage message; final bool mine; final String? peerName; + /// My own pubkey, so my messages carry my (distinctly coloured) avatar. + final String? selfPubkey; + @override Widget build(BuildContext context) { - if (mine) { - return Align( - alignment: AlignmentDirectional.centerEnd, - child: _Bubble(message: message, mine: true), - ); - } + // 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. + final avatar = mine + ? PeerAvatar(pubkey: selfPubkey ?? message.fromPubkey) + : PeerAvatar(pubkey: message.fromPubkey, name: peerName); + final bubble = Flexible( + child: _Bubble(message: message, mine: mine), + ); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.end, - children: [ - PeerAvatar(pubkey: message.fromPubkey, name: peerName), - const SizedBox(width: 8), - Flexible(child: _Bubble(message: message, mine: false)), - ], + mainAxisAlignment: mine + ? MainAxisAlignment.end + : MainAxisAlignment.start, + children: mine + ? [bubble, const SizedBox(width: 8), avatar] + : [avatar, const SizedBox(width: 8), bubble], ), ); } diff --git a/apps/app_seeds/test/ui/chat_message_list_test.dart b/apps/app_seeds/test/ui/chat_message_list_test.dart index cf28685..2eea2a2 100644 --- a/apps/app_seeds/test/ui/chat_message_list_test.dart +++ b/apps/app_seeds/test/ui/chat_message_list_test.dart @@ -113,9 +113,7 @@ void main() { expect(find.byKey(const Key('chat.daySeparator')), findsNWidgets(2)); }); - testWidgets('the peer bubble carries an avatar, mine does not', ( - tester, - ) async { + testWidgets('each sender gets a distinctly coloured avatar', (tester) async { final transport = _FakeMessageTransport(); final cubit = MessagesCubit(transport, peerPubkey: peer, selfPubkey: me) ..start(); @@ -126,8 +124,14 @@ void main() { await tester.pumpWidget(host(cubit)); await tester.pump(); - // One avatar, for the single peer message; my own bubble has none. - expect(find.byType(PeerAvatar), findsOneWidget); + // Both sides carry an avatar, and the two people are different colours — + // so you can tell who said what at a glance. + expect(find.byType(PeerAvatar), findsNWidgets(2)); + final discs = tester + .widgetList(find.byType(CircleAvatar)) + .toList(); + expect(discs, hasLength(2)); + expect(discs.first.backgroundColor, isNot(discs.last.backgroundColor)); }); testWidgets('a new message lands in view at the bottom, not below the fold', (