more performant member list

This commit is contained in:
Henry Hiles 2026-06-06 10:51:56 -04:00
commit 457de3c77c
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 99 additions and 88 deletions

View file

@ -8,12 +8,13 @@ import "package:nexus/models/content/create.dart";
import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/content/power_levels.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
class MembersGroupedController extends AsyncNotifier<IMap<int?, ISet<Event>>> { class MembersGroupedController
extends AsyncNotifier<IList<MapEntry<int?, ISet<Event>>>> {
final MembersByStatusConfig config; final MembersByStatusConfig config;
MembersGroupedController(this.config); MembersGroupedController(this.config);
@override @override
Future<IMap<int?, ISet<Event>>> build() async { Future<IList<MapEntry<int?, ISet<Event>>>> build() async {
final room = ref.watch( final room = ref.watch(
RoomsController.provider.select((value) => value[config.roomId]), RoomsController.provider.select((value) => value[config.roomId]),
); );
@ -42,23 +43,28 @@ class MembersGroupedController extends AsyncNotifier<IMap<int?, ISet<Event>>> {
MembersByStatusController.provider(config).future, MembersByStatusController.provider(config).future,
); );
return members.fold<IMap<int?, ISet<Event>>>(.new(), (result, event) { return members
final groupKey = creators?.contains(event.stateKey!) == true .fold<IMap<int?, ISet<Event>>>(.new(), (result, event) {
? null final groupKey = creators?.contains(event.stateKey!) == true
: content.users[event.stateKey!] ?? content.usersDefault; ? null
: content.users[event.stateKey!] ?? content.usersDefault;
return result.update( return result.update(
groupKey, groupKey,
(value) => value.add(event), (value) => value.add(event),
ifAbsent: () => .new({event}), ifAbsent: () => .new({event}),
); );
}); })
.toEntryIList(
compare: (a, b) =>
(b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity),
);
} }
static final provider = static final provider =
AsyncNotifierProvider.family< AsyncNotifierProvider.family<
MembersGroupedController, MembersGroupedController,
IMap<int?, ISet<Event>>, IList<MapEntry<int?, ISet<Event>>>,
MembersByStatusConfig MembersByStatusConfig
>(MembersGroupedController.new); >(MembersGroupedController.new);
} }

View file

