reactive members controller, better caching, fixes #6, fixes #7

This commit is contained in:
Henry Hiles 2026-05-20 11:07:49 -04:00
commit 0653961f9c
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 82 additions and 45 deletions

View file

@ -11,7 +11,9 @@ class EventController extends AsyncNotifier<Event?> {
@override @override
Future<Event?> build() async { Future<Event?> 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( final event = room?.events.firstWhereOrNull(
(event) => event.eventId == request.eventId, (event) => event.eventId == request.eventId,
); );

View file

@ -1,6 +1,8 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.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/controllers/selected_room_controller.dart";
import "package:nexus/models/content/content.dart"; import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
@ -13,12 +15,17 @@ class MembersController extends AsyncNotifier<IList<Event>> {
SelectedRoomController.provider.select( SelectedRoomController.provider.select(
(value) => value?.metadata == null (value) => value?.metadata == null
? null ? null
: (value!.metadata!.id, value.metadata!.hasMemberList), : (
value!.metadata!.id,
value.metadata!.hasMemberList,
value.hasFetchedMembers,
),
), ),
); );
if (data == null) return const IList.empty(); if (data == null) return const IList.empty();
final state = await ref if (!data.$3) {
final fetchedState = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.getRoomState( .getRoomState(
GetRoomStateRequest( GetRoomStateRequest(
@ -28,13 +35,27 @@ class MembersController extends AsyncNotifier<IList<Event>> {
), ),
); );
return state ref
.where((state) => state.type == EventType.membership.type) .read(RoomsController.provider.notifier)
.toIList(); .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 = static final provider =
AsyncNotifierProvider<MembersController, IList<Event>>( AsyncNotifierProvider.autoDispose<MembersController, IList<Event>>(
MembersController.new, MembersController.new,
); );
} }

View file

@ -23,39 +23,19 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
@override @override
Future<IList<Event>> build() async { Future<IList<Event>> build() async {
final client = ref.watch(ClientController.provider.notifier); 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( final room = ref.watch(
RoomsController.provider.select((rooms) => rooms[roomId]), RoomsController.provider.select((rooms) => rooms[roomId]),
); );
if (room == null) return const IList.empty(); 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. // 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) { if (room.hasMore && room.timeline.length < 30) {
loadOlder(); loadOlder();
@ -98,7 +78,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
PaginateRequest( PaginateRequest(
roomId: roomId, roomId: roomId,
maxTimelineId: ref maxTimelineId: ref
.read(RoomsController.provider)[roomId] .read(RoomsController.provider.select((value) => value[roomId]))
?.timeline ?.timeline
.firstOrNull .firstOrNull
?.timelineRowId, ?.timelineRowId,

View file

@ -1,6 +1,7 @@
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.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/read_receipt.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
@ -8,6 +9,30 @@ class RoomsController extends Notifier<IMap<String, Room>> {
@override @override
IMap<String, Room> build() => const IMap.empty(); IMap<String, Room> build() => const IMap.empty();
void addState(String roomId, IList<Event> 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<String, Room> rooms, ISet<String> leftRooms) { void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
final merged = rooms.entries.fold(state, (acc, entry) { final merged = rooms.entries.fold(state, (acc, entry) {
final roomId = entry.key; final roomId = entry.key;
@ -34,6 +59,11 @@ class RoomsController extends Notifier<IMap<String, Room>> {
), ),
), ),
), ),
reset: false,
hasFetchedMembers:
incoming.hasFetchedMembers || existing.hasFetchedMembers,
hasFetchedState:
incoming.hasFetchedState || existing.hasFetchedState,
timeline: timeline:
(incoming.reset (incoming.reset
? incoming.timeline ? incoming.timeline

View file

@ -12,6 +12,8 @@ abstract class Room with _$Room {
@JsonKey(name: "meta") RoomMetadata? metadata, @JsonKey(name: "meta") RoomMetadata? metadata,
@Default(IList.empty()) IList<TimelineRowTuple> timeline, @Default(IList.empty()) IList<TimelineRowTuple> timeline,
@Default(false) bool reset, @Default(false) bool reset,
@Default(false) bool hasFetchedState,
@Default(false) bool hasFetchedMembers,
@Default(IMap.empty()) IMap<String, IMap<String, int>> state, @Default(IMap.empty()) IMap<String, IMap<String, int>> state,
// required IMap<String, AccountData> accountData, // required IMap<String, AccountData> accountData,
@Default(IList.empty()) IList<Event> events, @Default(IList.empty()) IList<Event> events,

View file

@ -80,7 +80,9 @@ class RoomChat extends HookConsumerWidget {
if (!scrollController.position.atEdge) return; if (!scrollController.position.atEdge) return;
if (scrollController.position.pixels == 0) { 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); if (room != null) client.markRead(room);
} else { } else {
await notifier.loadOlder(); await notifier.loadOlder();