From 0653961f9cba1088bba52e18470431b02fb7dd37 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 20 May 2026 11:07:49 -0400 Subject: [PATCH] reactive members controller, better caching, fixes #6, fixes #7 --- lib/controllers/event_controller.dart | 4 +- lib/controllers/members_controller.dart | 49 ++++++++++++++++------- lib/controllers/room_chat_controller.dart | 38 +++++------------- lib/controllers/rooms_controller.dart | 30 ++++++++++++++ lib/models/room.dart | 2 + lib/widgets/room_chat.dart | 4 +- 6 files changed, 82 insertions(+), 45 deletions(-) diff --git a/lib/controllers/event_controller.dart b/lib/controllers/event_controller.dart index 8dfb356..e195f4d 100644 --- a/lib/controllers/event_controller.dart +++ b/lib/controllers/event_controller.dart @@ -11,7 +11,9 @@ class EventController extends AsyncNotifier { @override Future build() async { - final room = ref.watch(RoomsController.provider)[request.roomId]; + final room = ref.watch( + RoomsController.provider.select((value) => value[request.roomId]), + ); final event = room?.events.firstWhereOrNull( (event) => event.eventId == request.eventId, ); diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 91da138..0386776 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,6 +1,8 @@ +import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; +import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/models/content/content.dart"; import "package:nexus/models/event.dart"; @@ -13,28 +15,47 @@ class MembersController extends AsyncNotifier> { SelectedRoomController.provider.select( (value) => value?.metadata == null ? null - : (value!.metadata!.id, value.metadata!.hasMemberList), + : ( + value!.metadata!.id, + value.metadata!.hasMemberList, + value.hasFetchedMembers, + ), ), ); if (data == null) return const IList.empty(); - final state = await ref - .watch(ClientController.provider.notifier) - .getRoomState( - GetRoomStateRequest( - roomId: data.$1, - fetchMembers: data.$2 == false, - includeMembers: true, - ), - ); + if (!data.$3) { + final fetchedState = await ref + .watch(ClientController.provider.notifier) + .getRoomState( + GetRoomStateRequest( + roomId: data.$1, + fetchMembers: data.$2 == false, + includeMembers: true, + ), + ); - return state - .where((state) => state.type == EventType.membership.type) - .toIList(); + ref + .read(RoomsController.provider.notifier) + .addState(data.$1, fetchedState, isMembers: true); + } + + final room = ref.watch( + RoomsController.provider.select((value) => value[data.$1]), + ); + + return room?.state[EventType.membership.type]?.values + .map( + (rowId) => + room.events.firstWhereOrNull((event) => event.rowId == rowId), + ) + .nonNulls + .toIList() ?? + const IList.empty(); } static final provider = - AsyncNotifierProvider>( + AsyncNotifierProvider.autoDispose>( MembersController.new, ); } diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index a13a801..64fbfd7 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -23,39 +23,19 @@ class RoomChatController extends AsyncNotifier> { @override Future> build() async { final client = ref.watch(ClientController.provider.notifier); - - final state = await client.getRoomState( - GetRoomStateRequest(roomId: roomId), - ); - - ref - .read(RoomsController.provider.notifier) - .update( - { - roomId: Room( - events: state, - state: state.fold( - const IMap.empty(), - (previousValue, stateEvent) => previousValue.add( - stateEvent.type, - (previousValue[stateEvent.type] ?? const IMap.empty()).addAll( - IMap({ - if (stateEvent.stateKey != null) - stateEvent.stateKey!: stateEvent.rowId, - }), - ), - ), - ), - ), - }.toIMap(), - const ISet.empty(), - ); - final room = ref.watch( RoomsController.provider.select((rooms) => rooms[roomId]), ); if (room == null) return const IList.empty(); + if (!room.hasFetchedState) { + final state = await client.getRoomState( + GetRoomStateRequest(roomId: roomId), + ); + + ref.read(RoomsController.provider.notifier).addState(roomId, state); + } + // While there are under 30 messages, try up to load more messages until there's no more or we have 20 messages. if (room.hasMore && room.timeline.length < 30) { loadOlder(); @@ -98,7 +78,7 @@ class RoomChatController extends AsyncNotifier> { PaginateRequest( roomId: roomId, maxTimelineId: ref - .read(RoomsController.provider)[roomId] + .read(RoomsController.provider.select((value) => value[roomId])) ?.timeline .firstOrNull ?.timelineRowId, diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 69117f3..40f5627 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,6 +1,7 @@ import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/models/event.dart"; import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/room.dart"; @@ -8,6 +9,30 @@ class RoomsController extends Notifier> { @override IMap build() => const IMap.empty(); + void addState(String roomId, IList state, {bool isMembers = false}) => + update( + { + roomId: Room( + events: state, + hasFetchedState: true, + hasFetchedMembers: isMembers, + state: state.fold( + const IMap.empty(), + (previousValue, stateEvent) => previousValue.add( + stateEvent.type, + (previousValue[stateEvent.type] ?? const IMap.empty()).addAll( + IMap({ + if (stateEvent.stateKey != null) + stateEvent.stateKey!: stateEvent.rowId, + }), + ), + ), + ), + ), + }.toIMap(), + const ISet.empty(), + ); + void update(IMap rooms, ISet leftRooms) { final merged = rooms.entries.fold(state, (acc, entry) { final roomId = entry.key; @@ -34,6 +59,11 @@ class RoomsController extends Notifier> { ), ), ), + reset: false, + hasFetchedMembers: + incoming.hasFetchedMembers || existing.hasFetchedMembers, + hasFetchedState: + incoming.hasFetchedState || existing.hasFetchedState, timeline: (incoming.reset ? incoming.timeline diff --git a/lib/models/room.dart b/lib/models/room.dart index 3c3eec0..98dd6da 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -12,6 +12,8 @@ abstract class Room with _$Room { @JsonKey(name: "meta") RoomMetadata? metadata, @Default(IList.empty()) IList timeline, @Default(false) bool reset, + @Default(false) bool hasFetchedState, + @Default(false) bool hasFetchedMembers, @Default(IMap.empty()) IMap> state, // required IMap accountData, @Default(IList.empty()) IList events, diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 191cfcf..b8e4349 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -80,7 +80,9 @@ class RoomChat extends HookConsumerWidget { if (!scrollController.position.atEdge) return; if (scrollController.position.pixels == 0) { - final room = ref.watch(RoomsController.provider)[roomId]; + final room = ref.watch( + RoomsController.provider.select((value) => value[roomId]), + ); if (room != null) client.markRead(room); } else { await notifier.loadOlder();