@ -3,7 +3,8 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:m3e_buttons/m3e_buttons.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/controllers/members_grouped_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/helpers/extensions/string_to_color.dart"; import "package:nexus/helpers/extensions/string_to_color.dart";
@ -20,19 +21,14 @@ class MemberList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final status = useState(MembershipStatus.join); final statusIndex = useState(0);
final membersData = ref.watch(
MembersGroupedController.provider(
.new(roomId: roomId, status: status.value),
),
);
final options = <String, MembershipStatus>{ final options = <String, MembershipStatus>{
"Joined": .join, "Joined": .join,
"Invited": .invite, "Invited": .invite,
"Banned": .ban, "Banned": .ban,
}; };
final status = options.values.toIList()[statusIndex.value];
return Drawer( return Drawer(
shape: Border(), shape: Border(),
@ -54,16 +50,16 @@ class MemberList extends HookConsumerWidget {
), ),
M3EToggleButtonGroup( M3EToggleButtonGroup(
type: .connected, type: .connected,
selectedIndex: options.values.toIList().indexOf(status.value), selectedIndex: statusIndex.value,
onSelectedIndexChanged: (index) => onSelectedIndexChanged: (index) =>
status.value = options.values.elementAt(index ?? 0), statusIndex.value = index ?? statusIndex.value,
// overflow: M3EButtonGroupOverflow.menu, // overflow: M3EButtonGroupOverflow.menu,
actions: options actions: options
.mapTo( .mapTo(
(name, value) => M3EToggleButtonGroupAction( (name, value) => M3EToggleButtonGroupAction(
checkedLabel: Text( checkedLabel: Text(
"$name${switch (membersData) { "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) {
AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.values.expand((element) => element).length})", AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})",
_ => "", _ => "",
}}", }}",
), ),
@ -73,7 +69,11 @@ class MemberList extends HookConsumerWidget {
.toList(), .toList(),
), ),
switch (membersData) { switch (ref.watch(
MembersGroupedController.provider(
.new(roomId: roomId, status: status),
),
)) {
AsyncError(:final error, :final stackTrace) => ErrorDialog( AsyncError(:final error, :final stackTrace) => ErrorDialog(
error, error,
stackTrace, stackTrace,
@ -84,71 +84,76 @@ class MemberList extends HookConsumerWidget {
child: Padding( child: Padding(
padding: .symmetric(vertical: 18), padding: .symmetric(vertical: 18),
child: Text( 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, style: Theme.of(context).textTheme.headlineSmall,
), ),
), ),
) )
: Expanded( : Expanded(
child: ListView( child: CustomScrollView(
padding: .all(12), slivers: [
children: [
for (final MapEntry(key: powerLevel, value: members) for (final MapEntry(key: powerLevel, value: members)
in value.toEntryIList( in value) ...[
compare: (a, b) => (b?.key ?? double.infinity) SliverToBoxAdapter(
.compareTo(a?.key ?? double.infinity), child: Padding(
)) ...[ padding: .symmetric(horizontal: 16),
Padding( child: DividerText(
padding: .symmetric(horizontal: 4), powerLevel == null
child: DividerText( ? "Creators"
powerLevel == null : "Power Level $powerLevel",
? "Creators" ),
: "Power Level $powerLevel",
), ),
), ),
SegmentedListSection( SliverM3ECardList(
children: members padding: .all(4),
.map( color: Theme.of(
(member) => switch (member.content) { context,
MembershipContent( ).colorScheme.surfaceContainerHigh,
:final avatarUrl, margin: .symmetric(horizontal: 12, vertical: 4),
:final displayName, itemCount: members.length,
) => itemBuilder: (context, index) =>
SegmentedListTile( switch (members[index].content) {
onTap: () {}, MembershipContent(
// context.showUserPopover( :final avatarUrl,
// member.content as MembershipContent, :final displayName,
// member.stateKey!, ) =>
// roomId: roomId, ListTile(
// globalPosition: details.globalPosition, title: Text(
// ), displayName ??
title: Text( members[index]
displayName ?? .stateKey!
member.stateKey!.localpart, .localpart,
overflow: .ellipsis, overflow: .ellipsis,
style: .new( style: .new(
color: member.stateKey!.colorHash, color: members[index]
fontWeight: .bold, .stateKey!
), .colorHash,
), fontWeight: .bold,
subtitle: Text(
member.stateKey!,
overflow: .ellipsis,
),
leading: AvatarOrHash(
avatarUrl,
displayName ??
member.sender.localpart,
), ),
), ),
_ => throw Exception( subtitle: Text(
"Member content was not MembershipContent", members[index].stateKey!,
overflow: .ellipsis,
),
leading: AvatarOrHash(
avatarUrl,
displayName ??
members[index].sender.localpart,
),
), ),
}, _ => throw Exception(
) "Member content was not MembershipContent",
.toList(), ),
},
onTap: (index) {
// context.showUserPopover(
// member.content as MembershipContent,
// member.stateKey!,
// roomId: roomId,
// globalPosition: details.globalPosition,
// ),
},
), ),
SizedBox(height: 4),
], ],
], ],
), ),

View file

@ -783,6 +783,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.3" 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: m3e_design:
dependency: transitive dependency: transitive
description: description:
@ -807,14 +815,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" 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: measure_size:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -62,12 +62,12 @@ dependencies:
media_kit_video: 2.0.1 media_kit_video: 2.0.1
media_kit_libs_video: 1.0.7 media_kit_libs_video: 1.0.7
measure_size: ^5.0.2 measure_size: ^5.0.2
material_segmented_list: ^1.0.5
m3e_buttons: ^0.0.3 m3e_buttons: ^0.0.3
navigation_rail_m3e: navigation_rail_m3e:
git: git:
url: https://github.com/Henry-Hiles/material_3_expressive url: https://github.com/Henry-Hiles/material_3_expressive
path: packages/navigation_rail_m3e path: packages/navigation_rail_m3e
m3e_card_list: ^0.1.0
dev_dependencies: dev_dependencies:
build_runner: 2.15.0 build_runner: 2.15.0