make members controller an asyncnotifier

makes loading smoother and more responsive
This commit is contained in:
Henry Hiles 2026-03-22 16:46:48 -04:00
commit 237886971c
No known key found for this signature in database
4 changed files with 92 additions and 81 deletions

View file

@ -12,8 +12,8 @@ class AuthorController extends AsyncNotifier<Membership> {
@override @override
Future<Membership> build() async { Future<Membership> build() async {
var member = ref.watch( var member = await ref.watch(
MembersController.provider(config.room).select( MembersController.provider(config.room).selectAsync(
(value) => value.firstWhereOrNull( (value) => value.firstWhereOrNull(
(membership) => membership.userId == config.message.authorId, (membership) => membership.userId == config.message.authorId,
), ),

View file

@ -7,47 +7,35 @@ import "package:nexus/models/membership.dart";
import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
class MembersController extends Notifier<IList<Membership>> { class MembersController extends AsyncNotifier<IList<Membership>> {
final Room room; final Room room;
MembersController(this.room); MembersController(this.room);
@override @override
IList<Membership> build() { Future<IList<Membership>> build() async {
IList<Membership> membersFromState(IList<Event> members) => members.nonNulls if (room.metadata == null) return const IList.empty();
final state = await ref
.watch(ClientController.provider.notifier)
.getRoomState(
GetRoomStateRequest(
roomId: room.metadata!.id,
fetchMembers: room.metadata!.hasMemberList == false,
includeMembers: true,
),
);
return state.nonNulls
.where((member) => member.content["membership"] == "join") .where((member) => member.content["membership"] == "join")
.map( .map(
(membership) => (membership) =>
Membership.fromContent(membership.content, membership.stateKey!), Membership.fromContent(membership.content, membership.stateKey!),
) )
.toIList(); .toIList();
if (room.metadata != null) {
ref
.watch(ClientController.provider.notifier)
.getRoomState(
GetRoomStateRequest(
roomId: room.metadata!.id,
fetchMembers: room.metadata!.hasMemberList == false,
includeMembers: true,
),
)
.then((value) => state = membersFromState(value));
}
return membersFromState(
(room.state["m.room.members"]?.values ?? [])
.map(
(eventRowId) => room.events.firstWhereOrNull(
(event) => event.rowId == eventRowId,
),
)
.nonNulls
.toIList(),
);
} }
static final provider = static final provider =
NotifierProvider.family<MembersController, IList<Membership>, Room>( AsyncNotifierProvider.family<MembersController, IList<Membership>, Room>(
MembersController.new, MembersController.new,
); );
} }

View file

@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
@ -31,45 +32,47 @@ class MentionOverlay extends ConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
child: switch (triggerCharacter) { child: switch (triggerCharacter) {
"@" => Consumer( "@" =>
builder: (_, ref, _) { ref
final members = ref.watch(MembersController.provider(room)); .watch(MembersController.provider(room))
return ListView( .betterWhen(
children: data: (members) => ListView(
(query.isEmpty children:
? members (query.isEmpty
: members.where( ? members
(member) => : members.where(
member.userId.toLowerCase().contains( (member) =>
query.toLowerCase(), member.userId.toLowerCase().contains(
) == query.toLowerCase(),
true || ) ==
member.displayName.toLowerCase().contains( true ||
query.toLowerCase(), member.displayName
) == .toLowerCase()
true, .contains(
)) query.toLowerCase(),
.map( ) ==
(member) => ListTile( true,
leading: AvatarOrHash( ))
member.avatarUrl, .map(
member.displayName, (member) => ListTile(
), leading: AvatarOrHash(
title: Text(member.displayName), member.avatarUrl,
subtitle: Text(member.userId), member.displayName,
onTap: () => addTag( ),
id: "[@${member.displayName}](https://matrix.to/#/${member.userId})", title: Text(member.displayName),
name: member.userId subtitle: Text(member.userId),
.substring(1) onTap: () => addTag(
.split(":") id: "[@${member.displayName}](https://matrix.to/#/${member.userId})",
.first, name: member.userId
), .substring(1)
), .split(":")
) .first,
.toList(), ),
); ),
}, )
), .toList(),
),
),
"#" => ListView( "#" => ListView(
children: children:
(query.isEmpty (query.isEmpty

View file

@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/members_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
@ -10,15 +11,17 @@ class MemberList extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final members = ref.watch(MembersController.provider(room)); final membersProvider = ref.watch(MembersController.provider(room));
return Drawer( return Drawer(
shape: Border(), shape: Border(),
child: ListView( child: Column(
children: [ children: [
AppBar( AppBar(
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
leading: Icon(Icons.people), leading: Icon(Icons.people),
title: Text("Members (${members.length})"), title: Text(
"Members ${membersProvider.when(data: (members) => "${members.length}", error: (_, _) => "", loading: () => "")}",
),
actionsPadding: EdgeInsets.only(right: 4), actionsPadding: EdgeInsets.only(right: 4),
actions: [ actions: [
if (Scaffold.of(context).hasEndDrawer) if (Scaffold.of(context).hasEndDrawer)
@ -29,16 +32,33 @@ class MemberList extends ConsumerWidget {
), ),
], ],
), ),
...members.map( membersProvider.betterWhen(
(member) => ListTile( data: (members) => Expanded(
onTap: () => showDialog( child: ListView(
context: context, children: members
builder: (context) => .map(
Dialog(child: Text("TODO: Open member popover")), (member) => ListTile(
onTap: () => showDialog(
context: context,
builder: (context) =>
Dialog(child: Text("TODO: Open member popover")),
),
leading: AvatarOrHash(
member.avatarUrl,
member.displayName,
),
title: Text(
member.displayName,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
member.userId,
overflow: TextOverflow.ellipsis,
),
),
)
.toList(),
), ),
leading: AvatarOrHash(member.avatarUrl, member.displayName),
title: Text(member.displayName, overflow: TextOverflow.ellipsis),
subtitle: Text(member.userId, overflow: TextOverflow.ellipsis),
), ),
), ),
], ],