From c0f4c053b00543a5498bd8fdfe772445daa46f07 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Fri, 23 Oct 2026 01:10:01 -0400 Subject: [PATCH] Make displayname colors based off of power level Fixes #128 --- lib/helpers/extensions/displayname_color.dart | 13 ++++ .../lazy_loading/message_displayname.dart | 73 ++++++++++--------- lib/widgets/member_list.dart | 70 +++++++++--------- lib/widgets/renderers/membership.dart | 12 ++- 4 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 lib/helpers/extensions/displayname_color.dart diff --git a/lib/helpers/extensions/displayname_color.dart b/lib/helpers/extensions/displayname_color.dart new file mode 100644 index 0000000..6eab735 --- /dev/null +++ b/lib/helpers/extensions/displayname_color.dart @@ -0,0 +1,13 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:material_ui/material_ui.dart"; +import "package:nexus/controllers/room_power_levels.dart"; +import "package:nexus/helpers/extensions/string_to_color.dart"; + +extension DisplaynameColor on WidgetRef { + /// The color of [userId]'s name in [roomId], hashed from their power level, + /// so everyone with the same level shares one. + Color displaynameColor(String roomId, String userId) => watch( + RoomPowerLevelsController.provider(roomId) + .select((levels) => levels.levelOf(userId)), + ).toString().colorHash; +} diff --git a/lib/widgets/lazy_loading/message_displayname.dart b/lib/widgets/lazy_loading/message_displayname.dart index e8e4791..5d3bf19 100644 --- a/lib/widgets/lazy_loading/message_displayname.dart +++ b/lib/widgets/lazy_loading/message_displayname.dart @@ -3,7 +3,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/author.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; +import "package:nexus/helpers/extensions/displayname_color.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/content/membership.dart"; @@ -15,43 +15,44 @@ class const MessageDisplayname( super.key, }) extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch( - AuthorController.provider(event), - )) { - AsyncData(:final MembershipContent? value) || - AsyncLoading(:final value) || - AsyncError(:final value) => InkWell( - onTap: clickable - ? () => context.showUserPopover(event.sender, roomId: event.roomId) - : null, - child: switch ([ - Text( - value?.displayName ?? event.sender.localpart, - style: - style ?? .new(color: event.sender.colorHash, fontWeight: .bold), - maxLines: 1, - overflow: .ellipsis, - ), - if (event.pmp != null) + Widget build(BuildContext context, WidgetRef ref) { + final color = ref.displaynameColor(event.roomId, event.sender); + + return switch (ref.watch(AuthorController.provider(event))) { + AsyncData(:final MembershipContent? value) || + AsyncLoading(:final value) || + AsyncError(:final value) => InkWell( + onTap: clickable + ? () => context.showUserPopover(event.sender, roomId: event.roomId) + : null, + child: switch ([ Text( - "(via ${event.sender})", - style: Theme.of(context).textTheme.labelSmall - ?.copyWith(color: event.sender.colorHash, fontWeight: .bold), + value?.displayName ?? event.sender.localpart, + style: style ?? .new(color: color, fontWeight: .bold), maxLines: 1, overflow: .ellipsis, ), - ]) { - final children when shouldWrap => Wrap( - spacing: 4, - crossAxisAlignment: .center, - children: children, - ), - final children => Row( - spacing: 4, - mainAxisSize: .min, - children: [for (final child in children) Flexible(child: child)], - ), - }, - ), - }; + if (event.pmp != null) + Text( + "(via ${event.sender})", + style: Theme.of(context).textTheme.labelSmall + ?.copyWith(color: color, fontWeight: .bold), + maxLines: 1, + overflow: .ellipsis, + ), + ]) { + final children when shouldWrap => Wrap( + spacing: 4, + crossAxisAlignment: .center, + children: children, + ), + final children => Row( + spacing: 4, + mainAxisSize: .min, + children: [for (final child in children) Flexible(child: child)], + ), + }, + ), + }; + } } diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 731270d..02cc0c6 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -8,7 +8,7 @@ import "package:nexus/controllers/members_by_status.dart"; import "package:nexus/controllers/members_grouped.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; +import "package:nexus/helpers/extensions/displayname_color.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/membership_status.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; @@ -108,42 +108,44 @@ class const MemberList(final String roomId, {super.key}) :final avatarUrl, :final displayName, ) => - Builder( - builder: (context) => ListTile( - onTap: () => - context.showUserPopover( + Consumer( + builder: (context, ref, _) => + ListTile( + onTap: () => + context.showUserPopover( + members[index].stateKey!, + roomId: roomId, + ), + title: Text( + displayName ?? + members[index] + .stateKey! + .localpart, + overflow: .ellipsis, + style: .new( + color: ref.displaynameColor( + roomId, + members[index].stateKey!, + ), + fontWeight: .bold, + ), + ), + subtitle: Text( members[index].stateKey!, - roomId: roomId, + overflow: .ellipsis, + ), + leading: Transform.translate( + offset: .new(0, 2), + child: AvatarOrHash( + avatarUrl, + dimension: 40, + displayName ?? + members[index] + .stateKey! + .localpart, + ), ), - title: Text( - displayName ?? - members[index] - .stateKey! - .localpart, - overflow: .ellipsis, - style: .new( - color: members[index] - .stateKey! - .colorHash, - fontWeight: .bold, ), - ), - subtitle: Text( - members[index].stateKey!, - overflow: .ellipsis, - ), - leading: Transform.translate( - offset: .new(0, 2), - child: AvatarOrHash( - avatarUrl, - dimension: 40, - displayName ?? - members[index] - .stateKey! - .localpart, - ), - ), - ), ), _ => throw Exception( "Member content was not MembershipContent", diff --git a/lib/widgets/renderers/membership.dart b/lib/widgets/renderers/membership.dart index b1811f0..a2cd8cc 100644 --- a/lib/widgets/renderers/membership.dart +++ b/lib/widgets/renderers/membership.dart @@ -1,7 +1,8 @@ import "package:material_ui/material_ui.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/show_user_popover.dart"; -import "package:nexus/helpers/extensions/string_to_color.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/helpers/extensions/displayname_color.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; import "package:nexus/widgets/lazy_loading/message_displayname.dart"; @@ -11,9 +12,9 @@ class const MembershipRenderer( final Event event, { final int? maxLines, super.key, -}) extends StatelessWidget { +}) extends ConsumerWidget { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { assert( event.content is MembershipContent, "Make sure to only pass membership events to MembershipRenderer", @@ -40,7 +41,10 @@ class const MembershipRenderer( overflow: .ellipsis, content.displayName ?? event.stateKey!.localpart, maxLines: 1, - style: .new(color: event.sender.colorHash, fontWeight: .bold), + style: .new( + color: ref.displaynameColor(event.roomId, event.stateKey!), + fontWeight: .bold, + ), ), ), ),