From 457de3c77c05d245b6caee39edf206b8340cb455 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 10:51:56 -0400 Subject: [PATCH] more performant member list --- .../members_grouped_controller.dart | 32 ++-- lib/widgets/member_list.dart | 137 +++++++++--------- pubspec.lock | 16 +- pubspec.yaml | 2 +- 4 files changed, 99 insertions(+), 88 deletions(-) diff --git a/lib/controllers/members_grouped_controller.dart b/lib/controllers/members_grouped_controller.dart index 7cec625..e07bcf3 100644 --- a/lib/controllers/members_grouped_controller.dart +++ b/lib/controllers/members_grouped_controller.dart @@ -8,12 +8,13 @@ import "package:nexus/models/content/create.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/event.dart"; -class MembersGroupedController extends AsyncNotifier>> { +class MembersGroupedController + extends AsyncNotifier>>> { final MembersByStatusConfig config; MembersGroupedController(this.config); @override - Future>> build() async { + Future>>> build() async { final room = ref.watch( RoomsController.provider.select((value) => value[config.roomId]), ); @@ -42,23 +43,28 @@ class MembersGroupedController extends AsyncNotifier>> { MembersByStatusController.provider(config).future, ); - return members.fold>>(.new(), (result, event) { - final groupKey = creators?.contains(event.stateKey!) == true - ? null - : content.users[event.stateKey!] ?? content.usersDefault; + return members + .fold>>(.new(), (result, event) { + final groupKey = creators?.contains(event.stateKey!) == true + ? null + : content.users[event.stateKey!] ?? content.usersDefault; - return result.update( - groupKey, - (value) => value.add(event), - ifAbsent: () => .new({event}), - ); - }); + return result.update( + groupKey, + (value) => value.add(event), + ifAbsent: () => .new({event}), + ); + }) + .toEntryIList( + compare: (a, b) => + (b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity), + ); } static final provider = AsyncNotifierProvider.family< MembersGroupedController, - IMap>, + IList>>, MembersByStatusConfig >(MembersGroupedController.new); } diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index bdac414..06c0b3c 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -3,7 +3,8 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_buttons/m3e_buttons.dart"; -import "package:material_segmented_list/material_segmented_list.dart"; +import "package:m3e_card_list/m3e_card_list.dart"; +import "package:nexus/controllers/members_by_status_controller.dart"; import "package:nexus/controllers/members_grouped_controller.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; @@ -20,19 +21,14 @@ class MemberList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final status = useState(MembershipStatus.join); - - final membersData = ref.watch( - MembersGroupedController.provider( - .new(roomId: roomId, status: status.value), - ), - ); + final statusIndex = useState(0); final options = { "Joined": .join, "Invited": .invite, "Banned": .ban, }; + final status = options.values.toIList()[statusIndex.value]; return Drawer( shape: Border(), @@ -54,16 +50,16 @@ class MemberList extends HookConsumerWidget { ), M3EToggleButtonGroup( type: .connected, - selectedIndex: options.values.toIList().indexOf(status.value), + selectedIndex: statusIndex.value, onSelectedIndexChanged: (index) => - status.value = options.values.elementAt(index ?? 0), + statusIndex.value = index ?? statusIndex.value, // overflow: M3EButtonGroupOverflow.menu, actions: options .mapTo( (name, value) => M3EToggleButtonGroupAction( checkedLabel: Text( - "$name${switch (membersData) { - AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.values.expand((element) => element).length})", + "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) { + AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})", _ => "", }}", ), @@ -73,7 +69,11 @@ class MemberList extends HookConsumerWidget { .toList(), ), - switch (membersData) { + switch (ref.watch( + MembersGroupedController.provider( + .new(roomId: roomId, status: status), + ), + )) { AsyncError(:final error, :final stackTrace) => ErrorDialog( error, stackTrace, @@ -84,71 +84,76 @@ class MemberList extends HookConsumerWidget { child: Padding( padding: .symmetric(vertical: 18), child: Text( - "No ${options.keys.toIList()[options.values.toIList().indexOf(status.value)]} Members", + "No ${options.keys.toIList()[statusIndex.value]} Members", style: Theme.of(context).textTheme.headlineSmall, ), ), ) : Expanded( - child: ListView( - padding: .all(12), - children: [ + child: CustomScrollView( + slivers: [ for (final MapEntry(key: powerLevel, value: members) - in value.toEntryIList( - compare: (a, b) => (b?.key ?? double.infinity) - .compareTo(a?.key ?? double.infinity), - )) ...[ - Padding( - padding: .symmetric(horizontal: 4), - child: DividerText( - powerLevel == null - ? "Creators" - : "Power Level $powerLevel", + in value) ...[ + SliverToBoxAdapter( + child: Padding( + padding: .symmetric(horizontal: 16), + child: DividerText( + powerLevel == null + ? "Creators" + : "Power Level $powerLevel", + ), ), ), - SegmentedListSection( - children: members - .map( - (member) => switch (member.content) { - MembershipContent( - :final avatarUrl, - :final displayName, - ) => - SegmentedListTile( - onTap: () {}, - // context.showUserPopover( - // member.content as MembershipContent, - // member.stateKey!, - // roomId: roomId, - // globalPosition: details.globalPosition, - // ), - title: Text( - displayName ?? - member.stateKey!.localpart, - overflow: .ellipsis, - style: .new( - color: member.stateKey!.colorHash, - fontWeight: .bold, - ), - ), - subtitle: Text( - member.stateKey!, - overflow: .ellipsis, - ), - leading: AvatarOrHash( - avatarUrl, - displayName ?? - member.sender.localpart, + SliverM3ECardList( + padding: .all(4), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + margin: .symmetric(horizontal: 12, vertical: 4), + itemCount: members.length, + itemBuilder: (context, index) => + switch (members[index].content) { + MembershipContent( + :final avatarUrl, + :final displayName, + ) => + ListTile( + title: Text( + displayName ?? + members[index] + .stateKey! + .localpart, + overflow: .ellipsis, + style: .new( + color: members[index] + .stateKey! + .colorHash, + fontWeight: .bold, ), ), - _ => throw Exception( - "Member content was not MembershipContent", + subtitle: Text( + members[index].stateKey!, + overflow: .ellipsis, + ), + leading: AvatarOrHash( + avatarUrl, + displayName ?? + members[index].sender.localpart, + ), ), - }, - ) - .toList(), + _ => throw Exception( + "Member content was not MembershipContent", + ), + }, + onTap: (index) { + // context.showUserPopover( + // member.content as MembershipContent, + // member.stateKey!, + // roomId: roomId, + // globalPosition: details.globalPosition, + // ), + }, ), - SizedBox(height: 4), ], ], ), diff --git a/pubspec.lock b/pubspec.lock index f44a05d..cc7ee27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -783,6 +783,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + m3e_card_list: + dependency: "direct main" + description: + name: m3e_card_list + sha256: d4aba0123cccda40ac80789befa8d355e1dc16aa7dcee910157690b0546d78d6 + url: "https://pub.dev" + source: hosted + version: "0.1.0" m3e_design: dependency: transitive description: @@ -807,14 +815,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" - material_segmented_list: - dependency: "direct main" - description: - name: material_segmented_list - sha256: "384bfd41a78e745397ceff1dd39700961e6a5419ad911d1797bcc13ea3824241" - url: "https://pub.dev" - source: hosted - version: "1.0.5" measure_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c95a5a..d371873 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,12 +62,12 @@ dependencies: media_kit_video: 2.0.1 media_kit_libs_video: 1.0.7 measure_size: ^5.0.2 - material_segmented_list: ^1.0.5 m3e_buttons: ^0.0.3 navigation_rail_m3e: git: url: https://github.com/Henry-Hiles/material_3_expressive path: packages/navigation_rail_m3e + m3e_card_list: ^0.1.0 dev_dependencies: build_runner: 2.15.0